UNPKG

koatty_store

Version:
1,794 lines (1,790 loc) 71 kB
/*! * @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]; }