koatty_store
Version:
Cache store for koatty.
1,974 lines (1,971 loc) • 64.4 kB
JavaScript
import { flatten, isNil, union, isUndefined } from 'lodash';
import * as helper from 'koatty_lib';
import { EventEmitter } from 'events';
import { LRUCache } from 'lru-cache';
import AsyncLock from 'async-lock';
import { DefaultLogger } from 'koatty_logger';
import { Cluster, Redis } from 'ioredis';
import genericPool from 'generic-pool';
/*!
* @Author: richen
* @Date: 2026-04-24 08:20:20
* @License: BSD (3-Clause)
* @Copyright (c) - <richenlin(at)gmail.com>
* @HomePage: https://koatty.org/
*/
var __defProp = Object.defineProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
var messages = /* @__PURE__ */ (function(messages2) {
messages2["ok"] = "OK";
messages2["queued"] = "QUEUED";
messages2["pong"] = "PONG";
messages2["noint"] = "ERR value is not an integer or out of range";
messages2["nofloat"] = "ERR value is not an float or out of range";
messages2["nokey"] = "ERR no such key";
messages2["nomultiinmulti"] = "ERR MULTI calls can not be nested";
messages2["nomultiexec"] = "ERR EXEC without MULTI";
messages2["nomultidiscard"] = "ERR DISCARD without MULTI";
messages2["busykey"] = "ERR target key name is busy";
messages2["syntax"] = "ERR syntax error";
messages2["unsupported"] = "MemoryCache does not support that operation";
messages2["wrongTypeOp"] = "WRONGTYPE Operation against a key holding the wrong kind of value";
messages2["wrongPayload"] = "DUMP payload version or checksum are wrong";
messages2["wrongArgCount"] = "ERR wrong number of arguments for '%0' command";
messages2["bitopnotWrongCount"] = "ERR BITOP NOT must be called with a single source key";
messages2["indexOutOfRange"] = "ERR index out of range";
messages2["invalidLexRange"] = "ERR min or max not valid string range item";
messages2["invalidDBIndex"] = "ERR invalid DB index";
messages2["invalidDBIndexNX"] = "ERR invalid DB index, '%0' does not exist";
messages2["mutuallyExclusiveNXXX"] = "ERR XX and NX options at the same time are not compatible";
return messages2;
})({});
var MemoryCache = class extends EventEmitter {
static {
__name(this, "MemoryCache");
}
databases = /* @__PURE__ */ new Map();
options;
currentDBIndex;
connected;
lastSave;
multiMode;
cache;
responseMessages;
ttlCheckTimer = null;
lock;
/**
* Creates an instance of MemoryCache.
* @param {MemoryCacheOptions} options
* @memberof MemoryCache
*/
constructor(options) {
super();
this.options = {
database: 0,
maxKeys: 1e3,
evictionPolicy: "lru",
ttlCheckInterval: 6e4,
maxAge: 1e3 * 60 * 60,
...options
};
this.currentDBIndex = options.database || 0;
this.connected = false;
this.lastSave = Date.now();
this.multiMode = false;
this.responseMessages = [];
this.lock = new AsyncLock();
if (!this.databases.has(this.currentDBIndex)) {
this.databases.set(this.currentDBIndex, this.createLRUCache());
}
this.cache = this.databases.get(this.currentDBIndex);
this.startTTLCheck();
}
/**
* 创建LRU缓存实例
*/
createLRUCache() {
return new LRUCache({
max: this.options.maxKeys || 1e3,
ttl: this.options.maxAge || 1e3 * 60 * 60,
updateAgeOnGet: true,
dispose: /* @__PURE__ */ __name((value, key, reason) => {
this.emit("evict", key, value, reason);
}, "dispose"),
onInsert: /* @__PURE__ */ __name((value, key) => {
this.emit("insert", key, value);
}, "onInsert")
});
}
/**
* 启动TTL检查定时器
*/
startTTLCheck() {
if (this.ttlCheckTimer) {
clearInterval(this.ttlCheckTimer);
}
this.ttlCheckTimer = setInterval(() => {
this.cleanExpiredKeys();
}, this.options.ttlCheckInterval || 6e4);
}
/**
* 清理过期键
*/
cleanExpiredKeys() {
for (const [_dbIndex, cache] of this.databases) {
const keysToDelete = [];
cache.forEach((item, key) => {
if (item.timeout && item.timeout <= Date.now()) {
keysToDelete.push(key);
}
});
keysToDelete.forEach((key) => {
cache.delete(key);
this.emit("expire", key);
});
}
}
/**
* 停止TTL检查
*/
stopTTLCheck() {
if (this.ttlCheckTimer) {
clearInterval(this.ttlCheckTimer);
this.ttlCheckTimer = null;
}
}
/**
* 清理所有资源
* @private
*/
cleanup() {
this.stopTTLCheck();
this.databases.clear();
this.removeAllListeners();
}
/**
*
*
* @returns {*}
* @memberof MemoryCache
*/
createClient() {
if (!this.databases.has(this.options.database)) {
this.databases.set(this.options.database, this.createLRUCache());
}
this.cache = this.databases.get(this.options.database);
this.connected = true;
this.discard(null, true);
this.emit("connect");
this.emit("ready");
return this;
}
/**
*
*
* @returns {*}
* @memberof MemoryCache
*/
quit() {
this.cleanup();
this.connected = false;
this.discard(null, true);
this.emit("end");
return this;
}
/**
*
*
* @returns {*}
* @memberof MemoryCache
*/
end() {
return this.quit();
}
/**
* 获取缓存统计信息
*/
info() {
const stats = {
databases: this.databases.size,
currentDB: this.currentDBIndex,
keys: this.cache ? this.cache.length : 0,
maxKeys: this.options.maxKeys,
hits: 0,
misses: 0,
memory: this.getMemoryUsage()
};
if (this.cache && typeof this.cache.dump === "function") {
const dump = this.cache.dump();
stats.keys = dump.length;
}
return stats;
}
/**
* 估算内存使用量
*/
getMemoryUsage() {
let totalSize = 0;
for (const [, cache] of this.databases) {
cache.forEach((item, key) => {
totalSize += key.length * 2;
totalSize += JSON.stringify(item).length * 2;
});
}
return totalSize;
}
/**
*
*
* @param {string} message
* @param {Function} [callback]
* @returns {*}
* @memberof MemoryCache
*/
echo(message, callback) {
return this._handleCallback(callback, message);
}
/**
*
*
* @param {string} message
* @param {Function} [callback]
* @returns {*}
* @memberof MemoryCache
*/
ping(message, callback) {
message = message || "PONG";
return this._handleCallback(callback, message);
}
/**
*
*
* @param {string} password
* @param {Function} [callback]
* @returns {*}
* @memberof MemoryCache
*/
auth(password, callback) {
return this._handleCallback(callback, "OK");
}
/**
*
*
* @param {number} dbIndex
* @param {Function} [callback]
* @returns {*}
* @memberof MemoryCache
*/
select(dbIndex, callback) {
if (!helper.isNumber(dbIndex)) {
return this._handleCallback(callback, null, "ERR invalid DB index");
}
if (!this.databases.has(dbIndex)) {
this.databases.set(dbIndex, this.createLRUCache());
}
this.multiMode = false;
this.currentDBIndex = dbIndex;
this.cache = this.databases.get(dbIndex);
return this._handleCallback(callback, "OK");
}
// ---------------------------------------
// Keys
// ---------------------------------------
get(key, callback) {
let retVal = null;
if (this._hasKey(key)) {
this._testType(key, "string", true, callback);
retVal = this._getKey(key);
}
return this._handleCallback(callback, retVal);
}
/**
* set(key, value, ttl, pttl, notexist, onlyexist, callback)
*
* @param {string} key
* @param {(string | number)} value
* @param {...any[]} params
* @returns {*}
* @memberof MemoryCache
*/
set(key, value, ...params) {
const retVal = null;
params = flatten(params);
const callback = this._retrieveCallback(params);
let ttl, pttl, notexist, onlyexist;
while (params.length > 0) {
const param = params.shift();
switch (param.toString().toLowerCase()) {
case "nx":
notexist = true;
break;
case "xx":
onlyexist = true;
break;
case "ex":
if (params.length === 0) {
return this._handleCallback(callback, null, "ERR syntax error");
}
ttl = parseInt(params.shift());
if (isNaN(ttl)) {
return this._handleCallback(callback, null, "ERR value is not an integer or out of range");
}
break;
case "px":
if (params.length === 0) {
return this._handleCallback(callback, null, "ERR syntax error");
}
pttl = parseInt(params.shift());
if (isNaN(pttl)) {
return this._handleCallback(callback, null, "ERR value is not an integer or out of range");
}
break;
default:
return this._handleCallback(callback, null, "ERR syntax error");
}
}
if (!isNil(ttl) && !isNil(pttl)) {
return this._handleCallback(callback, null, "ERR syntax error");
}
if (notexist && onlyexist) {
return this._handleCallback(callback, null, "ERR syntax error");
}
pttl = pttl || ttl * 1e3 || null;
if (!isNil(pttl)) {
pttl = Date.now() + pttl;
}
if (this._hasKey(key)) {
this._testType(key, "string", true, callback);
if (notexist) {
return this._handleCallback(callback, retVal);
}
} else if (onlyexist) {
return this._handleCallback(callback, retVal);
}
this.cache.set(key, this._makeKey(value.toString(), "string", pttl));
return this._handleCallback(callback, "OK");
}
/**
*
*
* @param {string} key
* @param {Function} [callback]
* @returns {*}
* @memberof MemoryCache
*/
ttl(key, callback) {
let retVal = this.pttl(key);
if (retVal >= 0 || retVal <= -3) {
retVal = Math.floor(retVal / 1e3);
}
return this._handleCallback(callback, retVal);
}
/**
*
*
* @param {string} key
* @param {number} seconds
* @param {Function} [callback]
* @returns {*}
* @memberof MemoryCache
*/
expire(key, seconds, callback) {
let retVal = 0;
if (this._hasKey(key)) {
const pttl = seconds * 1e3;
this.cache.set(key, {
...this.cache.get(key),
timeout: Date.now() + pttl
});
retVal = 1;
}
return this._handleCallback(callback, retVal);
}
/**
*
*
* @param {...any[]} keys
* @returns {*}
* @memberof MemoryCache
*/
del(...keys) {
let retVal = 0;
const callback = this._retrieveCallback(keys);
keys = flatten(keys);
for (let itr = 0; itr < keys.length; itr++) {
const key = keys[itr];
if (this._hasKey(key)) {
this.cache.delete(key);
retVal++;
}
}
return this._handleCallback(callback, retVal);
}
/**
*
*
* @param {...any[]} keys
* @returns {*}
* @memberof MemoryCache
*/
exists(...keys) {
let retVal = 0;
const callback = this._retrieveCallback(keys);
for (let itr = 0; itr < keys.length; itr++) {
const key = keys[itr];
if (this._hasKey(key)) {
retVal++;
}
}
return this._handleCallback(callback, retVal);
}
/**
*
*
* @param {string} key
* @param {Function} [callback]
* @returns {*}
* @memberof MemoryCache
*/
incr(key, callback) {
const lockKey = `incr:${key}`;
return this.lock.acquire(lockKey, () => {
let retVal = null;
try {
retVal = this._addToKey(key, 1);
} catch (err) {
return this._handleCallback(callback, null, err);
}
return this._handleCallback(callback, retVal);
});
}
/**
*
*
* @param {string} key
* @param {number} amount
* @param {Function} [callback]
* @returns {*}
* @memberof MemoryCache
*/
incrby(key, amount, callback) {
const lockKey = `incrby:${key}`;
return this.lock.acquire(lockKey, () => {
let retVal = null;
try {
retVal = this._addToKey(key, amount);
} catch (err) {
return this._handleCallback(callback, null, err);
}
return this._handleCallback(callback, retVal);
});
}
/**
*
*
* @param {string} key
* @param {Function} [callback]
* @returns {*}
* @memberof MemoryCache
*/
decr(key, callback) {
const lockKey = `decr:${key}`;
return this.lock.acquire(lockKey, () => {
let retVal = null;
try {
retVal = this._addToKey(key, -1);
} catch (err) {
return this._handleCallback(callback, null, err);
}
return this._handleCallback(callback, retVal);
});
}
/**
*
*
* @param {string} key
* @param {number} amount
* @param {Function} [callback]
* @returns {*}
* @memberof MemoryCache
*/
decrby(key, amount, callback) {
const lockKey = `decrby:${key}`;
return this.lock.acquire(lockKey, () => {
let retVal = null;
try {
retVal = this._addToKey(key, 0 - amount);
} catch (err) {
return this._handleCallback(callback, null, err);
}
return this._handleCallback(callback, retVal);
});
}
// ---------------------------------------
// ## Hash ##
// ---------------------------------------
hset(key, field, value, timeout, callback) {
let retVal = 0;
if (this._hasKey(key)) {
this._testType(key, "hash", true, callback);
} else {
this.cache.set(key, this._makeKey({}, "hash"));
}
if (!this._hasField(key, field)) {
retVal = 1;
}
this._setField(key, field, value.toString());
if (typeof timeout === "number") {
const hashObj = this.cache.get(key).value;
if (!hashObj._fieldTTL) {
hashObj._fieldTTL = {};
}
hashObj._fieldTTL[field] = Date.now() + timeout * 1e3;
}
this.persist(key);
return this._handleCallback(callback, retVal);
}
/**
*
*
* @param {string} key
* @param {string} field
* @param {Function} [callback]
* @returns {*}
* @memberof MemoryCache
*/
hget(key, field, callback) {
let retVal = null;
if (this._hasKey(key)) {
this._testType(key, "hash", true, callback);
const hashObj = this._getKey(key);
if (hashObj && hashObj._fieldTTL && hashObj._fieldTTL[field]) {
if (hashObj._fieldTTL[field] <= Date.now()) {
this.hdel(key, field);
return this._handleCallback(callback, null);
}
}
if (this._hasField(key, field)) {
retVal = hashObj[field];
}
}
return this._handleCallback(callback, retVal);
}
/**
*
*
* @param {string} key
* @param {string} field
* @param {Function} [callback]
* @returns {*}
* @memberof MemoryCache
*/
hexists(key, field, callback) {
let retVal = 0;
if (this._hasKey(key)) {
this._testType(key, "hash", true, callback);
const hashObj = this._getKey(key);
if (hashObj && hashObj._fieldTTL && hashObj._fieldTTL[field]) {
if (hashObj._fieldTTL[field] <= Date.now()) {
this.hdel(key, field);
return this._handleCallback(callback, 0);
}
}
if (this._hasField(key, field)) {
retVal = 1;
}
}
return this._handleCallback(callback, retVal);
}
/**
*
*
* @param {string} key
* @param {...any[]} fields
* @returns {*}
* @memberof MemoryCache
*/
hdel(key, ...fields) {
let retVal = 0;
const callback = this._retrieveCallback(fields);
if (this._hasKey(key)) {
this._testType(key, "hash", true, callback);
const hashObj = this.cache.get(key).value;
for (let itr = 0; itr < fields.length; itr++) {
const field = fields[itr];
if (this._hasField(key, field)) {
delete hashObj[field];
if (hashObj._fieldTTL && hashObj._fieldTTL[field]) {
delete hashObj._fieldTTL[field];
}
retVal++;
}
}
}
return this._handleCallback(callback, retVal);
}
/**
*
*
* @param {string} key
* @param {Function} [callback]
* @returns {*}
* @memberof MemoryCache
*/
hlen(key, callback) {
const retVal = this.hkeys(key).length;
return this._handleCallback(callback, retVal);
}
/**
*
*
* @param {string} key
* @param {string} field
* @param {*} value
* @param {Function} [callback]
* @returns {*}
* @memberof MemoryCache
*/
hincrby(key, field, value, callback) {
const lockKey = `hincrby:${key}:${field}`;
return this.lock.acquire(lockKey, () => {
let retVal;
try {
retVal = this._addToField(key, field, value, false);
} catch (err) {
return this._handleCallback(callback, null, err);
}
return this._handleCallback(callback, retVal);
});
}
/**
*
*
* @param {string} key
* @param {Function} [callback]
* @returns {*}
* @memberof MemoryCache
*/
hgetall(key, callback) {
let retVals = {};
if (this._hasKey(key)) {
this._testType(key, "hash", true, callback);
const hashObj = this._getKey(key);
retVals = Object.entries(hashObj).filter(([k]) => k !== "_fieldTTL").reduce((acc, [k, v]) => ({
...acc,
[k]: v
}), {});
}
return this._handleCallback(callback, retVals);
}
/**
*
*
* @param {string} key
* @param {Function} [callback]
* @returns {*}
* @memberof MemoryCache
*/
hkeys(key, callback) {
let retVals = [];
if (this._hasKey(key)) {
this._testType(key, "hash", true, callback);
const hashObj = this._getKey(key);
retVals = Object.keys(hashObj).filter((k) => k !== "_fieldTTL");
}
return this._handleCallback(callback, retVals);
}
/**
*
*
* @param {string} key
* @param {Function} [callback]
* @returns {*}
* @memberof MemoryCache
*/
hvals(key, callback) {
let retVals = [];
if (this._hasKey(key)) {
this._testType(key, "hash", true, callback);
const hashObj = this._getKey(key);
retVals = Object.entries(hashObj).filter(([k]) => k !== "_fieldTTL").map(([, v]) => v);
}
return this._handleCallback(callback, retVals);
}
// ---------------------------------------
// Lists (Array / Queue / Stack)
// ---------------------------------------
/**
*
*
* @param {string} key
* @param {Function} [callback]
* @returns {*}
* @memberof MemoryCache
*/
llen(key, callback) {
let retVal = 0;
if (this._hasKey(key)) {
this._testType(key, "list", true, callback);
retVal = this._getKey(key).length || 0;
}
return this._handleCallback(callback, retVal);
}
/**
*
*
* @param {string} key
* @param {(string | number)} value
* @param {Function} [callback]
* @returns {*}
* @memberof MemoryCache
*/
rpush(key, value, callback) {
let retVal = 0;
if (this._hasKey(key)) {
this._testType(key, "list", true, callback);
} else {
this.cache.set(key, this._makeKey([], "list"));
}
this._getKey(key).push(value.toString());
retVal = this._getKey(key).length;
this.persist(key);
return this._handleCallback(callback, retVal);
}
/**
* List:从左侧推入
* @param key
* @param value
* @param callback
*/
lpush(key, value, callback) {
let retVal = 0;
if (this._hasKey(key)) {
this._testType(key, "list", true, callback);
} else {
this.cache.set(key, this._makeKey([], "list"));
}
const list = this._getKey(key);
retVal = list.unshift(value);
this._setKey(key, list);
return this._handleCallback(callback, retVal);
}
/**
* List:获取指定索引的元素
* @param key
* @param index
* @param callback
*/
lindex(key, index, callback) {
if (!this._hasKey(key)) {
return this._handleCallback(callback, null);
}
this._testType(key, "list", true, callback);
const list = this._getKey(key);
if (index < 0) {
index = list.length + index;
}
const value = index >= 0 && index < list.length ? list[index] : null;
return this._handleCallback(callback, value);
}
/**
*
*
* @param {string} key
* @param {Function} [callback]
* @returns {*}
* @memberof MemoryCache
*/
lpop(key, callback) {
let retVal = null;
if (this._hasKey(key)) {
this._testType(key, "list", true, callback);
const list = this._getKey(key);
if (list.length > 0) {
retVal = list.shift();
this.persist(key);
}
}
return this._handleCallback(callback, retVal);
}
/**
*
*
* @param {string} key
* @param {Function} [callback]
* @returns {*}
* @memberof MemoryCache
*/
rpop(key, callback) {
let retVal = null;
if (this._hasKey(key)) {
this._testType(key, "list", true, callback);
const list = this._getKey(key);
if (list.length > 0) {
retVal = list.pop();
this.persist(key);
}
}
return this._handleCallback(callback, retVal);
}
/**
*
*
* @param {string} key
* @param {number} start
* @param {number} stop
* @param {Function} [callback]
* @returns {*}
* @memberof MemoryCache
*/
lrange(key, start, stop, callback) {
const retVal = [];
if (this._hasKey(key)) {
this._testType(key, "list", true, callback);
const list = this._getKey(key);
const length = list.length;
if (stop < 0) {
stop = length + stop;
}
if (start < 0) {
start = length + start;
}
if (start < 0) {
start = 0;
}
if (stop >= length) {
stop = length - 1;
}
if (stop >= 0 && stop >= start) {
for (let itr = start; itr <= stop; itr++) {
retVal.push(list[itr]);
}
}
}
return this._handleCallback(callback, retVal);
}
// ---------------------------------------
// ## Sets (Unique Lists)##
// ---------------------------------------
/**
*
*
* @param {string} key
* @param {...any[]} members
* @returns {*}
* @memberof MemoryCache
*/
sadd(key, ...members) {
let retVal = 0;
const callback = this._retrieveCallback(members);
if (this._hasKey(key)) {
this._testType(key, "set", true, callback);
} else {
this.cache.set(key, this._makeKey([], "set"));
}
const val = this._getKey(key);
const length = val.length;
const nval = union(val, members);
const newlength = nval.length;
retVal = newlength - length;
this._setKey(key, nval);
return this._handleCallback(callback, retVal);
}
/**
*
*
* @param {string} key
* @param {Function} [callback]
* @returns {*}
* @memberof MemoryCache
*/
scard(key, callback) {
let retVal = 0;
if (this._hasKey(key)) {
this._testType(key, "set", true, callback);
retVal = this._getKey(key).length;
}
return this._handleCallback(callback, retVal);
}
/**
*
*
* @param {string} key
* @param {string} member
* @param {Function} [callback]
* @returns {*}
* @memberof MemoryCache
*/
sismember(key, member, callback) {
let retVal = 0;
if (this._hasKey(key)) {
this._testType(key, "set", true, callback);
const val = this._getKey(key);
if (val.includes(member)) {
retVal = 1;
}
}
return this._handleCallback(callback, retVal);
}
/**
*
*
* @param {string} key
* @param {Function} [callback]
* @returns {*}
* @memberof MemoryCache
*/
smembers(key, callback) {
let retVal = [];
if (this._hasKey(key)) {
this._testType(key, "set", true, callback);
retVal = this._getKey(key);
}
return this._handleCallback(callback, retVal);
}
/**
*
*
* @param {string} key
* @param {number} [count]
* @param {Function} [callback]
* @returns {*}
* @memberof MemoryCache
*/
spop(key, count, callback) {
let retVal = [];
count = count || 1;
if (typeof count === "function") {
callback = count;
count = 1;
}
if (this._hasKey(key)) {
this._testType(key, "set", true, callback);
const val = this._getKey(key);
const keys = Object.keys(val);
const keysLength = keys.length;
if (keysLength) {
if (count >= keysLength) {
retVal = keys;
this.del(key);
} else {
for (let itr = 0; itr < count; itr++) {
const randomNum = Math.floor(Math.random() * keys.length);
retVal.push(keys[randomNum]);
this.srem(key, keys[randomNum]);
}
}
}
}
return this._handleCallback(callback, retVal);
}
/**
*
*
* @param {string} key
* @param {...any[]} members
* @returns {*}
* @memberof MemoryCache
*/
srem(key, ...members) {
let retVal = 0;
const callback = this._retrieveCallback(members);
if (this._hasKey(key)) {
this._testType(key, "set", true, callback);
const val = this._getKey(key);
for (const index in members) {
if (members.hasOwnProperty(index)) {
const member = members[index];
const idx = val.indexOf(member);
if (idx !== -1) {
val.splice(idx, 1);
retVal++;
}
}
}
this._setKey(key, val);
}
return this._handleCallback(callback, retVal);
}
/**
*
*
* @param {string} sourcekey
* @param {string} destkey
* @param {string} member
* @param {Function} [callback]
* @returns {*}
* @memberof MemoryCache
*/
smove(sourcekey, destkey, member, callback) {
let retVal = 0;
if (this._hasKey(sourcekey)) {
this._testType(sourcekey, "set", true, callback);
const val = this._getKey(sourcekey);
const idx = val.indexOf(member);
if (idx !== -1) {
this.sadd(destkey, member);
val.splice(idx, 1);
retVal = 1;
}
}
return this._handleCallback(callback, retVal);
}
// ---------------------------------------
// ## Transactions (Atomic) ##
// ---------------------------------------
// TODO: Transaction Queues watch and unwatch
// https://redis.io/topics/transactions
// This can be accomplished by temporarily swapping this.cache to a temporary copy of the current statement
// holding and then using __.merge on actual this.cache with the temp storage.
discard(callback, silent) {
if (this.multiMode) {
this.cache = this.databases.get(this.currentDBIndex);
this.multiMode = false;
this.responseMessages = [];
}
if (!silent) {
return this._handleCallback(callback, "OK");
}
return null;
}
// ---------------------------------------
// ## Internal - Key ##
// ---------------------------------------
/**
*
*
* @param {string} key
* @param {Function} [callback]
* @returns {*}
* @memberof MemoryCache
*/
pttl(key, _callback) {
let retVal = -2;
if (this._hasKey(key)) {
if (!isNil(this.cache.get(key)?.timeout)) {
retVal = this.cache.get(key).timeout - Date.now();
if (retVal < 0 && retVal > -3) {
retVal = -3;
}
} else {
retVal = -1;
}
}
return retVal;
}
/**
*
*
* @private
* @param {string} key
* @param {Function} [callback]
* @returns {*}
* @memberof MemoryCache
*/
persist(key, callback) {
let retVal = 0;
if (this._hasKey(key)) {
if (!isNil(this._key(key).timeout)) {
this.cache.set(key, {
...this.cache.get(key),
timeout: null
});
retVal = 1;
}
}
return this._handleCallback(callback, retVal);
}
/**
*
*
* @private
* @param {string} key
* @returns {*} {boolean}
* @memberof MemoryCache
*/
_hasKey(key) {
return this.cache.has(key);
}
/**
*
*
* @private
* @param {*} value
* @param {string} type
* @param {number} timeout
* @returns {*}
* @memberof MemoryCache
*/
_makeKey(value, type, timeout) {
return {
value,
type,
timeout: timeout || null,
lastAccess: Date.now()
};
}
/**
*
*
* @private
* @param {string} key
* @returns {*}
* @memberof MemoryCache
*/
_key(key) {
this.cache.get(key).lastAccess = Date.now();
return this.cache.get(key);
}
/**
*
*
* @private
* @param {string} key
* @param {number} amount
* @param {Function} [callback]
* @returns {*}
* @memberof MemoryCache
*/
_addToKey(key, amount, callback) {
let keyValue = 0;
if (isNaN(amount) || isNil(amount)) {
return this._handleCallback(callback, null, "ERR value is not an integer or out of range");
}
if (this._hasKey(key)) {
this._testType(key, "string", true, callback);
keyValue = parseInt(this._getKey(key));
if (isNaN(keyValue) || isNil(keyValue)) {
return this._handleCallback(callback, null, "ERR value is not an integer or out of range");
}
} else {
this.cache.set(key, this._makeKey("0", "string"));
}
const val = keyValue + amount;
this._setKey(key, val.toString());
return val;
}
/**
*
*
* @private
* @param {string} key
* @param {string} type
* @param {boolean} [throwError]
* @param {Function} [callback]
* @returns {*}
* @memberof MemoryCache
*/
_testType(key, type, throwError, callback) {
throwError = !!throwError;
const keyType = this._key(key).type;
if (keyType !== type) {
if (throwError) {
return this._handleCallback(callback, null, "WRONGTYPE Operation against a key holding the wrong kind of value");
}
return false;
}
return true;
}
/**
*
*
* @private
* @param {string} key
* @returns {*}
* @memberof MemoryCache
*/
_getKey(key) {
const _key = this._key(key) || {};
if (_key.timeout && _key.timeout <= Date.now()) {
this.del(key);
return null;
}
return _key.value;
}
/**
*
*
* @private
* @param {string} key
* @param {(number | string)} value
* @memberof MemoryCache
*/
_setKey(key, value) {
this.cache.set(key, {
...this.cache.get(key),
value,
lastAccess: Date.now()
});
}
/**
*
*
* @private
* @param {string} key
* @param {string} field
* @param {number} [amount]
* @param {boolean} [useFloat]
* @param {Function} [callback]
* @returns {*}
* @memberof MemoryCache
*/
_addToField(key, field, amount, useFloat, callback) {
useFloat = useFloat || false;
let fieldValue = useFloat ? 0 : 0;
let value = 0;
if (isNaN(amount) || isNil(amount)) {
return this._handleCallback(callback, null, useFloat ? "ERR value is not an float or out of range" : "ERR value is not an integer or out of range");
}
if (this._hasKey(key)) {
this._testType(key, "hash", true, callback);
if (this._hasField(key, field)) {
value = this._getField(key, field);
}
} else {
this.cache.set(key, this._makeKey({}, "hash"));
}
fieldValue = useFloat ? parseFloat(`${value}`) : parseInt(`${value}`);
amount = useFloat ? parseFloat(`${amount}`) : parseInt(`${amount}`);
if (isNaN(fieldValue) || isNil(fieldValue)) {
return this._handleCallback(callback, null, useFloat ? "ERR value is not an float or out of range" : "ERR value is not an integer or out of range");
}
fieldValue += amount;
this._setField(key, field, fieldValue.toString());
return fieldValue;
}
/**
*
*
* @private
* @param {string} key
* @param {string} field
* @returns {*}
* @memberof MemoryCache
*/
_getField(key, field) {
return this._getKey(key)[field];
}
/**
*
*
* @private
* @param {string} key
* @param {string} field
* @returns {*} {boolean}
* @memberof MemoryCache
*/
_hasField(key, field) {
let retVal = false;
if (key && field) {
const ky = this._getKey(key);
if (ky) {
retVal = field !== "_fieldTTL" && ky.hasOwnProperty(field);
}
}
return retVal;
}
/**
*
*
* @param {string} key
* @param {string} field
* @param {*} value
* @memberof MemoryCache
*/
_setField(key, field, value) {
this._getKey(key)[field] = value;
}
/**
*
*
* @private
* @param {Function} [callback]
* @param {(any)} [message]
* @param {*} [error]
* @param {boolean} [nolog]
* @returns {*}
* @memberof MemoryCache
*/
_handleCallback(callback, message, error, nolog) {
let err = error;
let msg = message;
nolog = isNil(nolog) ? true : nolog;
if (nolog) {
err = this._logReturn(error);
msg = this._logReturn(message);
}
if (typeof callback === "function") {
callback(err, msg);
return;
}
if (err) {
throw new Error(err);
}
return msg;
}
_logReturn(message) {
if (!isUndefined(message)) {
if (this.multiMode) {
if (!isNil(this.responseMessages)) {
this.responseMessages.push(message);
if (message === "OK") {
message = "QUEUED";
}
}
}
return message;
}
return;
}
/**
*
*
* @private
* @param {any[]} [params]
* @returns {*}
* @memberof MemoryCache
*/
_retrieveCallback(params) {
if (Array.isArray(params) && params.length > 0 && typeof params[params.length - 1] === "function") {
return params.pop();
}
return;
}
/**
* 字符串追加操作
* @param key
* @param value
* @param callback
*/
append(key, value, callback) {
let retVal = 0;
if (this._hasKey(key)) {
this._testType(key, "string", true, callback);
const existingValue = this._getKey(key);
const newValue = existingValue + value;
this._setKey(key, newValue);
retVal = newValue.length;
} else {
this.cache.set(key, this._makeKey(value, "string"));
retVal = value.length;
}
return this._handleCallback(callback, retVal);
}
/**
* 获取字符串长度
* @param key
* @param callback
*/
strlen(key, callback) {
let retVal = 0;
if (this._hasKey(key)) {
this._testType(key, "string", true, callback);
retVal = this._getKey(key).length;
}
return this._handleCallback(callback, retVal);
}
/**
* 获取子字符串
* @param key
* @param start
* @param end
* @param callback
*/
getrange(key, start, end, callback) {
let retVal = "";
if (this._hasKey(key)) {
this._testType(key, "string", true, callback);
const value = this._getKey(key);
retVal = value.substring(start, end + 1);
}
return this._handleCallback(callback, retVal);
}
/**
* 设置子字符串
* @param key
* @param offset
* @param value
* @param callback
*/
setrange(key, offset, value, callback) {
let retVal = 0;
if (this._hasKey(key)) {
this._testType(key, "string", true, callback);
const existingValue = this._getKey(key);
const newValue = existingValue.substring(0, offset) + value + existingValue.substring(offset + value.length);
this._setKey(key, newValue);
retVal = newValue.length;
} else {
const newValue = "".padEnd(offset, "\0") + value;
this.cache.set(key, this._makeKey(newValue, "string"));
retVal = newValue.length;
}
return this._handleCallback(callback, retVal);
}
/**
* 批量获取
* @param keys
* @param callback
*/
mget(...keys) {
const callback = this._retrieveCallback(keys);
const retVal = [];
for (const key of keys) {
if (this._hasKey(key)) {
this._testType(key, "string", false, callback);
retVal.push(this._getKey(key));
} else {
retVal.push(null);
}
}
return this._handleCallback(callback, retVal);
}
/**
* 批量设置
* @param keyValuePairs
* @param callback
*/
mset(...keyValuePairs) {
const callback = this._retrieveCallback(keyValuePairs);
if (keyValuePairs.length % 2 !== 0) {
return this._handleCallback(callback, null, "ERR wrong number of arguments for '%0' command".replace("%0", "mset"));
}
for (let i = 0; i < keyValuePairs.length; i += 2) {
const key = keyValuePairs[i];
const value = keyValuePairs[i + 1];
this.cache.set(key, this._makeKey(value.toString(), "string"));
}
return this._handleCallback(callback, "OK");
}
/**
* 获取所有键
* @param pattern
* @param callback
*/
keys(pattern = "*", callback) {
const retVal = [];
this.cache.forEach((_item, key) => {
if (pattern === "*" || this.matchPattern(key, pattern)) {
retVal.push(key);
}
});
return this._handleCallback(callback, retVal);
}
/**
* 简单的模式匹配
* @param key
* @param pattern
*/
matchPattern(key, pattern) {
if (pattern === "*") return true;
const regexPattern = pattern.replace(/\*/g, ".*").replace(/\?/g, ".").replace(/\[([^\]]*)\]/g, "[$1]");
const regex = new RegExp(`^${regexPattern}$`);
return regex.test(key);
}
/**
* 获取随机键
* @param callback
*/
randomkey(callback) {
const keys = [];
this.cache.forEach((_item, key) => {
keys.push(key);
});
if (keys.length === 0) {
return this._handleCallback(callback, null);
}
const randomIndex = Math.floor(Math.random() * keys.length);
return this._handleCallback(callback, keys[randomIndex]);
}
/**
* 重命名键
* @param oldKey
* @param newKey
* @param callback
*/
rename(oldKey, newKey, callback) {
if (!this._hasKey(oldKey)) {
return this._handleCallback(callback, null, "ERR no such key");
}
const value = this.cache.get(oldKey);
this.cache.set(newKey, value);
this.cache.delete(oldKey);
return this._handleCallback(callback, "OK");
}
/**
* 安全重命名键(目标键不存在时才重命名)
* @param oldKey
* @param newKey
* @param callback
*/
renamenx(oldKey, newKey, callback) {
if (!this._hasKey(oldKey)) {
return this._handleCallback(callback, null, "ERR no such key");
}
if (this._hasKey(newKey)) {
return this._handleCallback(callback, 0);
}
const value = this.cache.get(oldKey);
this.cache.set(newKey, value);
this.cache.delete(oldKey);
return this._handleCallback(callback, 1);
}
/**
* 获取键的类型
* @param key
* @param callback
*/
type(key, callback) {
if (!this._hasKey(key)) {
return this._handleCallback(callback, "none");
}
const item = this.cache.get(key);
return this._handleCallback(callback, item.type);
}
/**
* 清空当前数据库
* @param callback
*/
flushdb(callback) {
this.cache.clear();
return this._handleCallback(callback, "OK");
}
/**
* 清空所有数据库
* @param callback
*/
flushall(callback) {
this.databases.clear();
this.cache = this.createLRUCache();
this.databases.set(this.currentDBIndex, this.cache);
return this._handleCallback(callback, "OK");
}
/**
* 获取数据库大小
* @param callback
*/
dbsize(callback) {
const size = this.cache.size || 0;
return this._handleCallback(callback, size);
}
/**
* Sorted Set基础实现 - 添加成员
* @param key
* @param score
* @param member
* @param callback
*/
zadd(key, score, member, callback) {
let retVal = 0;
if (this._hasKey(key)) {
this._testType(key, "zset", true, callback);
} else {
this.cache.set(key, this._makeKey([], "zset"));
}
const zset = this._getKey(key);
const existing = zset.find((item) => item.member === member);
if (existing) {
existing.score = score;
} else {
zset.push({
score,
member
});
retVal = 1;
}
zset.sort((a, b) => a.score - b.score);
this._setKey(key, zset);
return this._handleCallback(callback, retVal);
}
/**
* Sorted Set - 获取成员分数
* @param key
* @param member
* @param callback
*/
zscore(key, member, callback) {
if (!this._hasKey(key)) {
return this._handleCallback(callback, null);
}
this._testType(key, "zset", true, callback);
const zset = this._getKey(key);
const item = zset.find((item2) => item2.member === member);
return this._handleCallback(callback, item ? item.score : null);
}
/**
* Sorted Set - 获取范围内的成员
* @param key
* @param start
* @param stop
* @param callback
*/
zrange(key, start, stop, callback) {
if (!this._hasKey(key)) {
return this._handleCallback(callback, []);
}
this._testType(key, "zset", true, callback);
const zset = this._getKey(key);
const length = zset.length;
if (stop < 0) {
stop = length + stop;
}
if (start < 0) {
start = length + start;
}
const retVal = zset.slice(start, stop + 1).map((item) => item.member);
return this._handleCallback(callback, retVal);
}
/**
* Sorted Set - 获取成员数量
* @param key
* @param callback
*/
zcard(key, callback) {
if (!this._hasKey(key)) {
return this._handleCallback(callback, 0);
}
this._testType(key, "zset", true, callback);
const zset = this._getKey(key);
return this._handleCallback(callback, zset.length);
}
/**
* Sorted Set - 删除成员
* @param key
* @param member
* @param callback
*/
zrem(key, member, callback) {
let retVal = 0;
if (this._hasKey(key)) {
this._testType(key, "zset", true, callback);
const zset = this._getKey(key);
const index = zset.findIndex((item) => item.member === member);
if (index !== -1) {
zset.splice(index, 1);
retVal = 1;
this._setKey(key, zset);
}
}
return this._handleCallback(callback, retVal);
}
};
// src/store/memory.ts
var MemoryStore = class {
static {
__name(this, "MemoryStore");
}
client;
options;
/**
* Creates an instance of MemoryStore.
* @param {MemoryStoreOpt} options
* @memberof MemoryStore
*/
constructor(options) {
this.options = {
maxKeys: 1e3,
evictionPolicy: "lru",
ttlCheckInterval: 6e4,
...options
};
this.client = new MemoryCache({
database: this.options.db || 0,
maxKeys: this.options.maxKeys,
maxMemory: this.options.maxMemory,
evictionPolicy: this.options.evictionPolicy,
ttlCheckInterval: this.options.ttlCheckInterval
});
this.client.createClient();
}
/**
* getConnection
*
* @returns {*}
* @memberof MemoryStore
*/
getConnection() {
return this.client;
}
/**
* close
*
* @returns {*} {Promise<void>}
* @memberof MemoryStore
*/
async close() {
if (this.client) {
this.client.end();
this.client = null;
}
}
/**
* release
*
* @param {*} _conn
* @returns {*} {Promise<void>}
* @memberof MemoryStore
*/
async release(_conn) {
return Promise.resolve();
}
/**
* defineCommand
*
* @param {string} _name
* @param {*} _scripts
* @memberof MemoryStore
*/
async defineCommand(_name, _scripts) {
throw new Error(messages.unsupported);
}
/**
* get and compare value
*
* @param {string} name
* @param {(string | number)} value
* @returns {*} {Promise<any>}
* @memberof MemoryStore
*/
async getCompare(name, value) {
const client = this.getConnection();
const val = client.get(`${this.options.keyPrefix}${name}`);
if (!val) {
return 0;
} else if (val == value) {
return client.del(`${this.options.keyPrefix}${name}`);
} else {
return -1;
}
}
/**
* 获取缓存统计信息
*/
getStats() {
if (this.client) {
return this.client.info();
}
return {
keys: 0,
memory: 0,
hits: 0,
misses: 0
};
}
/**
* Begin a transaction (not supported by MemoryStore)
* @throws {Error} Transactions are not supported by MemoryStore
*/
async beginTransaction() {
throw new Error("Transactions are not supported by MemoryStore. Use RedisStore for transaction support.");
}
/**
* Commit a transaction (not supported by MemoryStore)
* @throws {Error} Transactions are not supported by MemoryStore
*/
async commit() {
throw new Error("Transactions are not supported by MemoryStore. Use RedisStore for transaction support.");
}
/**
* Rollback a transaction (not supported by MemoryStore)
* @throws {Error} Transactions are not supported by MemoryStore
*/
async rollback() {
throw new Error("Transactions are not supported by MemoryStore. Use RedisStore for transaction support.");
}
};
var RedisStore = class {
static {
__name(this, "RedisStore");
}
options;
pool;
client;
reconnectAttempts = 0;
maxReconnectAttempts = 5;
reconnectDelay = 1e3;
/**
* Creates an instance of RedisStore.
* @param {RedisStoreOpt} options
* @memberof RedisStore
*/
constructor(options) {
this.options = this.parseOpt(options);
this.pool = null;
}
// parseOpt
parseOpt(options) {
const opt = {
type: options.type,
host: options.host || "127.0.0.1",
port: options.port || 3306,
username: options.username || "",
password: options.password || "",
db: options.db || 0,
timeout: options.timeout,
keyPrefix: options.keyPrefix || "",
poolSize: options.poolSize || 10,
connectTimeout: options.connectTimeout || 500
};
if (helper.isArray(options.host)) {
const hosts = [];
for (let i = 0; i < options.host.length; i++) {
const h = options.host[i];
if (!helper.isEmpty(options.host[i])) {
let p;
if (helper.isArray(options.port)) {
p = options.port[i];
} else {
p = options.port || 6379;
}
hosts.push({
host: h,
port: helper.toNumber(p)
});
}
}
if (!helper.isEmpty(options.name)) {
opt.host = "";
opt.port = null;
opt.sentinels = [
...hosts
];
opt.sentinelUsername = options.username;
opt.sentinelPassword = options.password;
} else {
opt.host = "";
opt.port = null;
opt.clusters = [
...hosts
];
}
}
return opt;
}
/**
* create connection by native with improved error handling
*
* @param {number} [connNum=0]
* @returns {*} {Promise<Redis | Cluster>}
* @memberof RedisStore
*/
async connect(connNum = 0) {
if (this.client && this.client.status === "ready") {
return this.client;
}
const defer = helper.getDefer();
let connection;
try {
if (!helper.isEmpty(this.options.clusters)) {
connection = new Cluster([
...this.options.clusters
], {
redisOptions: this.options,
enableOfflineQueue: false,
retryDelayOnFailover: 100
});
} else {
connection = new Redis({
...this.options,
enableOfflineQueue: false,
retryDelayOnFailover: 100,
lazyConnect: true
});
}
this.options.keyPrefix = "";
connection.on("error", (err) => {
DefaultLogger.Error(`Redis connection error: ${err.message}`);
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.scheduleReconnect(connNum);
} else {
defer.reject(new Error(`Redis connection failed after ${this.maxReconnectAttempts} attempts`));
}
});
connection.on("end", () => {
DefaultLogger.Warn("Redis connection ended");
if (connNum < 3) {
this.scheduleReconnect(connNum + 1);
} else {
this.close();
defer.reject(new Error("Redis connection end after 3 attempts"));
}
});
connection.on("ready", () => {
DefaultLogger.Info("Redis connection ready");
this.client = connection;
this.reconnectAttempts = 0;
defer.resolve(connection);
});
if (connection instanceof Redis) {
await connection.connect();
}
} catch (error) {
DefaultLogger.Error(`Failed to create Redis connection: ${error.message}`);
defer.reject(error);
}
return defer.promise;
}
/**
* 计划重连,使用指数退避策略
* @private
* @param {number} connNum
*/
scheduleReconnect(connNum) {
this.reconnectAttempts++;
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
DefaultLogger.Info(`Scheduling Redis reconnect attempt ${this.reconnectAttempts} in ${delay}ms`);
setTimeout(() => {
this.connect(connNum).catch((err) => {
DefaultLogger.Error(`Reconnect attempt ${this.reconnectAttempts} failed: ${err.message}`);
});
}, delay);
}
/**
* get connection from pool with improved configuration
*
* @returns {*}
* @memberof RedisStore
*/
getConnection() {
if (!this.pool || !this.pool.acquire) {
const factory = {
create: /* @__PURE__ */ __name(() => {
return this.connect();
}, "create"),
destroy: /* @__PURE__ */ __name((resource) => {
if (resource && typeof resource.disconnect === "function") {
resource.disconnect();
}
return Promise.resolve();
}, "destroy"),
validate: /* @__PURE__ */ __name((resource) => {
return Promise.resolve(resource && resource.status === "ready");
}, "validate")
};
this.pool = genericPool.createPool(factory, {
max: this.options.poolSize || 10,
min: Math.min(2, this.options.poolSize || 2),
acquireTimeoutMillis: 3e4,
testOnBorrow: true,
evictionRunIntervalMillis: 3e4,
idleTimeoutMillis: 3e5,
softIdleTimeoutMillis: 18e4
// 3分钟软空闲超时
});
this.pool.on("factoryCreateError", function(err) {
DefaultLogger.Error(`Redis pool create error: ${err.message}`);
});
this.pool.on("factoryDestroyError", function(err) {
DefaultLogger.Error(`Redis pool destroy error: ${err.message}`);
});
}
return this.pool.acquire();
}
/**
* close connection with proper cleanup
*
* @returns {*}
* @memberof RedisStore
*/
async close() {
try {
if (this.pool) {