UNPKG

koatty_store

Version:
1,974 lines (1,971 loc) 64.4 kB
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) {