koatty_store
Version:
Cache store for koatty.
1,794 lines (1,790 loc) • 71 kB
JavaScript
/*!
* @Author: richen
* @Date: 2025-06-09 12:32:48
* @License: BSD (3-Clause)
* @Copyright (c) - <richenlin(at)gmail.com>
* @HomePage: https://koatty.org/
*/
import { flatten, isNil, union, isUndefined } from 'lodash';
import * as helper from 'koatty_lib';
import { EventEmitter } from 'events';
import { LRUCache } from 'lru-cache';
import { DefaultLogger } from 'koatty_logger';
import { Cluster, Redis } from 'ioredis';
import genericPool from 'generic-pool';
/*
* @Description:
* @Usage:
* @Author: richen
* @Date: 2021-12-02 11:03:20
* @LastEditTime: 2023-12-20 19:04:29
*/
/**
*
*
* @enum {number}
*/
var messages;
(function (messages) {
messages["ok"] = "OK";
messages["queued"] = "QUEUED";
messages["pong"] = "PONG";
messages["noint"] = "ERR value is not an integer or out of range";
messages["nofloat"] = "ERR value is not an float or out of range";
messages["nokey"] = "ERR no such key";
messages["nomultiinmulti"] = "ERR MULTI calls can not be nested";
messages["nomultiexec"] = "ERR EXEC without MULTI";
messages["nomultidiscard"] = "ERR DISCARD without MULTI";
messages["busykey"] = "ERR target key name is busy";
messages["syntax"] = "ERR syntax error";
messages["unsupported"] = "MemoryCache does not support that operation";
messages["wrongTypeOp"] = "WRONGTYPE Operation against a key holding the wrong kind of value";
messages["wrongPayload"] = "DUMP payload version or checksum are wrong";
messages["wrongArgCount"] = "ERR wrong number of arguments for '%0' command";
messages["bitopnotWrongCount"] = "ERR BITOP NOT must be called with a single source key";
messages["indexOutOfRange"] = "ERR index out of range";
messages["invalidLexRange"] = "ERR min or max not valid string range item";
messages["invalidDBIndex"] = "ERR invalid DB index";
messages["invalidDBIndexNX"] = "ERR invalid DB index, '%0' does not exist";
messages["mutuallyExclusiveNXXX"] = "ERR XX and NX options at the same time are not compatible";
})(messages || (messages = {}));
class MemoryCache extends EventEmitter {
databases = new Map();
options;
currentDBIndex;
connected;
lastSave;
multiMode;
cache;
responseMessages;
ttlCheckTimer = null;
/**
* Creates an instance of MemoryCache.
* @param {MemoryCacheOptions} options
* @memberof MemoryCache
*/
constructor(options) {
super();
this.options = {
database: 0,
maxKeys: 1000,
evictionPolicy: 'lru',
ttlCheckInterval: 60000, // 1分钟检查一次过期键
maxAge: 1000 * 60 * 60, // 默认1小时过期
...options
};
this.currentDBIndex = options.database || 0;
this.connected = false;
this.lastSave = Date.now();
this.multiMode = false;
this.responseMessages = [];
// 初始化数据库和缓存
if (!this.databases.has(this.currentDBIndex)) {
this.databases.set(this.currentDBIndex, this.createLRUCache());
}
this.cache = this.databases.get(this.currentDBIndex);
// 启动TTL检查定时器
this.startTTLCheck();
}
/**
* 创建LRU缓存实例
*/
createLRUCache() {
return new LRUCache({
max: this.options.maxKeys || 1000,
ttl: this.options.maxAge || 1000 * 60 * 60, // 1小时默认
updateAgeOnGet: true, // 访问时更新age
dispose: (value, key, reason) => {
// 键被淘汰时的回调 - 直接使用lru-cache的事件机制
this.emit('evict', key, value, reason);
},
onInsert: (value, key) => {
// 键被插入时的回调
this.emit('insert', key, value);
}
});
}
/**
* 启动TTL检查定时器
*/
startTTLCheck() {
if (this.ttlCheckTimer) {
clearInterval(this.ttlCheckTimer);
}
this.ttlCheckTimer = setInterval(() => {
this.cleanExpiredKeys();
}, this.options.ttlCheckInterval || 60000);
}
/**
* 清理过期键
*/
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;
}
}
/**
*
*
* @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;
// exit multi mode if we are in it
this.discard(null, true);
this.emit('connect');
this.emit('ready');
return this;
}
/**
*
*
* @returns {*}
* @memberof MemoryCache
*/
quit() {
this.connected = false;
this.stopTTLCheck();
// exit multi mode if we are in it
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) => {
// 粗略估算:key长度 + JSON序列化后的大小
totalSize += key.length * 2; // Unicode字符占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 || messages.pong;
return this._handleCallback(callback, message);
}
/**
*
*
* @param {string} password
* @param {Function} [callback]
* @returns {*}
* @memberof MemoryCache
*/
auth(password, callback) {
return this._handleCallback(callback, messages.ok);
}
/**
*
*
* @param {number} dbIndex
* @param {Function} [callback]
* @returns {*}
* @memberof MemoryCache
*/
select(dbIndex, callback) {
if (!helper.isNumber(dbIndex)) {
return this._handleCallback(callback, null, messages.invalidDBIndex);
}
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, messages.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;
// parse parameters
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, messages.syntax);
}
ttl = parseInt(params.shift());
if (isNaN(ttl)) {
return this._handleCallback(callback, null, messages.noint);
}
break;
case 'px':
if (params.length === 0) {
return this._handleCallback(callback, null, messages.syntax);
}
pttl = parseInt(params.shift());
if (isNaN(pttl)) {
return this._handleCallback(callback, null, messages.noint);
}
break;
default:
return this._handleCallback(callback, null, messages.syntax);
}
}
if (!isNil(ttl) && !isNil(pttl)) {
return this._handleCallback(callback, null, messages.syntax);
}
if (notexist && onlyexist) {
return this._handleCallback(callback, null, messages.syntax);
}
pttl = pttl || ttl * 1000 || 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, messages.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 / 1000);
}
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 * 1000;
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);
// Flatten the array in case an array was passed
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) {
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) {
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) {
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) {
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, 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());
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);
if (this._hasField(key, field)) {
retVal = this._getKey(key)[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);
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);
for (let itr = 0; itr < fields.length; itr++) {
const field = fields[itr];
if (this._hasField(key, field)) {
delete this.cache.get(key).value[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) {
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);
retVals = this._getKey(key);
}
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);
retVals = Object.keys(this._getKey(key));
}
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);
retVals = Object.values(this._getKey(key));
}
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) {
const size = stop - start + 1;
for (let itr = start; itr < size; 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) {
// Clear the queue mode, drain the queue, empty the watch list
if (this.multiMode) {
this.cache = this.databases.get(this.currentDBIndex);
this.multiMode = false;
this.responseMessages = [];
}
if (!silent) {
return this._handleCallback(callback, messages.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();
// Prevent unexpected errors if the actual ttl just happens to be -2 or -1
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: value, type: 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, messages.noint);
}
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, messages.noint);
}
}
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, messages.wrongTypeOp);
}
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: 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 : 0;
let value = 0;
if (isNaN(amount) || isNil(amount)) {
return this._handleCallback(callback, null, useFloat ? messages.nofloat : messages.noint);
}
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 ? messages.nofloat : messages.noint);
}
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 = 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 === messages.ok) {
message = messages.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, messages.wrongArgCount.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, messages.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;
// 转换glob模式为正则表达式
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, messages.nokey);
}
const value = this.cache.get(oldKey);
this.cache.set(newKey, value);
this.cache.delete(oldKey);
return this._handleCallback(callback, messages.ok);
}
/**
* 安全重命名键(目标键不存在时才重命名)
* @param oldKey
* @param newKey
* @param callback
*/
renamenx(oldKey, newKey, callback) {
if (!this._hasKey(oldKey)) {
return this._handleCallback(callback, null, messages.nokey);
}
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, messages.ok);
}
/**
* 清空所有数据库
* @param callback
*/
flushall(callback) {
this.databases.clear();
this.cache = this.createLRUCache();
this.databases.set(this.currentDBIndex, this.cache);
return this._handleCallback(callback, messages.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((item) => item.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);
}
}
/*
* @Description:
* @Usage:
* @Author: richen
* @Date: 2021-06-29 19:07:57
* @LastEditTime: 2023-02-18 23:52:47
*/
class MemoryStore {
client;
pool;
options;
/**
* Creates an instance of MemoryStore.
* @param {MemoryStoreOpt} options
* @memberof MemoryStore
*/
constructor(options) {
this.options = {
maxKeys: 1000,
evictionPolicy: 'lru',
ttlCheckInterval: 60000, // 1分钟
...options
};
this.client = null;
}
/**
* getConnection
*
* @returns {*}
* @memberof MemoryStore
*/
getConnection() {
if (!this.pool) {
this.pool = new MemoryCache({
database: this.options.db || 0,
maxKeys: this.options.maxKeys,
maxMemory: this.options.maxMemory,
evictionPolicy: this.options.evictionPolicy,
ttlCheckInterval: this.options.ttlCheckInterval
});
}
if (!this.client) {
this.client = this.pool.createClient();
this.client.status = "ready";
}
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;
}
/**
* 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
};
}
}
/*
* @Author: richen
* @Date: 2020-11-30 15:56:08
* @LastEditors: Please set LastEditors
* @LastEditTime: 2023-02-19 00:02:09
* @License: BSD (3-Clause)
* @Copyright (c) - <richenlin(at)gmail.com>
*/
/**
*
*
* @export
* @class RedisStore
*/
class RedisStore {
options;
pool;
client;
reconnectAttempts = 0;
maxReconnectAttempts = 5;
reconnectDelay = 1000; // 初始重连延迟1秒
/**
* 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];
}