mm_cachebase 2.0.4 → 2.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/example/index.js +53 -0
- package/index.js +9 -3156
- package/lib/cachebase.js +3125 -0
- package/package.json +8 -2
- package/test.js +13 -0
package/lib/cachebase.js
ADDED
|
@@ -0,0 +1,3125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CacheBase 是一个通用缓存基类,支持内存缓存和持久化缓存
|
|
3
|
+
* 接口与 mm_redis 完全对齐:全异步、冒号风格事件、统一签名与返回值
|
|
4
|
+
* @class CacheBase
|
|
5
|
+
* @extends Base
|
|
6
|
+
*/
|
|
7
|
+
const Base = require('mm_corejs').Base;
|
|
8
|
+
const { fullname, hasFile, loadJson, saveJson, addDir } = require('mm_helper');
|
|
9
|
+
|
|
10
|
+
class CacheBase extends Base {
|
|
11
|
+
/**
|
|
12
|
+
* 默认配置静态变量
|
|
13
|
+
*/
|
|
14
|
+
static config = {
|
|
15
|
+
enabled: true,
|
|
16
|
+
persist: true,
|
|
17
|
+
scope: 'sys',
|
|
18
|
+
dir: './cache',
|
|
19
|
+
debug: false,
|
|
20
|
+
auto_save: true,
|
|
21
|
+
save_interval: 30000,
|
|
22
|
+
expire_check_interval: 1000,
|
|
23
|
+
max_size: null,
|
|
24
|
+
storage_type: 'memory'
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 构造函数
|
|
29
|
+
* @param {Object} config - 配置对象
|
|
30
|
+
*/
|
|
31
|
+
constructor(config = {}) {
|
|
32
|
+
// 先调用父类构造函数
|
|
33
|
+
super(CacheBase.config);
|
|
34
|
+
|
|
35
|
+
// 存储实际数据
|
|
36
|
+
this._data = {};
|
|
37
|
+
|
|
38
|
+
// 存储过期时间
|
|
39
|
+
this._expires = {};
|
|
40
|
+
|
|
41
|
+
// 存储数据类型
|
|
42
|
+
this._types = {};
|
|
43
|
+
|
|
44
|
+
// 存储持久化数据的定时器
|
|
45
|
+
this._save_timer = null;
|
|
46
|
+
|
|
47
|
+
// 存储过期检查的定时器
|
|
48
|
+
this._check_timer = null;
|
|
49
|
+
|
|
50
|
+
// 初始化服务状态
|
|
51
|
+
this.is_inited = false;
|
|
52
|
+
this.is_destroyed = false;
|
|
53
|
+
|
|
54
|
+
// 连接状态(与 mm_redis 保持一致)
|
|
55
|
+
this.connected = false;
|
|
56
|
+
|
|
57
|
+
// 发布订阅频道回调
|
|
58
|
+
this._channels = {};
|
|
59
|
+
|
|
60
|
+
this.setConfig(config);
|
|
61
|
+
// 初始化服务(内存缓存特性:构造即初始化)
|
|
62
|
+
this.init();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 设置配置
|
|
68
|
+
* @param {Object} config - 配置对象
|
|
69
|
+
*/
|
|
70
|
+
CacheBase.prototype.setConfig = function (config) {
|
|
71
|
+
// 加载外部配置文件
|
|
72
|
+
const external_config = this._loadExternalConfig();
|
|
73
|
+
// 更新配置
|
|
74
|
+
Object.assign(this.config, external_config, config);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 初始化服务
|
|
79
|
+
* @returns {Promise<void>}
|
|
80
|
+
*/
|
|
81
|
+
CacheBase.prototype.init = async function () {
|
|
82
|
+
if (this.is_inited || this.is_destroyed) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
await this._initService();
|
|
88
|
+
this.is_inited = true;
|
|
89
|
+
this.connected = true;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
this.log('error', 'Failed to initialize CacheBase service:', error);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 加载外部配置文件
|
|
97
|
+
* @private
|
|
98
|
+
* @returns {Object} 外部配置对象
|
|
99
|
+
*/
|
|
100
|
+
CacheBase.prototype._loadExternalConfig = function () {
|
|
101
|
+
try {
|
|
102
|
+
const config_path = fullname('./config/cachebase.config.json', __dirname);
|
|
103
|
+
if (hasFile(config_path)) {
|
|
104
|
+
const config = loadJson(config_path);
|
|
105
|
+
return config;
|
|
106
|
+
}
|
|
107
|
+
} catch (error) {
|
|
108
|
+
this.log('warn', '外部配置文件加载失败,使用默认配置:', error.message);
|
|
109
|
+
}
|
|
110
|
+
return {};
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 设置文件路径
|
|
115
|
+
* @private
|
|
116
|
+
*/
|
|
117
|
+
CacheBase.prototype._setFilePath = function () {
|
|
118
|
+
const scope = this.config.scope || 'sys';
|
|
119
|
+
const dir = this.config.dir || './cache';
|
|
120
|
+
this.file_path = fullname(`./${scope}.json`, dir);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 初始化服务
|
|
125
|
+
* @private
|
|
126
|
+
* @returns {Promise<void>}
|
|
127
|
+
*/
|
|
128
|
+
CacheBase.prototype._initService = async function () {
|
|
129
|
+
try {
|
|
130
|
+
// 设置文件路径
|
|
131
|
+
this._setFilePath();
|
|
132
|
+
// 初始化过期检查定时器
|
|
133
|
+
this._check_timer = setInterval(() => {
|
|
134
|
+
this.clearExpire();
|
|
135
|
+
}, this.config.expire_check_interval || 1000);
|
|
136
|
+
|
|
137
|
+
// 设置最大大小限制
|
|
138
|
+
if (this.config.max_size) {
|
|
139
|
+
this._max_size = this.config.max_size;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 加载数据
|
|
143
|
+
this._loadData();
|
|
144
|
+
|
|
145
|
+
// 设置定时保存
|
|
146
|
+
this._connect();
|
|
147
|
+
|
|
148
|
+
// 设置初始化完成状态
|
|
149
|
+
this.is_inited = true;
|
|
150
|
+
} catch (error) {
|
|
151
|
+
this.log('error', 'Failed to initialize service:', error);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 设置定时保存(内部方法)
|
|
157
|
+
* @private
|
|
158
|
+
*/
|
|
159
|
+
CacheBase.prototype._connect = function () {
|
|
160
|
+
// 如果启用了自动保存
|
|
161
|
+
if (this.config.auto_save) {
|
|
162
|
+
this._save_timer = setInterval(() => {
|
|
163
|
+
this.save();
|
|
164
|
+
}, this.config.save_interval || 30000);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* 从文件加载数据
|
|
170
|
+
* @private
|
|
171
|
+
*/
|
|
172
|
+
CacheBase.prototype._loadData = function () {
|
|
173
|
+
try {
|
|
174
|
+
if (this.config.persist && this.file_path) {
|
|
175
|
+
if (hasFile(this.file_path)) {
|
|
176
|
+
// 使用mm_helper的函数读取JSON文件
|
|
177
|
+
const parsed_data = loadJson(this.file_path);
|
|
178
|
+
if (parsed_data && parsed_data.data) {
|
|
179
|
+
this._data = parsed_data.data || {};
|
|
180
|
+
} else {
|
|
181
|
+
this.log('warn', `[_loadData] 解析后的数据中没有data字段`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (parsed_data && parsed_data.expires) {
|
|
185
|
+
this._expires = parsed_data.expires || {};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (parsed_data && parsed_data.types) {
|
|
189
|
+
this._types = parsed_data.types || {};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
} catch (error) {
|
|
194
|
+
this.log('error', '加载数据失败:', error);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* 保存数据到文件
|
|
200
|
+
*/
|
|
201
|
+
CacheBase.prototype.save = function () {
|
|
202
|
+
// 检查服务状态
|
|
203
|
+
if (!this.is_inited || this.is_destroyed) {
|
|
204
|
+
this.log('error', 'Service is not initialized or has been destroyed');
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
this.emit('save:before');
|
|
210
|
+
this._saveData();
|
|
211
|
+
this.emit('save:after');
|
|
212
|
+
} catch (error) {
|
|
213
|
+
this.log('error', 'Failed to save data:', error);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* 保存数据到文件
|
|
219
|
+
* @private
|
|
220
|
+
*/
|
|
221
|
+
CacheBase.prototype._saveData = function () {
|
|
222
|
+
try {
|
|
223
|
+
if (this.config.persist && this.file_path) {
|
|
224
|
+
// 确保目录存在
|
|
225
|
+
addDir(this.file_path);
|
|
226
|
+
|
|
227
|
+
const data_to_save = {
|
|
228
|
+
data: this._data,
|
|
229
|
+
expires: this._expires,
|
|
230
|
+
types: this._types,
|
|
231
|
+
timestamp: Date.now()
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// 使用mm_helper的函数写入JSON文件
|
|
235
|
+
saveJson(this.file_path, data_to_save, null, 2);
|
|
236
|
+
}
|
|
237
|
+
} catch (error) {
|
|
238
|
+
this.log('error', 'Failed to save data:', error);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* 销毁服务
|
|
244
|
+
* @returns {Promise<void>}
|
|
245
|
+
*/
|
|
246
|
+
CacheBase.prototype.destroy = async function () {
|
|
247
|
+
if (this.is_destroyed) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
// 保存数据
|
|
253
|
+
this.save();
|
|
254
|
+
|
|
255
|
+
// 清除定时器
|
|
256
|
+
if (this._save_timer) {
|
|
257
|
+
clearInterval(this._save_timer);
|
|
258
|
+
this._save_timer = null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (this._check_timer) {
|
|
262
|
+
clearInterval(this._check_timer);
|
|
263
|
+
this._check_timer = null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 清空数据
|
|
267
|
+
this._data = {};
|
|
268
|
+
this._expires = {};
|
|
269
|
+
this._types = {};
|
|
270
|
+
this._channels = {};
|
|
271
|
+
|
|
272
|
+
// 设置销毁状态
|
|
273
|
+
this.is_inited = false;
|
|
274
|
+
this.is_destroyed = true;
|
|
275
|
+
this.connected = false;
|
|
276
|
+
|
|
277
|
+
// 触发销毁事件
|
|
278
|
+
this.emit('destroy', { success: true });
|
|
279
|
+
} catch (error) {
|
|
280
|
+
this.log('error', 'Failed to destroy service:', error);
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* 打开缓存服务(与 mm_redis 接口对齐)
|
|
286
|
+
* @returns {Promise<Boolean>} 连接结果
|
|
287
|
+
*/
|
|
288
|
+
CacheBase.prototype.open = async function () {
|
|
289
|
+
try {
|
|
290
|
+
if (!this.is_inited) {
|
|
291
|
+
await this.init();
|
|
292
|
+
}
|
|
293
|
+
return this.connected;
|
|
294
|
+
} catch (error) {
|
|
295
|
+
this.log('error', 'Failed to open CacheBase:', error);
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* 关闭缓存服务(与 mm_redis 接口对齐)
|
|
302
|
+
* @returns {Promise<Boolean>} 关闭结果
|
|
303
|
+
*/
|
|
304
|
+
CacheBase.prototype.close = async function () {
|
|
305
|
+
try {
|
|
306
|
+
await this.destroy();
|
|
307
|
+
return true;
|
|
308
|
+
} catch (error) {
|
|
309
|
+
this.log('error', 'Failed to close CacheBase:', error);
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* 检查连接状态(与 mm_redis 接口对齐)
|
|
316
|
+
* @returns {Boolean} 是否已连接
|
|
317
|
+
*/
|
|
318
|
+
CacheBase.prototype.isConnected = function () {
|
|
319
|
+
return this.connected;
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* 清理资源并销毁实例
|
|
324
|
+
* @returns {Boolean} 操作结果
|
|
325
|
+
*/
|
|
326
|
+
CacheBase.prototype.dispose = function () {
|
|
327
|
+
try {
|
|
328
|
+
// 防止重复销毁
|
|
329
|
+
if (this.is_destroyed) {
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// 清除定时任务
|
|
334
|
+
if (this._save_timer) {
|
|
335
|
+
clearInterval(this._save_timer);
|
|
336
|
+
this._save_timer = null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (this._check_timer) {
|
|
340
|
+
clearInterval(this._check_timer);
|
|
341
|
+
this._check_timer = null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// 清除所有数据
|
|
345
|
+
this._data = {};
|
|
346
|
+
this._expires = {};
|
|
347
|
+
this._types = {};
|
|
348
|
+
this._channels = {};
|
|
349
|
+
|
|
350
|
+
// 设置销毁状态
|
|
351
|
+
this.is_inited = false;
|
|
352
|
+
this.is_destroyed = true;
|
|
353
|
+
this.connected = false;
|
|
354
|
+
|
|
355
|
+
// 触发销毁事件
|
|
356
|
+
this.emit('destroy', {
|
|
357
|
+
success: true
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
return true;
|
|
361
|
+
} catch (error) {
|
|
362
|
+
this.log('error', 'Failed to dispose:', error);
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
// ==================== 键值操作 ====================
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* 设置键值对
|
|
371
|
+
* @param {String} key 键
|
|
372
|
+
* @param {*} value 值
|
|
373
|
+
* @param {Number} [seconds=0] 过期时间(秒),0表示永不过期
|
|
374
|
+
* @returns {Promise<Boolean>} 成功返回true,失败返回false
|
|
375
|
+
*/
|
|
376
|
+
CacheBase.prototype.set = async function (key, value, seconds = 0) {
|
|
377
|
+
// 检查服务状态
|
|
378
|
+
if (!this.is_inited || this.is_destroyed) {
|
|
379
|
+
this.log('error', 'Service is not initialized or has been destroyed');
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// 参数验证
|
|
384
|
+
if (!key || typeof key !== 'string') {
|
|
385
|
+
throw new TypeError('key必须是非空字符串');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
try {
|
|
389
|
+
// 触发前置事件
|
|
390
|
+
const ctx = {
|
|
391
|
+
key,
|
|
392
|
+
value,
|
|
393
|
+
seconds,
|
|
394
|
+
cancel: false
|
|
395
|
+
};
|
|
396
|
+
this.emit('set:before', ctx);
|
|
397
|
+
|
|
398
|
+
if (ctx.cancel) {
|
|
399
|
+
this.log('debug', `set操作被事件处理器取消 [${key}]`);
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// 使用可能被修改的值
|
|
404
|
+
const {
|
|
405
|
+
key: finalKey,
|
|
406
|
+
value: finalValue,
|
|
407
|
+
seconds: finalSeconds
|
|
408
|
+
} = ctx;
|
|
409
|
+
|
|
410
|
+
// 保存数据
|
|
411
|
+
this._data[finalKey] = finalValue;
|
|
412
|
+
|
|
413
|
+
// 设置类型
|
|
414
|
+
this._types[finalKey] = this._getDataTypes(finalValue);
|
|
415
|
+
|
|
416
|
+
// 设置过期时间
|
|
417
|
+
if (finalSeconds && typeof finalSeconds === 'number' && finalSeconds > 0) {
|
|
418
|
+
this._expires[finalKey] = Date.now() + finalSeconds * 1000;
|
|
419
|
+
} else {
|
|
420
|
+
delete this._expires[finalKey];
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// 触发后置事件
|
|
424
|
+
this.emit('set:after', {
|
|
425
|
+
key,
|
|
426
|
+
value,
|
|
427
|
+
seconds,
|
|
428
|
+
success: true
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
return true;
|
|
432
|
+
} catch (error) {
|
|
433
|
+
this.log('error', `Failed to set key ${key}:`, error);
|
|
434
|
+
this.emit('set:error', { key, value, seconds, error });
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* 获取键值
|
|
441
|
+
* @param {String} key 键
|
|
442
|
+
* @returns {Promise<Object|null>} 值,不存在或错误返回null
|
|
443
|
+
*/
|
|
444
|
+
CacheBase.prototype.get = async function (key) {
|
|
445
|
+
// 检查服务状态
|
|
446
|
+
if (!this.is_inited || this.is_destroyed) {
|
|
447
|
+
this.log('error', 'Service is not initialized or has been destroyed');
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
try {
|
|
452
|
+
this.emit('get:before', { key });
|
|
453
|
+
|
|
454
|
+
// 检查是否过期
|
|
455
|
+
if (this._isExpired(key)) {
|
|
456
|
+
await this.del(key);
|
|
457
|
+
this.emit('get:after', { key, value: null });
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const value = this._data[key];
|
|
462
|
+
this.emit('get:after', { key, value });
|
|
463
|
+
return value !== undefined ? value : null;
|
|
464
|
+
} catch (error) {
|
|
465
|
+
this.log('error', `Failed to get key ${key}:`, error);
|
|
466
|
+
this.emit('get:error', { key, error });
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* 删除键
|
|
473
|
+
* @param {String|Array<String>} key 键名或键名数组
|
|
474
|
+
* @returns {Promise<Number>} 删除的键数量
|
|
475
|
+
*/
|
|
476
|
+
CacheBase.prototype.del = async function (key) {
|
|
477
|
+
// 参数验证
|
|
478
|
+
if (!key) {
|
|
479
|
+
throw new TypeError('key必须是非空字符串或非空字符串数组');
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (typeof key === 'string') {
|
|
483
|
+
if (!key) {
|
|
484
|
+
throw new TypeError('key必须是非空字符串');
|
|
485
|
+
}
|
|
486
|
+
} else if (Array.isArray(key)) {
|
|
487
|
+
for (const k of key) {
|
|
488
|
+
if (!k || typeof k !== 'string') {
|
|
489
|
+
throw new TypeError('key必须是非空字符串或非空字符串数组');
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
} else {
|
|
493
|
+
throw new TypeError('key必须是非空字符串或非空字符串数组');
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// 检查服务状态
|
|
497
|
+
if (!this.is_inited || this.is_destroyed) {
|
|
498
|
+
this.log('error', 'Service is not initialized or has been destroyed');
|
|
499
|
+
return 0;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
try {
|
|
503
|
+
// 触发前置事件
|
|
504
|
+
const ctx = {
|
|
505
|
+
key,
|
|
506
|
+
cancel: false
|
|
507
|
+
};
|
|
508
|
+
this.emit('del:before', ctx);
|
|
509
|
+
|
|
510
|
+
if (ctx.cancel) {
|
|
511
|
+
return 0;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const finalKey = ctx.key;
|
|
515
|
+
let deletedCount = 0;
|
|
516
|
+
|
|
517
|
+
if (Array.isArray(finalKey)) {
|
|
518
|
+
// 批量删除
|
|
519
|
+
for (const k of finalKey) {
|
|
520
|
+
if (k in this._data) {
|
|
521
|
+
delete this._data[k];
|
|
522
|
+
delete this._types[k];
|
|
523
|
+
delete this._expires[k];
|
|
524
|
+
deletedCount++;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
} else {
|
|
528
|
+
// 单个删除
|
|
529
|
+
if (finalKey in this._data) {
|
|
530
|
+
delete this._data[finalKey];
|
|
531
|
+
delete this._types[finalKey];
|
|
532
|
+
delete this._expires[finalKey];
|
|
533
|
+
deletedCount++;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// 触发后置事件
|
|
538
|
+
this.emit('del:after', {
|
|
539
|
+
key: finalKey,
|
|
540
|
+
success: deletedCount > 0,
|
|
541
|
+
deleted: deletedCount
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
return deletedCount;
|
|
545
|
+
} catch (error) {
|
|
546
|
+
this.log('error', 'Failed to del:', error);
|
|
547
|
+
this.emit('del:error', { key, error });
|
|
548
|
+
return 0;
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* 批量删除键
|
|
554
|
+
* @param {Array} keys 键名数组
|
|
555
|
+
* @returns {Promise<Number>} 删除的键数量
|
|
556
|
+
*/
|
|
557
|
+
CacheBase.prototype.delBatch = async function (keys) {
|
|
558
|
+
try {
|
|
559
|
+
if (!Array.isArray(keys)) {
|
|
560
|
+
return 0;
|
|
561
|
+
}
|
|
562
|
+
return await this.del(keys);
|
|
563
|
+
} catch (error) {
|
|
564
|
+
this.log('error', 'Failed to delBatch:', error);
|
|
565
|
+
return 0;
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* 检查键是否存在
|
|
571
|
+
* @param {String} key 键
|
|
572
|
+
* @returns {Promise<Number>} 存在返回1,不存在返回0
|
|
573
|
+
*/
|
|
574
|
+
CacheBase.prototype.exists = async function (key) {
|
|
575
|
+
try {
|
|
576
|
+
this.emit('exists:before', { key });
|
|
577
|
+
|
|
578
|
+
if (this._isExpired(key)) {
|
|
579
|
+
await this.del(key);
|
|
580
|
+
this.emit('exists:after', { key, result: 0 });
|
|
581
|
+
return 0;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const result = key in this._data ? 1 : 0;
|
|
585
|
+
this.emit('exists:after', { key, result });
|
|
586
|
+
return result;
|
|
587
|
+
} catch (error) {
|
|
588
|
+
this.log('error', `Failed to check key ${key}:`, error);
|
|
589
|
+
return 0;
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* 检查键是否存在
|
|
595
|
+
* @param {String} key 键名
|
|
596
|
+
* @returns {Promise<Boolean>} 是否存在
|
|
597
|
+
*/
|
|
598
|
+
CacheBase.prototype.has = async function (key) {
|
|
599
|
+
const result = await this.exists(key);
|
|
600
|
+
return result > 0;
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* 清空过期数据
|
|
605
|
+
* @returns {Promise<void>}
|
|
606
|
+
*/
|
|
607
|
+
CacheBase.prototype.clearExpire = async function () {
|
|
608
|
+
try {
|
|
609
|
+
const now = Date.now();
|
|
610
|
+
const expiredKeys = [];
|
|
611
|
+
for (const key in this._expires) {
|
|
612
|
+
if (this._expires[key] < now) {
|
|
613
|
+
expiredKeys.push(key);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if (expiredKeys.length > 0) {
|
|
617
|
+
await this.del(expiredKeys);
|
|
618
|
+
}
|
|
619
|
+
} catch (error) {
|
|
620
|
+
this.log('error', 'Failed to clear expired data:', error);
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* 检查键是否过期
|
|
626
|
+
* @private
|
|
627
|
+
* @param {String} key 键名
|
|
628
|
+
* @returns {Boolean} 是否过期
|
|
629
|
+
*/
|
|
630
|
+
CacheBase.prototype._isExpired = function (key) {
|
|
631
|
+
// 先检查键是否存在
|
|
632
|
+
if (!(key in this._data)) {
|
|
633
|
+
return true;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// 如果没有设置过期时间,则未过期
|
|
637
|
+
if (!(key in this._expires)) {
|
|
638
|
+
return false;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const now = Date.now();
|
|
642
|
+
const is_expired = now > this._expires[key];
|
|
643
|
+
|
|
644
|
+
// 仅返回过期状态,不在此处做惰性删除。
|
|
645
|
+
// 删除统一由 del 方法完成,避免事件不成对。
|
|
646
|
+
return is_expired;
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* 获取数据类型
|
|
651
|
+
* @private
|
|
652
|
+
* @param {*} value 值
|
|
653
|
+
* @returns {String} 数据类型
|
|
654
|
+
*/
|
|
655
|
+
CacheBase.prototype._getDataTypes = function (value) {
|
|
656
|
+
if (value === null) return 'null';
|
|
657
|
+
if (value instanceof Set) return 'set';
|
|
658
|
+
if (value instanceof Map) return 'zset';
|
|
659
|
+
if (Array.isArray(value)) return 'array';
|
|
660
|
+
return typeof value;
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
// ==================== 过期时间管理 ====================
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* 设置键的过期时间
|
|
667
|
+
* @param {String} key 键名
|
|
668
|
+
* @param {Number} seconds 过期时间(秒)
|
|
669
|
+
* @returns {Promise<Boolean>} 是否成功
|
|
670
|
+
*/
|
|
671
|
+
CacheBase.prototype.expire = async function (key, seconds) {
|
|
672
|
+
try {
|
|
673
|
+
if (!key || typeof key !== 'string') {
|
|
674
|
+
return false;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (typeof seconds !== 'number' || seconds <= 0) {
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// 触发前置事件
|
|
682
|
+
const ctx = {
|
|
683
|
+
key,
|
|
684
|
+
ttl: seconds,
|
|
685
|
+
cancel: false
|
|
686
|
+
};
|
|
687
|
+
this.emit('expire:before', ctx);
|
|
688
|
+
|
|
689
|
+
if (ctx.cancel) {
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const {
|
|
694
|
+
key: finalKey,
|
|
695
|
+
ttl: finalSeconds
|
|
696
|
+
} = ctx;
|
|
697
|
+
|
|
698
|
+
// 检查键是否存在
|
|
699
|
+
if (!(finalKey in this._data)) {
|
|
700
|
+
return false;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// 设置过期时间
|
|
704
|
+
this._expires[finalKey] = Date.now() + finalSeconds * 1000;
|
|
705
|
+
|
|
706
|
+
// 触发后置事件
|
|
707
|
+
this.emit('expire:after', {
|
|
708
|
+
key: finalKey,
|
|
709
|
+
ttl: finalSeconds,
|
|
710
|
+
success: true
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
return true;
|
|
714
|
+
} catch (error) {
|
|
715
|
+
this.log('error', `Failed to set expire for key ${key}:`, error);
|
|
716
|
+
this.emit('expire:error', { key, ttl: seconds, error });
|
|
717
|
+
return false;
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* 设置键的过期时间(毫秒)
|
|
723
|
+
* @param {String} key 键名
|
|
724
|
+
* @param {Number} milliseconds 过期时间(毫秒)
|
|
725
|
+
* @returns {Promise<Boolean>} 是否成功
|
|
726
|
+
*/
|
|
727
|
+
CacheBase.prototype.pexpire = async function (key, milliseconds) {
|
|
728
|
+
try {
|
|
729
|
+
if (!key || typeof key !== 'string') {
|
|
730
|
+
throw new TypeError('key必须是非空字符串');
|
|
731
|
+
}
|
|
732
|
+
if (typeof milliseconds !== 'number' || milliseconds <= 0) {
|
|
733
|
+
throw new TypeError('milliseconds必须是正数');
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// 触发前置事件
|
|
737
|
+
const ctx = {
|
|
738
|
+
key,
|
|
739
|
+
ttl: milliseconds,
|
|
740
|
+
cancel: false
|
|
741
|
+
};
|
|
742
|
+
this.emit('pexpire:before', ctx);
|
|
743
|
+
|
|
744
|
+
if (ctx.cancel) {
|
|
745
|
+
return false;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const {
|
|
749
|
+
key: finalKey,
|
|
750
|
+
ttl: finalMilliseconds
|
|
751
|
+
} = ctx;
|
|
752
|
+
|
|
753
|
+
// 检查键是否存在
|
|
754
|
+
if (!(finalKey in this._data)) {
|
|
755
|
+
return false;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// 设置过期时间(毫秒)
|
|
759
|
+
this._expires[finalKey] = Date.now() + finalMilliseconds;
|
|
760
|
+
|
|
761
|
+
// 触发后置事件
|
|
762
|
+
this.emit('pexpire:after', {
|
|
763
|
+
key: finalKey,
|
|
764
|
+
ttl: finalMilliseconds,
|
|
765
|
+
success: true
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
return true;
|
|
769
|
+
} catch (error) {
|
|
770
|
+
this.log('error', `Failed to set pexpire for key ${key}:`, error);
|
|
771
|
+
this.emit('pexpire:error', { key, ttl: milliseconds, error });
|
|
772
|
+
return false;
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* 移除键的过期时间
|
|
778
|
+
* @param {String} key 键名
|
|
779
|
+
* @returns {Promise<Boolean>} 是否成功移除
|
|
780
|
+
*/
|
|
781
|
+
CacheBase.prototype.persist = async function (key) {
|
|
782
|
+
try {
|
|
783
|
+
if (!key || typeof key !== 'string') {
|
|
784
|
+
return false;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// 触发前置事件
|
|
788
|
+
const ctx = {
|
|
789
|
+
key,
|
|
790
|
+
cancel: false
|
|
791
|
+
};
|
|
792
|
+
this.emit('persist:before', ctx);
|
|
793
|
+
|
|
794
|
+
if (ctx.cancel) {
|
|
795
|
+
return false;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const {
|
|
799
|
+
key: finalKey
|
|
800
|
+
} = ctx;
|
|
801
|
+
|
|
802
|
+
// 检查键是否存在
|
|
803
|
+
if (!(finalKey in this._data)) {
|
|
804
|
+
return false;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// 检查是否过期
|
|
808
|
+
if (this._isExpired(finalKey)) {
|
|
809
|
+
await this.del(finalKey);
|
|
810
|
+
return false;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// 移除过期时间
|
|
814
|
+
if (finalKey in this._expires) {
|
|
815
|
+
delete this._expires[finalKey];
|
|
816
|
+
|
|
817
|
+
this.emit('persist:after', {
|
|
818
|
+
key: finalKey,
|
|
819
|
+
success: true
|
|
820
|
+
});
|
|
821
|
+
return true;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
return false;
|
|
825
|
+
} catch (error) {
|
|
826
|
+
this.log('error', `Failed to persist key ${key}:`, error);
|
|
827
|
+
this.emit('persist:error', { key, error });
|
|
828
|
+
return false;
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* 获取或设置键的过期时间
|
|
834
|
+
* @param {String} key 键名
|
|
835
|
+
* @param {Number} [seconds] 过期时间(秒),不传则获取剩余过期时间
|
|
836
|
+
* @returns {Promise<Number|Boolean>} 不传seconds返回剩余秒数(-1无过期,-2已过期),传seconds返回是否设置成功
|
|
837
|
+
*/
|
|
838
|
+
CacheBase.prototype.ttl = async function (key, seconds) {
|
|
839
|
+
const isSetMode = arguments.length > 1 && seconds !== undefined;
|
|
840
|
+
try {
|
|
841
|
+
this.emit('ttl:before', { key, seconds });
|
|
842
|
+
|
|
843
|
+
if (!key || typeof key !== 'string') {
|
|
844
|
+
return isSetMode ? false : -2;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
if (isSetMode) {
|
|
848
|
+
// 设置过期时间模式
|
|
849
|
+
if (typeof seconds !== 'number' || seconds < 0) {
|
|
850
|
+
return false;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
if (!(key in this._data)) {
|
|
854
|
+
return false;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
if (this._isExpired(key)) {
|
|
858
|
+
await this.del(key);
|
|
859
|
+
return false;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
if (seconds > 0) {
|
|
863
|
+
this._expires[key] = Date.now() + seconds * 1000;
|
|
864
|
+
} else if (seconds === 0) {
|
|
865
|
+
delete this._expires[key];
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
this.emit('ttl:after', { key, seconds, success: true });
|
|
869
|
+
return true;
|
|
870
|
+
} else {
|
|
871
|
+
// 获取剩余过期时间模式
|
|
872
|
+
if (!(key in this._data)) {
|
|
873
|
+
return -2;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
if (this._isExpired(key)) {
|
|
877
|
+
await this.del(key);
|
|
878
|
+
return -2;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
if (!(key in this._expires)) {
|
|
882
|
+
return -1;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
const now = Date.now();
|
|
886
|
+
const remaining = this._expires[key] - now;
|
|
887
|
+
return Math.max(0, Math.ceil(remaining / 1000));
|
|
888
|
+
}
|
|
889
|
+
} catch (error) {
|
|
890
|
+
this.log('error', `Failed to ttl for key ${key}:`, error);
|
|
891
|
+
return isSetMode ? false : -2;
|
|
892
|
+
}
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
// ==================== 哈希表操作 ====================
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* 设置哈希表字段
|
|
899
|
+
* @param {String} key 哈希表键名
|
|
900
|
+
* @param {String} field 字段名
|
|
901
|
+
* @param {*} value 值
|
|
902
|
+
* @returns {Promise<Boolean>} 操作结果
|
|
903
|
+
*/
|
|
904
|
+
CacheBase.prototype.hset = async function (key, field, value) {
|
|
905
|
+
try {
|
|
906
|
+
if (!key || typeof key !== 'string' || !field || typeof field !== 'string') {
|
|
907
|
+
return false;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// 触发前置事件
|
|
911
|
+
const ctx = {
|
|
912
|
+
key,
|
|
913
|
+
field,
|
|
914
|
+
value,
|
|
915
|
+
cancel: false
|
|
916
|
+
};
|
|
917
|
+
this.emit('hset:before', ctx);
|
|
918
|
+
|
|
919
|
+
if (ctx.cancel) {
|
|
920
|
+
return false;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
const {
|
|
924
|
+
key: finalKey,
|
|
925
|
+
field: finalField,
|
|
926
|
+
value: finalValue
|
|
927
|
+
} = ctx;
|
|
928
|
+
|
|
929
|
+
// 检查是否过期
|
|
930
|
+
if (this._isExpired(finalKey)) {
|
|
931
|
+
await this.del(finalKey);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// 初始化哈希表
|
|
935
|
+
if (!(finalKey in this._data) || this._types[finalKey] !== 'hash') {
|
|
936
|
+
this._data[finalKey] = {};
|
|
937
|
+
this._types[finalKey] = 'hash';
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// 设置字段值
|
|
941
|
+
this._data[finalKey][finalField] = finalValue;
|
|
942
|
+
|
|
943
|
+
this.emit('hset:after', {
|
|
944
|
+
key: finalKey,
|
|
945
|
+
field: finalField,
|
|
946
|
+
value: finalValue,
|
|
947
|
+
success: true
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
return true;
|
|
951
|
+
} catch (error) {
|
|
952
|
+
this.log('error', `Failed to hset for key ${key}:`, error);
|
|
953
|
+
this.emit('hset:error', { key, field, value, error });
|
|
954
|
+
return false;
|
|
955
|
+
}
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
/**
|
|
959
|
+
* 获取哈希表字段值
|
|
960
|
+
* @param {String} key 哈希表键名
|
|
961
|
+
* @param {String} field 字段名
|
|
962
|
+
* @returns {Promise<Object|null>} 字段值,不存在或错误返回null
|
|
963
|
+
*/
|
|
964
|
+
CacheBase.prototype.hget = async function (key, field) {
|
|
965
|
+
try {
|
|
966
|
+
this.emit('hget:before', { key, field });
|
|
967
|
+
|
|
968
|
+
if (!key || typeof key !== 'string' || !field || typeof field !== 'string') {
|
|
969
|
+
return null;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// 检查是否过期
|
|
973
|
+
if (this._isExpired(key)) {
|
|
974
|
+
await this.del(key);
|
|
975
|
+
return null;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// 检查键是否存在且类型正确
|
|
979
|
+
if (!(key in this._data) || this._types[key] !== 'hash') {
|
|
980
|
+
return null;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
const value = this._data[key][field];
|
|
984
|
+
this.emit('hget:after', { key, field, value });
|
|
985
|
+
return value !== undefined ? value : null;
|
|
986
|
+
} catch (error) {
|
|
987
|
+
this.log('error', `Failed to hget for key ${key}:`, error);
|
|
988
|
+
return null;
|
|
989
|
+
}
|
|
990
|
+
};
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* 获取哈希表所有字段和值
|
|
994
|
+
* @param {String} key 哈希表键名
|
|
995
|
+
* @returns {Promise<Object>} 哈希表内容
|
|
996
|
+
*/
|
|
997
|
+
CacheBase.prototype.hgetall = async function (key) {
|
|
998
|
+
try {
|
|
999
|
+
this.emit('hgetall:before', { key });
|
|
1000
|
+
|
|
1001
|
+
if (!key || typeof key !== 'string') {
|
|
1002
|
+
return {};
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// 检查是否过期
|
|
1006
|
+
if (this._isExpired(key)) {
|
|
1007
|
+
await this.del(key);
|
|
1008
|
+
return {};
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// 检查键是否存在且类型正确
|
|
1012
|
+
if (!(key in this._data) || this._types[key] !== 'hash') {
|
|
1013
|
+
return {};
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
const result = { ...this._data[key] };
|
|
1017
|
+
this.emit('hgetall:after', { key, result });
|
|
1018
|
+
return result;
|
|
1019
|
+
} catch (error) {
|
|
1020
|
+
this.log('error', `Failed to hgetall for key ${key}:`, error);
|
|
1021
|
+
return {};
|
|
1022
|
+
}
|
|
1023
|
+
};
|
|
1024
|
+
|
|
1025
|
+
/**
|
|
1026
|
+
* 删除哈希表字段
|
|
1027
|
+
* @param {String} key 哈希表键名
|
|
1028
|
+
* @param {String|Array<String>} field 字段名或字段名数组
|
|
1029
|
+
* @returns {Promise<Number>} 删除的字段数量
|
|
1030
|
+
*/
|
|
1031
|
+
CacheBase.prototype.hdel = async function (key, field) {
|
|
1032
|
+
try {
|
|
1033
|
+
if (!key || typeof key !== 'string') {
|
|
1034
|
+
return 0;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// 触发前置事件
|
|
1038
|
+
const ctx = {
|
|
1039
|
+
key,
|
|
1040
|
+
field,
|
|
1041
|
+
cancel: false
|
|
1042
|
+
};
|
|
1043
|
+
this.emit('hdel:before', ctx);
|
|
1044
|
+
|
|
1045
|
+
if (ctx.cancel) {
|
|
1046
|
+
return 0;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// 检查是否过期
|
|
1050
|
+
if (this._isExpired(key)) {
|
|
1051
|
+
await this.del(key);
|
|
1052
|
+
this.emit('hdel:after', { key, field, success: false, deleted: 0 });
|
|
1053
|
+
return 0;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// 检查键是否存在且类型正确
|
|
1057
|
+
if (!(key in this._data) || this._types[key] !== 'hash') {
|
|
1058
|
+
this.emit('hdel:after', { key, field, success: false, deleted: 0 });
|
|
1059
|
+
return 0;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// 处理字段参数
|
|
1063
|
+
const fields = Array.isArray(field) ? field : [field];
|
|
1064
|
+
let deletedCount = 0;
|
|
1065
|
+
|
|
1066
|
+
for (const f of fields) {
|
|
1067
|
+
if (typeof f === 'string' && f && f in this._data[key]) {
|
|
1068
|
+
delete this._data[key][f];
|
|
1069
|
+
deletedCount++;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// 如果哈希表为空,删除整个键
|
|
1074
|
+
if (deletedCount > 0 && Object.keys(this._data[key]).length === 0) {
|
|
1075
|
+
delete this._data[key];
|
|
1076
|
+
delete this._types[key];
|
|
1077
|
+
delete this._expires[key];
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
this.emit('hdel:after', {
|
|
1081
|
+
key,
|
|
1082
|
+
field,
|
|
1083
|
+
success: deletedCount > 0,
|
|
1084
|
+
deleted: deletedCount
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
return deletedCount;
|
|
1088
|
+
} catch (error) {
|
|
1089
|
+
this.emit('hdel:error', { key, field, error });
|
|
1090
|
+
this.log('error', 'Failed to hdel:', error);
|
|
1091
|
+
return 0;
|
|
1092
|
+
}
|
|
1093
|
+
};
|
|
1094
|
+
|
|
1095
|
+
/**
|
|
1096
|
+
* 批量设置哈希表字段
|
|
1097
|
+
* @param {String} key 哈希表键名
|
|
1098
|
+
* @param {Object} obj 包含多个字段的对象
|
|
1099
|
+
* @returns {Promise<Boolean>} 操作结果
|
|
1100
|
+
*/
|
|
1101
|
+
CacheBase.prototype.hmset = async function (key, obj) {
|
|
1102
|
+
try {
|
|
1103
|
+
if (!key || typeof key !== 'string' || !obj || typeof obj !== 'object' || Array.isArray(obj)) {
|
|
1104
|
+
return false;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// 触发前置事件
|
|
1108
|
+
const ctx = {
|
|
1109
|
+
key,
|
|
1110
|
+
value: obj,
|
|
1111
|
+
cancel: false
|
|
1112
|
+
};
|
|
1113
|
+
this.emit('hmset:before', ctx);
|
|
1114
|
+
|
|
1115
|
+
if (ctx.cancel) {
|
|
1116
|
+
return false;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
const {
|
|
1120
|
+
key: finalKey,
|
|
1121
|
+
value: finalObj
|
|
1122
|
+
} = ctx;
|
|
1123
|
+
|
|
1124
|
+
// 检查是否过期
|
|
1125
|
+
if (this._isExpired(finalKey)) {
|
|
1126
|
+
await this.del(finalKey);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// 初始化哈希表
|
|
1130
|
+
if (!(finalKey in this._data) || this._types[finalKey] !== 'hash') {
|
|
1131
|
+
this._data[finalKey] = {};
|
|
1132
|
+
this._types[finalKey] = 'hash';
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// 批量设置字段
|
|
1136
|
+
for (const field in finalObj) {
|
|
1137
|
+
if (Object.prototype.hasOwnProperty.call(finalObj, field)) {
|
|
1138
|
+
this._data[finalKey][field] = finalObj[field];
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
this.emit('hmset:after', {
|
|
1143
|
+
key: finalKey,
|
|
1144
|
+
value: finalObj,
|
|
1145
|
+
success: true
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
return true;
|
|
1149
|
+
} catch (error) {
|
|
1150
|
+
this.emit('hmset:error', { key, value: obj, error });
|
|
1151
|
+
this.log('error', 'Failed to hmset:', error);
|
|
1152
|
+
return false;
|
|
1153
|
+
}
|
|
1154
|
+
};
|
|
1155
|
+
|
|
1156
|
+
/**
|
|
1157
|
+
* 批量获取哈希表字段
|
|
1158
|
+
* @param {String} key 哈希表键名
|
|
1159
|
+
* @param {Array} fields 字段名数组
|
|
1160
|
+
* @returns {Promise<Array>} 字段值数组,不存在的字段返回null
|
|
1161
|
+
*/
|
|
1162
|
+
CacheBase.prototype.hmget = async function (key, fields) {
|
|
1163
|
+
try {
|
|
1164
|
+
this.emit('hmget:before', { key, fields });
|
|
1165
|
+
|
|
1166
|
+
if (!key || typeof key !== 'string' || !Array.isArray(fields)) {
|
|
1167
|
+
return [];
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// 检查是否过期
|
|
1171
|
+
if (this._isExpired(key)) {
|
|
1172
|
+
await this.del(key);
|
|
1173
|
+
return fields.map(() => null);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// 检查键是否存在且类型正确
|
|
1177
|
+
if (!(key in this._data) || this._types[key] !== 'hash') {
|
|
1178
|
+
return fields.map(() => null);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// 批量获取字段值
|
|
1182
|
+
const results = [];
|
|
1183
|
+
for (const field of fields) {
|
|
1184
|
+
const val = this._data[key][field];
|
|
1185
|
+
results.push(val !== undefined ? val : null);
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
this.emit('hmget:after', { key, fields, results });
|
|
1189
|
+
return results;
|
|
1190
|
+
} catch (error) {
|
|
1191
|
+
this.emit('hmget:error', { key, fields, error });
|
|
1192
|
+
this.log('error', 'Failed to hmget:', error);
|
|
1193
|
+
return [];
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1196
|
+
|
|
1197
|
+
/**
|
|
1198
|
+
* 获取哈希表字段数量
|
|
1199
|
+
* @param {String} key 哈希表键名
|
|
1200
|
+
* @returns {Promise<Number>} 字段数量
|
|
1201
|
+
*/
|
|
1202
|
+
CacheBase.prototype.hlen = async function (key) {
|
|
1203
|
+
try {
|
|
1204
|
+
this.emit('hlen:before', { key });
|
|
1205
|
+
|
|
1206
|
+
if (!key || typeof key !== 'string') {
|
|
1207
|
+
return 0;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// 检查是否过期
|
|
1211
|
+
if (this._isExpired(key)) {
|
|
1212
|
+
await this.del(key);
|
|
1213
|
+
return 0;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// 检查键是否存在且类型正确
|
|
1217
|
+
if (!(key in this._data) || this._types[key] !== 'hash') {
|
|
1218
|
+
return 0;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
const result = Object.keys(this._data[key]).length;
|
|
1222
|
+
this.emit('hlen:after', { key, result });
|
|
1223
|
+
return result;
|
|
1224
|
+
} catch (error) {
|
|
1225
|
+
this.emit('hlen:error', { key, error });
|
|
1226
|
+
this.log('error', 'Failed to hlen:', error);
|
|
1227
|
+
return 0;
|
|
1228
|
+
}
|
|
1229
|
+
};
|
|
1230
|
+
|
|
1231
|
+
// ==================== 数值操作 ====================
|
|
1232
|
+
|
|
1233
|
+
/**
|
|
1234
|
+
* 将键的值增加1
|
|
1235
|
+
* @param {String} key 键
|
|
1236
|
+
* @param {Number} [seconds=0] 过期时间(秒)
|
|
1237
|
+
* @returns {Promise<Number>} 增加后的值,失败返回null
|
|
1238
|
+
*/
|
|
1239
|
+
CacheBase.prototype.incr = async function (key, seconds = 0) {
|
|
1240
|
+
try {
|
|
1241
|
+
if (!key || typeof key !== 'string') {
|
|
1242
|
+
throw new TypeError('键必须是非空字符串');
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
const ctx = {
|
|
1246
|
+
key,
|
|
1247
|
+
ttl: seconds,
|
|
1248
|
+
cancel: false
|
|
1249
|
+
};
|
|
1250
|
+
this.emit('incr:before', ctx);
|
|
1251
|
+
|
|
1252
|
+
if (ctx.cancel) {
|
|
1253
|
+
return null;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
const {
|
|
1257
|
+
key: finalKey,
|
|
1258
|
+
ttl: finalSeconds
|
|
1259
|
+
} = ctx;
|
|
1260
|
+
|
|
1261
|
+
// 确保_data对象存在
|
|
1262
|
+
if (!this._data) {
|
|
1263
|
+
this._data = {};
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
let current = 0;
|
|
1267
|
+
if (finalKey in this._data && !this._isExpired(finalKey)) {
|
|
1268
|
+
current = Number(this._data[finalKey]) || 0;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
const newValue = current + 1;
|
|
1272
|
+
this._data[finalKey] = newValue;
|
|
1273
|
+
this._types[finalKey] = 'number';
|
|
1274
|
+
|
|
1275
|
+
if (finalSeconds > 0) {
|
|
1276
|
+
this._expires[finalKey] = Date.now() + finalSeconds * 1000;
|
|
1277
|
+
} else {
|
|
1278
|
+
delete this._expires[finalKey];
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
this.emit('incr:after', {
|
|
1282
|
+
key: finalKey,
|
|
1283
|
+
ttl: finalSeconds,
|
|
1284
|
+
success: true,
|
|
1285
|
+
result: newValue
|
|
1286
|
+
});
|
|
1287
|
+
return newValue;
|
|
1288
|
+
} catch (error) {
|
|
1289
|
+
this.emit('incr:error', { key, ttl: seconds, error });
|
|
1290
|
+
this.log('error', 'incr操作失败:', error);
|
|
1291
|
+
return null;
|
|
1292
|
+
}
|
|
1293
|
+
};
|
|
1294
|
+
|
|
1295
|
+
/**
|
|
1296
|
+
* 将键的值减少1
|
|
1297
|
+
* @param {String} key 键
|
|
1298
|
+
* @param {Number} [seconds=0] 过期时间(秒)
|
|
1299
|
+
* @returns {Promise<Number>} 减少后的值,失败返回null
|
|
1300
|
+
*/
|
|
1301
|
+
CacheBase.prototype.decr = async function (key, seconds = 0) {
|
|
1302
|
+
try {
|
|
1303
|
+
if (!key || typeof key !== 'string') {
|
|
1304
|
+
throw new TypeError('键必须是非空字符串');
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
const ctx = {
|
|
1308
|
+
key,
|
|
1309
|
+
ttl: seconds,
|
|
1310
|
+
cancel: false
|
|
1311
|
+
};
|
|
1312
|
+
this.emit('decr:before', ctx);
|
|
1313
|
+
|
|
1314
|
+
if (ctx.cancel) {
|
|
1315
|
+
return null;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
const {
|
|
1319
|
+
key: finalKey,
|
|
1320
|
+
ttl: finalSeconds
|
|
1321
|
+
} = ctx;
|
|
1322
|
+
|
|
1323
|
+
if (!this._data) {
|
|
1324
|
+
this._data = {};
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
let current = 0;
|
|
1328
|
+
if (finalKey in this._data && !this._isExpired(finalKey)) {
|
|
1329
|
+
current = Number(this._data[finalKey]) || 0;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
const newValue = current - 1;
|
|
1333
|
+
this._data[finalKey] = newValue;
|
|
1334
|
+
this._types[finalKey] = 'number';
|
|
1335
|
+
|
|
1336
|
+
if (finalSeconds > 0) {
|
|
1337
|
+
this._expires[finalKey] = Date.now() + finalSeconds * 1000;
|
|
1338
|
+
} else {
|
|
1339
|
+
delete this._expires[finalKey];
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
this.emit('decr:after', {
|
|
1343
|
+
key: finalKey,
|
|
1344
|
+
ttl: finalSeconds,
|
|
1345
|
+
success: true,
|
|
1346
|
+
result: newValue
|
|
1347
|
+
});
|
|
1348
|
+
return newValue;
|
|
1349
|
+
} catch (error) {
|
|
1350
|
+
this.emit('decr:error', { key, ttl: seconds, error });
|
|
1351
|
+
this.log('error', 'decr操作失败:', error);
|
|
1352
|
+
return null;
|
|
1353
|
+
}
|
|
1354
|
+
};
|
|
1355
|
+
|
|
1356
|
+
/**
|
|
1357
|
+
* 将键的值增加指定整数
|
|
1358
|
+
* @param {String} key 键
|
|
1359
|
+
* @param {Number} increment 增量
|
|
1360
|
+
* @param {Number} [seconds=0] 过期时间(秒)
|
|
1361
|
+
* @returns {Promise<Number>} 增加后的值,失败返回null
|
|
1362
|
+
*/
|
|
1363
|
+
CacheBase.prototype.incrby = async function (key, increment, seconds = 0) {
|
|
1364
|
+
// 检查服务状态
|
|
1365
|
+
if (!this.is_inited || this.is_destroyed) {
|
|
1366
|
+
this.log('error', 'Service is not initialized or has been destroyed');
|
|
1367
|
+
return null;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
if (!key || typeof key !== 'string' || typeof increment !== 'number') {
|
|
1371
|
+
this.log('error', 'incrby参数错误: key必须是非空字符串,increment必须是数字');
|
|
1372
|
+
return null;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
try {
|
|
1376
|
+
const ctx = {
|
|
1377
|
+
key,
|
|
1378
|
+
increment,
|
|
1379
|
+
seconds,
|
|
1380
|
+
cancel: false
|
|
1381
|
+
};
|
|
1382
|
+
this.emit('incrby:before', ctx);
|
|
1383
|
+
|
|
1384
|
+
if (ctx.cancel) {
|
|
1385
|
+
return null;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
const {
|
|
1389
|
+
key: finalKey,
|
|
1390
|
+
increment: finalIncrement,
|
|
1391
|
+
seconds: finalSeconds
|
|
1392
|
+
} = ctx;
|
|
1393
|
+
|
|
1394
|
+
if (!this._data) {
|
|
1395
|
+
this._data = {};
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
let current = 0;
|
|
1399
|
+
if (finalKey in this._data && !this._isExpired(finalKey)) {
|
|
1400
|
+
current = Number(this._data[finalKey]) || 0;
|
|
1401
|
+
} else if (finalKey in this._data && this._isExpired(finalKey)) {
|
|
1402
|
+
await this.del(finalKey);
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
const newValue = current + finalIncrement;
|
|
1406
|
+
this._data[finalKey] = newValue;
|
|
1407
|
+
this._types[finalKey] = 'number';
|
|
1408
|
+
|
|
1409
|
+
// 设置过期时间(保留 TTL 语义与 Redis 一致)
|
|
1410
|
+
if (finalSeconds > 0) {
|
|
1411
|
+
this._expires[finalKey] = Date.now() + finalSeconds * 1000;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
this.emit('incrby:after', {
|
|
1415
|
+
key: finalKey,
|
|
1416
|
+
increment: finalIncrement,
|
|
1417
|
+
seconds: finalSeconds,
|
|
1418
|
+
success: true,
|
|
1419
|
+
result: newValue
|
|
1420
|
+
});
|
|
1421
|
+
|
|
1422
|
+
return newValue;
|
|
1423
|
+
} catch (error) {
|
|
1424
|
+
this.emit('incrby:error', { key, increment, seconds, error });
|
|
1425
|
+
this.log('error', 'incrby操作失败:', error);
|
|
1426
|
+
return null;
|
|
1427
|
+
}
|
|
1428
|
+
};
|
|
1429
|
+
|
|
1430
|
+
/**
|
|
1431
|
+
* 将键的值减少指定整数
|
|
1432
|
+
* @param {String} key 键
|
|
1433
|
+
* @param {Number} decrement 减量
|
|
1434
|
+
* @param {Number} [seconds=0] 过期时间(秒)
|
|
1435
|
+
* @returns {Promise<Number>} 减少后的值,失败返回null
|
|
1436
|
+
*/
|
|
1437
|
+
CacheBase.prototype.decrby = async function (key, decrement, seconds = 0) {
|
|
1438
|
+
// 检查服务状态
|
|
1439
|
+
if (!this.is_inited || this.is_destroyed) {
|
|
1440
|
+
this.log('error', 'Service is not initialized or has been destroyed');
|
|
1441
|
+
return null;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
if (!key || typeof key !== 'string' || typeof decrement !== 'number') {
|
|
1445
|
+
this.log('error', 'decrby参数错误: key必须是非空字符串,decrement必须是数字');
|
|
1446
|
+
return null;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
try {
|
|
1450
|
+
const ctx = {
|
|
1451
|
+
key,
|
|
1452
|
+
decrement,
|
|
1453
|
+
seconds,
|
|
1454
|
+
cancel: false
|
|
1455
|
+
};
|
|
1456
|
+
this.emit('decrby:before', ctx);
|
|
1457
|
+
|
|
1458
|
+
if (ctx.cancel) {
|
|
1459
|
+
return null;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
const {
|
|
1463
|
+
key: finalKey,
|
|
1464
|
+
decrement: finalDecrement,
|
|
1465
|
+
seconds: finalSeconds
|
|
1466
|
+
} = ctx;
|
|
1467
|
+
|
|
1468
|
+
if (!this._data) {
|
|
1469
|
+
this._data = {};
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
let current = 0;
|
|
1473
|
+
if (finalKey in this._data && !this._isExpired(finalKey)) {
|
|
1474
|
+
current = Number(this._data[finalKey]) || 0;
|
|
1475
|
+
} else if (finalKey in this._data && this._isExpired(finalKey)) {
|
|
1476
|
+
await this.del(finalKey);
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
const newValue = current - finalDecrement;
|
|
1480
|
+
this._data[finalKey] = newValue;
|
|
1481
|
+
this._types[finalKey] = 'number';
|
|
1482
|
+
|
|
1483
|
+
if (finalSeconds > 0) {
|
|
1484
|
+
this._expires[finalKey] = Date.now() + finalSeconds * 1000;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
this.emit('decrby:after', {
|
|
1488
|
+
key: finalKey,
|
|
1489
|
+
decrement: finalDecrement,
|
|
1490
|
+
seconds: finalSeconds,
|
|
1491
|
+
success: true,
|
|
1492
|
+
result: newValue
|
|
1493
|
+
});
|
|
1494
|
+
|
|
1495
|
+
return newValue;
|
|
1496
|
+
} catch (error) {
|
|
1497
|
+
this.emit('decrby:error', { key, decrement, seconds, error });
|
|
1498
|
+
this.log('error', 'decrby操作失败:', error);
|
|
1499
|
+
return null;
|
|
1500
|
+
}
|
|
1501
|
+
};
|
|
1502
|
+
|
|
1503
|
+
/**
|
|
1504
|
+
* 将键的值增加指定浮点数
|
|
1505
|
+
* @param {String} key 键
|
|
1506
|
+
* @param {Number} [increment=1.0] 增量
|
|
1507
|
+
* @param {Number} [seconds=0] 过期时间(秒)
|
|
1508
|
+
* @returns {Promise<Number>} 增加后的值,失败返回null
|
|
1509
|
+
*/
|
|
1510
|
+
CacheBase.prototype.incrbyfloat = async function (key, increment = 1.0, seconds = 0) {
|
|
1511
|
+
try {
|
|
1512
|
+
if (!key || typeof key !== 'string' || typeof increment !== 'number') {
|
|
1513
|
+
this.log('error', 'incrbyfloat参数错误: key必须是非空字符串,increment必须是数字');
|
|
1514
|
+
return null;
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
const ctx = {
|
|
1518
|
+
key,
|
|
1519
|
+
increment,
|
|
1520
|
+
ttl: seconds,
|
|
1521
|
+
cancel: false
|
|
1522
|
+
};
|
|
1523
|
+
this.emit('incrbyfloat:before', ctx);
|
|
1524
|
+
|
|
1525
|
+
if (ctx.cancel) {
|
|
1526
|
+
return null;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
const {
|
|
1530
|
+
key: finalKey,
|
|
1531
|
+
increment: finalIncrement,
|
|
1532
|
+
ttl: finalSeconds
|
|
1533
|
+
} = ctx;
|
|
1534
|
+
|
|
1535
|
+
if (!this._data) {
|
|
1536
|
+
this._data = {};
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
let current = 0;
|
|
1540
|
+
if (finalKey in this._data && !this._isExpired(finalKey)) {
|
|
1541
|
+
current = Number(this._data[finalKey]) || 0;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
const newValue = current + finalIncrement;
|
|
1545
|
+
this._data[finalKey] = newValue;
|
|
1546
|
+
this._types[finalKey] = 'number';
|
|
1547
|
+
|
|
1548
|
+
if (finalSeconds > 0) {
|
|
1549
|
+
this._expires[finalKey] = Date.now() + finalSeconds * 1000;
|
|
1550
|
+
} else {
|
|
1551
|
+
delete this._expires[finalKey];
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
this.emit('incrbyfloat:after', {
|
|
1555
|
+
key: finalKey,
|
|
1556
|
+
increment: finalIncrement,
|
|
1557
|
+
ttl: finalSeconds,
|
|
1558
|
+
success: true,
|
|
1559
|
+
result: newValue
|
|
1560
|
+
});
|
|
1561
|
+
return newValue;
|
|
1562
|
+
} catch (error) {
|
|
1563
|
+
this.emit('incrbyfloat:error', { key, increment, ttl: seconds, error });
|
|
1564
|
+
this.log('error', 'incrbyfloat操作失败:', error);
|
|
1565
|
+
return null;
|
|
1566
|
+
}
|
|
1567
|
+
};
|
|
1568
|
+
|
|
1569
|
+
/**
|
|
1570
|
+
* 增加整数值(兼容方法)
|
|
1571
|
+
* @param {String} key 键名
|
|
1572
|
+
* @param {Number} value 要增加的整数值
|
|
1573
|
+
* @param {Number} [seconds=0] 过期时间(秒)
|
|
1574
|
+
* @returns {Promise<Number>} 增加后的值,失败返回null
|
|
1575
|
+
*/
|
|
1576
|
+
CacheBase.prototype.addInt = async function (key, value, seconds = 0) {
|
|
1577
|
+
try {
|
|
1578
|
+
if (!key || typeof key !== 'string') {
|
|
1579
|
+
throw new TypeError('key必须是非空字符串');
|
|
1580
|
+
}
|
|
1581
|
+
if (typeof value !== 'number') {
|
|
1582
|
+
throw new TypeError('value必须是数字');
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
const ctx = {
|
|
1586
|
+
key,
|
|
1587
|
+
value,
|
|
1588
|
+
ttl: seconds,
|
|
1589
|
+
cancel: false
|
|
1590
|
+
};
|
|
1591
|
+
this.emit('addInt:before', ctx);
|
|
1592
|
+
|
|
1593
|
+
if (ctx.cancel) {
|
|
1594
|
+
return null;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
const {
|
|
1598
|
+
key: finalKey,
|
|
1599
|
+
value: finalValue,
|
|
1600
|
+
ttl: finalSeconds
|
|
1601
|
+
} = ctx;
|
|
1602
|
+
|
|
1603
|
+
let currentValue = 0;
|
|
1604
|
+
if (finalKey in this._data && !this._isExpired(finalKey)) {
|
|
1605
|
+
currentValue = parseInt(this._data[finalKey], 10) || 0;
|
|
1606
|
+
}
|
|
1607
|
+
const result = Math.floor(currentValue + parseInt(finalValue, 10));
|
|
1608
|
+
|
|
1609
|
+
this._data[finalKey] = result;
|
|
1610
|
+
this._types[finalKey] = 'number';
|
|
1611
|
+
|
|
1612
|
+
if (finalSeconds > 0) {
|
|
1613
|
+
this._expires[finalKey] = Date.now() + finalSeconds * 1000;
|
|
1614
|
+
} else {
|
|
1615
|
+
delete this._expires[finalKey];
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
this.emit('addInt:after', {
|
|
1619
|
+
key: finalKey,
|
|
1620
|
+
value: finalValue,
|
|
1621
|
+
ttl: finalSeconds,
|
|
1622
|
+
success: true,
|
|
1623
|
+
result
|
|
1624
|
+
});
|
|
1625
|
+
return result;
|
|
1626
|
+
} catch (error) {
|
|
1627
|
+
this.emit('addInt:error', { key, value, ttl: seconds, error });
|
|
1628
|
+
this.log('error', 'addInt操作失败:', error);
|
|
1629
|
+
return null;
|
|
1630
|
+
}
|
|
1631
|
+
};
|
|
1632
|
+
|
|
1633
|
+
/**
|
|
1634
|
+
* 增加浮点数(兼容方法)
|
|
1635
|
+
* @param {String} key 键名
|
|
1636
|
+
* @param {Number} num 要增加的浮点数值
|
|
1637
|
+
* @param {Number} [seconds=0] 过期时间(秒)
|
|
1638
|
+
* @returns {Promise<Number>} 增加后的值,失败返回null
|
|
1639
|
+
*/
|
|
1640
|
+
CacheBase.prototype.addFloat = async function (key, num, seconds = 0) {
|
|
1641
|
+
try {
|
|
1642
|
+
if (!key || typeof key !== 'string' || typeof num !== 'number') {
|
|
1643
|
+
throw new TypeError('addFloat参数错误: key必须是非空字符串,num必须是数字');
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
const ctx = {
|
|
1647
|
+
key,
|
|
1648
|
+
num,
|
|
1649
|
+
ttl: seconds,
|
|
1650
|
+
cancel: false
|
|
1651
|
+
};
|
|
1652
|
+
this.emit('addFloat:before', ctx);
|
|
1653
|
+
|
|
1654
|
+
if (ctx.cancel) {
|
|
1655
|
+
return null;
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
const {
|
|
1659
|
+
key: finalKey,
|
|
1660
|
+
num: finalNum,
|
|
1661
|
+
ttl: finalSeconds
|
|
1662
|
+
} = ctx;
|
|
1663
|
+
|
|
1664
|
+
let currentValue = 0;
|
|
1665
|
+
if (finalKey in this._data && !this._isExpired(finalKey)) {
|
|
1666
|
+
currentValue = Number(this._data[finalKey]) || 0;
|
|
1667
|
+
}
|
|
1668
|
+
const result = currentValue + Number(finalNum);
|
|
1669
|
+
|
|
1670
|
+
this._data[finalKey] = result;
|
|
1671
|
+
this._types[finalKey] = 'number';
|
|
1672
|
+
|
|
1673
|
+
if (finalSeconds > 0) {
|
|
1674
|
+
this._expires[finalKey] = Date.now() + finalSeconds * 1000;
|
|
1675
|
+
} else {
|
|
1676
|
+
delete this._expires[finalKey];
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
this.emit('addFloat:after', {
|
|
1680
|
+
key: finalKey,
|
|
1681
|
+
num: finalNum,
|
|
1682
|
+
ttl: finalSeconds,
|
|
1683
|
+
success: true,
|
|
1684
|
+
result
|
|
1685
|
+
});
|
|
1686
|
+
return result;
|
|
1687
|
+
} catch (error) {
|
|
1688
|
+
this.emit('addFloat:error', { key, num, ttl: seconds, error });
|
|
1689
|
+
this.log('error', 'addFloat操作失败:', error);
|
|
1690
|
+
return null;
|
|
1691
|
+
}
|
|
1692
|
+
};
|
|
1693
|
+
|
|
1694
|
+
// ==================== 字符串操作 ====================
|
|
1695
|
+
|
|
1696
|
+
/**
|
|
1697
|
+
* 追加字符串(兼容方法,对齐 mm_redis 的 setrange)
|
|
1698
|
+
* @param {String} key 键名
|
|
1699
|
+
* @param {String} value 要追加的字符串
|
|
1700
|
+
* @param {Number} [seconds=0] 过期时间(秒)
|
|
1701
|
+
* @returns {Promise<Number>} 追加后字符串的长度,失败返回null
|
|
1702
|
+
*/
|
|
1703
|
+
CacheBase.prototype.setrange = async function (key, value, seconds = 0) {
|
|
1704
|
+
try {
|
|
1705
|
+
if (!key || typeof key !== 'string' || typeof value !== 'string') {
|
|
1706
|
+
throw new TypeError('setrange参数错误: key和value必须是非空字符串');
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
const ctx = {
|
|
1710
|
+
key,
|
|
1711
|
+
value,
|
|
1712
|
+
ttl: seconds,
|
|
1713
|
+
cancel: false
|
|
1714
|
+
};
|
|
1715
|
+
this.emit('setrange:before', ctx);
|
|
1716
|
+
|
|
1717
|
+
if (ctx.cancel) {
|
|
1718
|
+
return null;
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
const {
|
|
1722
|
+
key: finalKey,
|
|
1723
|
+
value: finalValue,
|
|
1724
|
+
ttl: finalSeconds
|
|
1725
|
+
} = ctx;
|
|
1726
|
+
|
|
1727
|
+
let currentValue = '';
|
|
1728
|
+
if (finalKey in this._data && !this._isExpired(finalKey)) {
|
|
1729
|
+
currentValue = String(this._data[finalKey]) || '';
|
|
1730
|
+
}
|
|
1731
|
+
const result = currentValue + finalValue;
|
|
1732
|
+
const length = result.length;
|
|
1733
|
+
|
|
1734
|
+
this._data[finalKey] = result;
|
|
1735
|
+
this._types[finalKey] = 'string';
|
|
1736
|
+
|
|
1737
|
+
if (finalSeconds > 0) {
|
|
1738
|
+
this._expires[finalKey] = Date.now() + finalSeconds * 1000;
|
|
1739
|
+
} else {
|
|
1740
|
+
delete this._expires[finalKey];
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
this.emit('setrange:after', {
|
|
1744
|
+
key: finalKey,
|
|
1745
|
+
value: finalValue,
|
|
1746
|
+
ttl: finalSeconds,
|
|
1747
|
+
result,
|
|
1748
|
+
length,
|
|
1749
|
+
success: true
|
|
1750
|
+
});
|
|
1751
|
+
return length;
|
|
1752
|
+
} catch (error) {
|
|
1753
|
+
this.emit('setrange:error', { key, value, ttl: seconds, error });
|
|
1754
|
+
this.log('error', 'setrange操作失败:', error);
|
|
1755
|
+
return null;
|
|
1756
|
+
}
|
|
1757
|
+
};
|
|
1758
|
+
|
|
1759
|
+
/**
|
|
1760
|
+
* 向字符串末尾追加内容
|
|
1761
|
+
* @param {String} key 键名
|
|
1762
|
+
* @param {String} append 要追加的内容
|
|
1763
|
+
* @returns {Promise<Number>} 追加后的字符串长度,失败返回null
|
|
1764
|
+
*/
|
|
1765
|
+
CacheBase.prototype.append = async function (key, append) {
|
|
1766
|
+
try {
|
|
1767
|
+
if (!key || typeof key !== 'string' || typeof append !== 'string') {
|
|
1768
|
+
return null;
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
// 检查是否过期
|
|
1772
|
+
if (this._isExpired(key)) {
|
|
1773
|
+
await this.del(key);
|
|
1774
|
+
return null;
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
let current = '';
|
|
1778
|
+
if (key in this._data && typeof this._data[key] === 'string') {
|
|
1779
|
+
current = this._data[key];
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
current += append;
|
|
1783
|
+
this._data[key] = current;
|
|
1784
|
+
this._types[key] = 'string';
|
|
1785
|
+
|
|
1786
|
+
return current.length;
|
|
1787
|
+
} catch (error) {
|
|
1788
|
+
this.log('error', `Failed to append for key ${key}:`, error);
|
|
1789
|
+
return null;
|
|
1790
|
+
}
|
|
1791
|
+
};
|
|
1792
|
+
|
|
1793
|
+
/**
|
|
1794
|
+
* 获取字符串长度
|
|
1795
|
+
* @param {String} key 键名
|
|
1796
|
+
* @returns {Promise<Number>} 字符串长度,不存在返回0
|
|
1797
|
+
*/
|
|
1798
|
+
CacheBase.prototype.strlen = async function (key) {
|
|
1799
|
+
try {
|
|
1800
|
+
if (!key || typeof key !== 'string') {
|
|
1801
|
+
return 0;
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
if (this._isExpired(key)) {
|
|
1805
|
+
await this.del(key);
|
|
1806
|
+
return 0;
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
if (!(key in this._data)) {
|
|
1810
|
+
return 0;
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
if (typeof this._data[key] !== 'string') {
|
|
1814
|
+
return 0;
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
return this._data[key].length;
|
|
1818
|
+
} catch (error) {
|
|
1819
|
+
this.log('error', `Failed to strlen for key ${key}:`, error);
|
|
1820
|
+
return 0;
|
|
1821
|
+
}
|
|
1822
|
+
};
|
|
1823
|
+
|
|
1824
|
+
/**
|
|
1825
|
+
* 获取并设置值
|
|
1826
|
+
* @param {String} key 键名
|
|
1827
|
+
* @param {*} value 新值
|
|
1828
|
+
* @param {Number} [seconds=0] 过期时间(秒)
|
|
1829
|
+
* @returns {Promise<Object|null>} 旧值
|
|
1830
|
+
*/
|
|
1831
|
+
CacheBase.prototype.getset = async function (key, value, seconds = 0) {
|
|
1832
|
+
try {
|
|
1833
|
+
if (!key || typeof key !== 'string') {
|
|
1834
|
+
return null;
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
// 获取旧值
|
|
1838
|
+
const oldValue = await this.get(key);
|
|
1839
|
+
|
|
1840
|
+
// 设置新值
|
|
1841
|
+
await this.set(key, value, seconds);
|
|
1842
|
+
|
|
1843
|
+
return oldValue;
|
|
1844
|
+
} catch (error) {
|
|
1845
|
+
this.log('error', 'Failed to getset:', error);
|
|
1846
|
+
return null;
|
|
1847
|
+
}
|
|
1848
|
+
};
|
|
1849
|
+
|
|
1850
|
+
/**
|
|
1851
|
+
* 仅当键不存在时设置值(兼容方法,类似 Redis SETNX 语义)
|
|
1852
|
+
* @param {String} key 键名
|
|
1853
|
+
* @param {*} value 要设置的值
|
|
1854
|
+
* @param {Number} [seconds=0] 过期时间(秒)
|
|
1855
|
+
* @returns {Promise<Boolean>} 键不存在并设置成功返回true,键已存在返回false
|
|
1856
|
+
*/
|
|
1857
|
+
CacheBase.prototype.add = async function (key, value, seconds = 0) {
|
|
1858
|
+
try {
|
|
1859
|
+
if (!key || typeof key !== 'string') {
|
|
1860
|
+
throw new TypeError('add参数错误: key必须是非空字符串');
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
const ctx = {
|
|
1864
|
+
key,
|
|
1865
|
+
value,
|
|
1866
|
+
ttl: seconds,
|
|
1867
|
+
cancel: false
|
|
1868
|
+
};
|
|
1869
|
+
this.emit('add:before', ctx);
|
|
1870
|
+
|
|
1871
|
+
if (ctx.cancel) {
|
|
1872
|
+
this.log('debug', `add操作被事件处理器取消 [${key}]`);
|
|
1873
|
+
return false;
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
const {
|
|
1877
|
+
key: finalKey,
|
|
1878
|
+
value: finalValue,
|
|
1879
|
+
ttl: finalSeconds
|
|
1880
|
+
} = ctx;
|
|
1881
|
+
|
|
1882
|
+
// 检查键是否存在且未过期
|
|
1883
|
+
if (finalKey in this._data && !this._isExpired(finalKey)) {
|
|
1884
|
+
this.emit('add:after', {
|
|
1885
|
+
key: finalKey,
|
|
1886
|
+
value: finalValue,
|
|
1887
|
+
ttl: finalSeconds,
|
|
1888
|
+
success: false
|
|
1889
|
+
});
|
|
1890
|
+
return false;
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
// 键不存在或已过期,设置新值
|
|
1894
|
+
this._data[finalKey] = finalValue;
|
|
1895
|
+
this._types[finalKey] = this._getDataTypes(finalValue);
|
|
1896
|
+
|
|
1897
|
+
if (finalSeconds > 0) {
|
|
1898
|
+
this._expires[finalKey] = Date.now() + finalSeconds * 1000;
|
|
1899
|
+
} else {
|
|
1900
|
+
delete this._expires[finalKey];
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
this.emit('add:after', {
|
|
1904
|
+
key: finalKey,
|
|
1905
|
+
value: finalValue,
|
|
1906
|
+
ttl: finalSeconds,
|
|
1907
|
+
success: true
|
|
1908
|
+
});
|
|
1909
|
+
return true;
|
|
1910
|
+
} catch (error) {
|
|
1911
|
+
this.log('error', `add操作失败 [${key}]:`, error);
|
|
1912
|
+
this.emit('add:error', { key, value, ttl: seconds, error });
|
|
1913
|
+
return false;
|
|
1914
|
+
}
|
|
1915
|
+
};
|
|
1916
|
+
|
|
1917
|
+
// ==================== 批量操作 ====================
|
|
1918
|
+
|
|
1919
|
+
/**
|
|
1920
|
+
* 一次性设置多个键值对
|
|
1921
|
+
* @param {Object} obj 键值对对象
|
|
1922
|
+
* @param {Number} [seconds=0] 过期时间(秒)
|
|
1923
|
+
* @returns {Promise<Boolean>} 成功返回true,失败返回false
|
|
1924
|
+
*/
|
|
1925
|
+
CacheBase.prototype.mset = async function (obj, seconds = 0) {
|
|
1926
|
+
// 检查服务状态
|
|
1927
|
+
if (!this.is_inited || this.is_destroyed) {
|
|
1928
|
+
this.log('error', 'Service is not initialized or has been destroyed');
|
|
1929
|
+
return false;
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
try {
|
|
1933
|
+
// 参数验证:必须是对象
|
|
1934
|
+
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
|
|
1935
|
+
throw new TypeError('参数必须是键值对对象');
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
// 触发前置事件
|
|
1939
|
+
const ctx = {
|
|
1940
|
+
keyValuePairs: obj,
|
|
1941
|
+
seconds,
|
|
1942
|
+
cancel: false
|
|
1943
|
+
};
|
|
1944
|
+
this.emit('mset:before', ctx);
|
|
1945
|
+
|
|
1946
|
+
if (ctx.cancel) {
|
|
1947
|
+
this.log('debug', '[mset] 操作被事件处理器取消');
|
|
1948
|
+
return false;
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
const {
|
|
1952
|
+
keyValuePairs: finalKeyValuePairs,
|
|
1953
|
+
seconds: finalSeconds
|
|
1954
|
+
} = ctx;
|
|
1955
|
+
|
|
1956
|
+
// 先校验所有键的合法性,避免批量写入中途失败导致部分写入(保证原子性)
|
|
1957
|
+
const entries = Object.entries(finalKeyValuePairs);
|
|
1958
|
+
for (const [key] of entries) {
|
|
1959
|
+
if (typeof key !== 'string' || !key) {
|
|
1960
|
+
throw new TypeError('键必须是非空字符串');
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
// 批量设置键值对
|
|
1965
|
+
for (const [key, value] of entries) {
|
|
1966
|
+
// 检查键是否过期
|
|
1967
|
+
if (this._isExpired(key)) {
|
|
1968
|
+
await this.del(key);
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
this._data[key] = value;
|
|
1972
|
+
this._types[key] = this._getDataTypes(value);
|
|
1973
|
+
|
|
1974
|
+
if (finalSeconds > 0) {
|
|
1975
|
+
this._expires[key] = Date.now() + finalSeconds * 1000;
|
|
1976
|
+
} else {
|
|
1977
|
+
delete this._expires[key];
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
this.emit('mset:after', {
|
|
1982
|
+
keyValuePairs: finalKeyValuePairs,
|
|
1983
|
+
seconds: finalSeconds,
|
|
1984
|
+
result: true,
|
|
1985
|
+
success: true
|
|
1986
|
+
});
|
|
1987
|
+
return true;
|
|
1988
|
+
} catch (error) {
|
|
1989
|
+
this.emit('mset:error', { keyValuePairs: obj, error });
|
|
1990
|
+
// 参数校验类错误属预期内,用 debug 级别避免误导;运行时错误保留 error 级别
|
|
1991
|
+
if (error instanceof TypeError) {
|
|
1992
|
+
this.log('debug', `[mset] 参数错误: ${error.message}`);
|
|
1993
|
+
} else {
|
|
1994
|
+
this.log('error', 'Failed to mset:', error);
|
|
1995
|
+
}
|
|
1996
|
+
return false;
|
|
1997
|
+
}
|
|
1998
|
+
};
|
|
1999
|
+
|
|
2000
|
+
/**
|
|
2001
|
+
* 批量获取多个键的值
|
|
2002
|
+
* @param {Array} keys 键名数组
|
|
2003
|
+
* @returns {Promise<Array>} 值数组,按键的顺序返回
|
|
2004
|
+
*/
|
|
2005
|
+
CacheBase.prototype.mget = async function (keys) {
|
|
2006
|
+
try {
|
|
2007
|
+
this.emit('mget:before', { keys });
|
|
2008
|
+
|
|
2009
|
+
if (!Array.isArray(keys)) {
|
|
2010
|
+
return [];
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
const results = [];
|
|
2014
|
+
for (const key of keys) {
|
|
2015
|
+
if (typeof key === 'string') {
|
|
2016
|
+
results.push(await this.get(key));
|
|
2017
|
+
} else {
|
|
2018
|
+
results.push(null);
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
this.emit('mget:after', { keys, results });
|
|
2023
|
+
return results;
|
|
2024
|
+
} catch (error) {
|
|
2025
|
+
this.log('error', 'Failed to mget:', error);
|
|
2026
|
+
return [];
|
|
2027
|
+
}
|
|
2028
|
+
};
|
|
2029
|
+
|
|
2030
|
+
// ==================== 键管理 ====================
|
|
2031
|
+
|
|
2032
|
+
/**
|
|
2033
|
+
* 获取所有键名
|
|
2034
|
+
* @param {String} [pattern] 键名匹配模式,支持*通配符
|
|
2035
|
+
* @returns {Promise<Array>} 所有键名数组
|
|
2036
|
+
*/
|
|
2037
|
+
CacheBase.prototype.keys = async function (pattern) {
|
|
2038
|
+
try {
|
|
2039
|
+
this.emit('keys:before', { pattern });
|
|
2040
|
+
|
|
2041
|
+
const result = [];
|
|
2042
|
+
for (const key in this._data) {
|
|
2043
|
+
if (!this._isExpired(key)) {
|
|
2044
|
+
if (!pattern || this._matchPattern(key, pattern)) {
|
|
2045
|
+
result.push(key);
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
this.emit('keys:after', { pattern, result });
|
|
2051
|
+
return result;
|
|
2052
|
+
} catch (error) {
|
|
2053
|
+
this.log('error', 'Failed to get keys:', error);
|
|
2054
|
+
return [];
|
|
2055
|
+
}
|
|
2056
|
+
};
|
|
2057
|
+
|
|
2058
|
+
/**
|
|
2059
|
+
* 检查键是否匹配指定模式
|
|
2060
|
+
* @private
|
|
2061
|
+
* @param {String} key 要检查的键名
|
|
2062
|
+
* @param {String} pattern 匹配模式,支持*通配符
|
|
2063
|
+
* @returns {Boolean} 是否匹配
|
|
2064
|
+
*/
|
|
2065
|
+
CacheBase.prototype._matchPattern = function (key, pattern) {
|
|
2066
|
+
try {
|
|
2067
|
+
// 转义正则元字符(包含 ])
|
|
2068
|
+
var regexPattern = pattern
|
|
2069
|
+
.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
|
|
2070
|
+
.replace(/\*/g, '.*');
|
|
2071
|
+
var regex = new RegExp('^' + regexPattern + '$');
|
|
2072
|
+
return regex.test(key);
|
|
2073
|
+
} catch (error) {
|
|
2074
|
+
return false;
|
|
2075
|
+
}
|
|
2076
|
+
};
|
|
2077
|
+
|
|
2078
|
+
/**
|
|
2079
|
+
* 获取键的数量
|
|
2080
|
+
* @returns {Promise<Number>} 键的数量
|
|
2081
|
+
*/
|
|
2082
|
+
CacheBase.prototype.dbsize = async function () {
|
|
2083
|
+
try {
|
|
2084
|
+
let count = 0;
|
|
2085
|
+
for (const key in this._data) {
|
|
2086
|
+
if (!this._isExpired(key)) {
|
|
2087
|
+
count++;
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
return count;
|
|
2091
|
+
} catch (error) {
|
|
2092
|
+
this.log('error', 'Failed to get dbsize:', error);
|
|
2093
|
+
return 0;
|
|
2094
|
+
}
|
|
2095
|
+
};
|
|
2096
|
+
|
|
2097
|
+
/**
|
|
2098
|
+
* 清空所有数据库键
|
|
2099
|
+
* @returns {Promise<Boolean>} 操作结果
|
|
2100
|
+
*/
|
|
2101
|
+
CacheBase.prototype.flushdb = async function () {
|
|
2102
|
+
try {
|
|
2103
|
+
const ctx = {
|
|
2104
|
+
cancel: false
|
|
2105
|
+
};
|
|
2106
|
+
this.emit('clear:before', ctx);
|
|
2107
|
+
|
|
2108
|
+
if (ctx.cancel) {
|
|
2109
|
+
return false;
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
this._data = {};
|
|
2113
|
+
this._expires = {};
|
|
2114
|
+
this._types = {};
|
|
2115
|
+
|
|
2116
|
+
this.emit('clear:after', { success: true });
|
|
2117
|
+
return true;
|
|
2118
|
+
} catch (error) {
|
|
2119
|
+
this.log('error', 'Failed to flushdb:', error);
|
|
2120
|
+
return false;
|
|
2121
|
+
}
|
|
2122
|
+
};
|
|
2123
|
+
|
|
2124
|
+
/**
|
|
2125
|
+
* 清空所有数据(flushdb 别名)
|
|
2126
|
+
* @returns {Promise<Boolean>} 操作结果
|
|
2127
|
+
*/
|
|
2128
|
+
CacheBase.prototype.clear = async function () {
|
|
2129
|
+
return this.flushdb();
|
|
2130
|
+
};
|
|
2131
|
+
|
|
2132
|
+
// ==================== 列表操作 ====================
|
|
2133
|
+
|
|
2134
|
+
/**
|
|
2135
|
+
* 设置列表
|
|
2136
|
+
* @param {String} key 键名
|
|
2137
|
+
* @param {Array} value 列表值
|
|
2138
|
+
* @param {Number} [seconds=0] 过期时间(秒)
|
|
2139
|
+
* @returns {Promise<Number>} 列表长度
|
|
2140
|
+
*/
|
|
2141
|
+
CacheBase.prototype.setForList = async function (key, value, seconds = 0) {
|
|
2142
|
+
try {
|
|
2143
|
+
if (!key || typeof key !== 'string') {
|
|
2144
|
+
return 0;
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
// 确保value是数组
|
|
2148
|
+
let stringValues = [];
|
|
2149
|
+
if (Array.isArray(value)) {
|
|
2150
|
+
stringValues = value.map(item => String(item));
|
|
2151
|
+
} else {
|
|
2152
|
+
stringValues = [String(value)];
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
const ctx = {
|
|
2156
|
+
key,
|
|
2157
|
+
value,
|
|
2158
|
+
seconds,
|
|
2159
|
+
cancel: false
|
|
2160
|
+
};
|
|
2161
|
+
this.emit('setForList:before', ctx);
|
|
2162
|
+
|
|
2163
|
+
if (ctx.cancel) {
|
|
2164
|
+
this.log('debug', `setForList操作被事件处理器取消 [${key}]`);
|
|
2165
|
+
return 0;
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
const {
|
|
2169
|
+
key: finalKey,
|
|
2170
|
+
value: finalValue,
|
|
2171
|
+
seconds: finalSeconds
|
|
2172
|
+
} = ctx;
|
|
2173
|
+
|
|
2174
|
+
// 重新处理 finalValue
|
|
2175
|
+
let finalStringValues = [];
|
|
2176
|
+
if (Array.isArray(finalValue)) {
|
|
2177
|
+
finalStringValues = finalValue.map(item => String(item));
|
|
2178
|
+
} else {
|
|
2179
|
+
finalStringValues = [String(finalValue)];
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
// 检查键是否存在且未过期
|
|
2183
|
+
if (finalKey in this._data && !this._isExpired(finalKey)) {
|
|
2184
|
+
if (!Array.isArray(this._data[finalKey])) {
|
|
2185
|
+
delete this._data[finalKey];
|
|
2186
|
+
delete this._types[finalKey];
|
|
2187
|
+
delete this._expires[finalKey];
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
this._data[finalKey] = finalStringValues;
|
|
2192
|
+
this._types[finalKey] = 'array';
|
|
2193
|
+
|
|
2194
|
+
if (finalSeconds > 0) {
|
|
2195
|
+
this._expires[finalKey] = Date.now() + finalSeconds * 1000;
|
|
2196
|
+
} else {
|
|
2197
|
+
delete this._expires[finalKey];
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
const length = finalStringValues.length;
|
|
2201
|
+
this.emit('setForList:after', {
|
|
2202
|
+
key: finalKey,
|
|
2203
|
+
value: finalStringValues,
|
|
2204
|
+
seconds: finalSeconds,
|
|
2205
|
+
result: length
|
|
2206
|
+
});
|
|
2207
|
+
return length;
|
|
2208
|
+
} catch (error) {
|
|
2209
|
+
this.emit('setForList:error', { key, value, seconds, error });
|
|
2210
|
+
this.log('error', 'setForList操作失败:', error);
|
|
2211
|
+
return 0;
|
|
2212
|
+
}
|
|
2213
|
+
};
|
|
2214
|
+
|
|
2215
|
+
/**
|
|
2216
|
+
* 向列表添加元素
|
|
2217
|
+
* @param {String} key 键名
|
|
2218
|
+
* @param {*} value 要添加的元素
|
|
2219
|
+
* @param {Number} [seconds=0] 过期时间(秒)
|
|
2220
|
+
* @returns {Promise<Number>} 添加后列表长度
|
|
2221
|
+
*/
|
|
2222
|
+
CacheBase.prototype.addForList = async function (key, value, seconds = 0) {
|
|
2223
|
+
try {
|
|
2224
|
+
if (!key || typeof key !== 'string') {
|
|
2225
|
+
return 0;
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
const ctx = {
|
|
2229
|
+
key,
|
|
2230
|
+
value,
|
|
2231
|
+
seconds,
|
|
2232
|
+
cancel: false
|
|
2233
|
+
};
|
|
2234
|
+
this.emit('addForList:before', ctx);
|
|
2235
|
+
|
|
2236
|
+
if (ctx.cancel) {
|
|
2237
|
+
this.log('debug', `addForList操作被事件处理器取消 [${key}]`);
|
|
2238
|
+
return 0;
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
const {
|
|
2242
|
+
key: finalKey,
|
|
2243
|
+
value: finalValue,
|
|
2244
|
+
seconds: finalSeconds
|
|
2245
|
+
} = ctx;
|
|
2246
|
+
|
|
2247
|
+
const stringValue = String(finalValue);
|
|
2248
|
+
|
|
2249
|
+
// 检查键是否存在且未过期
|
|
2250
|
+
if (finalKey in this._data && !this._isExpired(finalKey)) {
|
|
2251
|
+
if (!Array.isArray(this._data[finalKey])) {
|
|
2252
|
+
this._data[finalKey] = [stringValue];
|
|
2253
|
+
this._types[finalKey] = 'array';
|
|
2254
|
+
} else {
|
|
2255
|
+
this._data[finalKey].push(stringValue);
|
|
2256
|
+
}
|
|
2257
|
+
} else {
|
|
2258
|
+
this._data[finalKey] = [stringValue];
|
|
2259
|
+
this._types[finalKey] = 'array';
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
if (finalSeconds > 0) {
|
|
2263
|
+
this._expires[finalKey] = Date.now() + finalSeconds * 1000;
|
|
2264
|
+
} else if (finalKey in this._expires) {
|
|
2265
|
+
delete this._expires[finalKey];
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
const length = this._data[finalKey].length;
|
|
2269
|
+
this.emit('addForList:after', {
|
|
2270
|
+
key: finalKey,
|
|
2271
|
+
value: stringValue,
|
|
2272
|
+
seconds: finalSeconds,
|
|
2273
|
+
result: length
|
|
2274
|
+
});
|
|
2275
|
+
return length;
|
|
2276
|
+
} catch (error) {
|
|
2277
|
+
this.emit('addForList:error', { key, value, seconds, error });
|
|
2278
|
+
this.log('error', 'addForList操作失败:', error);
|
|
2279
|
+
return 0;
|
|
2280
|
+
}
|
|
2281
|
+
};
|
|
2282
|
+
|
|
2283
|
+
/**
|
|
2284
|
+
* 检查列表中是否包含指定元素
|
|
2285
|
+
* @param {String} key 键名
|
|
2286
|
+
* @param {*} value 要检查的元素
|
|
2287
|
+
* @returns {Promise<Boolean>} 是否包含
|
|
2288
|
+
*/
|
|
2289
|
+
CacheBase.prototype.hasForList = async function (key, value) {
|
|
2290
|
+
try {
|
|
2291
|
+
if (!key || typeof key !== 'string') {
|
|
2292
|
+
return false;
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
const ctx = {
|
|
2296
|
+
key,
|
|
2297
|
+
value,
|
|
2298
|
+
cancel: false
|
|
2299
|
+
};
|
|
2300
|
+
this.emit('hasForList:before', ctx);
|
|
2301
|
+
|
|
2302
|
+
if (ctx.cancel) {
|
|
2303
|
+
this.log('debug', `hasForList操作被事件处理器取消 [${key}]`);
|
|
2304
|
+
return false;
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
const {
|
|
2308
|
+
key: finalKey,
|
|
2309
|
+
value: finalValue
|
|
2310
|
+
} = ctx;
|
|
2311
|
+
|
|
2312
|
+
const stringValue = String(finalValue);
|
|
2313
|
+
|
|
2314
|
+
if (finalKey in this._data && !this._isExpired(finalKey)) {
|
|
2315
|
+
if (Array.isArray(this._data[finalKey])) {
|
|
2316
|
+
const result = this._data[finalKey].includes(stringValue);
|
|
2317
|
+
this.emit('hasForList:after', { key: finalKey, value: finalValue, result });
|
|
2318
|
+
return result;
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
this.emit('hasForList:after', { key: finalKey, value: finalValue, result: false });
|
|
2323
|
+
return false;
|
|
2324
|
+
} catch (error) {
|
|
2325
|
+
this.emit('hasForList:error', { key, value, error });
|
|
2326
|
+
this.log('error', 'hasForList操作失败:', error);
|
|
2327
|
+
return false;
|
|
2328
|
+
}
|
|
2329
|
+
};
|
|
2330
|
+
|
|
2331
|
+
/**
|
|
2332
|
+
* 获取列表缓存
|
|
2333
|
+
* @param {String} key 键名
|
|
2334
|
+
* @param {Number} [start=0] 起始索引
|
|
2335
|
+
* @param {Number} [end=-1] 结束索引
|
|
2336
|
+
* @returns {Promise<Array>} 列表数组
|
|
2337
|
+
*/
|
|
2338
|
+
CacheBase.prototype.getForList = async function (key, start = 0, end = -1) {
|
|
2339
|
+
try {
|
|
2340
|
+
this.emit('getForList:before', { key, start, end });
|
|
2341
|
+
|
|
2342
|
+
if (!key || typeof key !== 'string') {
|
|
2343
|
+
return [];
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
// 检查是否过期
|
|
2347
|
+
if (this._isExpired(key)) {
|
|
2348
|
+
await this.del(key);
|
|
2349
|
+
return [];
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
// 检查键是否存在
|
|
2353
|
+
if (!(key in this._data)) {
|
|
2354
|
+
return [];
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
// 检查是否为数组
|
|
2358
|
+
if (Array.isArray(this._data[key])) {
|
|
2359
|
+
const array = this._data[key];
|
|
2360
|
+
|
|
2361
|
+
// 处理负索引,符合 Redis LRANGE 语义
|
|
2362
|
+
const startIndex = start < 0
|
|
2363
|
+
? Math.max(0, array.length + start)
|
|
2364
|
+
: start;
|
|
2365
|
+
const endIndex = end < 0
|
|
2366
|
+
? Math.max(0, array.length + end + 1)
|
|
2367
|
+
: end + 1;
|
|
2368
|
+
|
|
2369
|
+
const result = array.slice(startIndex, endIndex);
|
|
2370
|
+
this.emit('getForList:after', { key, start, end, result });
|
|
2371
|
+
return result;
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
return [];
|
|
2375
|
+
} catch (error) {
|
|
2376
|
+
this.log('error', 'getForList操作失败:', error);
|
|
2377
|
+
return [];
|
|
2378
|
+
}
|
|
2379
|
+
};
|
|
2380
|
+
|
|
2381
|
+
/**
|
|
2382
|
+
* 清空列表
|
|
2383
|
+
* @param {String} key 键名
|
|
2384
|
+
* @returns {Promise<Boolean>} 操作结果
|
|
2385
|
+
*/
|
|
2386
|
+
CacheBase.prototype.clearForList = async function (key) {
|
|
2387
|
+
try {
|
|
2388
|
+
const ctx = {
|
|
2389
|
+
key,
|
|
2390
|
+
cancel: false
|
|
2391
|
+
};
|
|
2392
|
+
this.emit('clearForList:before', ctx);
|
|
2393
|
+
|
|
2394
|
+
if (ctx.cancel) {
|
|
2395
|
+
return false;
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
const {
|
|
2399
|
+
key: finalKey
|
|
2400
|
+
} = ctx;
|
|
2401
|
+
|
|
2402
|
+
if (!finalKey || typeof finalKey !== 'string') {
|
|
2403
|
+
return false;
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
if (finalKey in this._data && !this._isExpired(finalKey)) {
|
|
2407
|
+
if (Array.isArray(this._data[finalKey])) {
|
|
2408
|
+
this._data[finalKey] = [];
|
|
2409
|
+
this.emit('clearForList:after', { key: finalKey, result: true });
|
|
2410
|
+
return true;
|
|
2411
|
+
} else {
|
|
2412
|
+
delete this._data[finalKey];
|
|
2413
|
+
delete this._types[finalKey];
|
|
2414
|
+
delete this._expires[finalKey];
|
|
2415
|
+
this.emit('clearForList:after', { key: finalKey, result: false });
|
|
2416
|
+
return false;
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
this._data[finalKey] = [];
|
|
2421
|
+
this._types[finalKey] = 'array';
|
|
2422
|
+
this.emit('clearForList:after', { key: finalKey, result: true });
|
|
2423
|
+
return true;
|
|
2424
|
+
} catch (error) {
|
|
2425
|
+
this.emit('clearForList:error', { key, error });
|
|
2426
|
+
this.log('error', 'clearForList操作失败:', error);
|
|
2427
|
+
return false;
|
|
2428
|
+
}
|
|
2429
|
+
};
|
|
2430
|
+
|
|
2431
|
+
// ==================== 集合操作 ====================
|
|
2432
|
+
|
|
2433
|
+
/**
|
|
2434
|
+
* 将成员添加到集合
|
|
2435
|
+
* @param {String} key 键
|
|
2436
|
+
* @param {String|Array} members 成员或成员数组
|
|
2437
|
+
* @returns {Promise<Number>} 添加的成员数量
|
|
2438
|
+
*/
|
|
2439
|
+
CacheBase.prototype.sadd = async function (key, members) {
|
|
2440
|
+
try {
|
|
2441
|
+
const ctx = {
|
|
2442
|
+
key,
|
|
2443
|
+
members: Array.isArray(members) ? members : [members],
|
|
2444
|
+
cancel: false
|
|
2445
|
+
};
|
|
2446
|
+
this.emit('sadd:before', ctx);
|
|
2447
|
+
|
|
2448
|
+
if (ctx.cancel) {
|
|
2449
|
+
return 0;
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
const {
|
|
2453
|
+
key: finalKey,
|
|
2454
|
+
members: finalMembers
|
|
2455
|
+
} = ctx;
|
|
2456
|
+
|
|
2457
|
+
// 检查是否过期
|
|
2458
|
+
if (this._isExpired(finalKey)) {
|
|
2459
|
+
await this.del(finalKey);
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
// 初始化集合
|
|
2463
|
+
if (!(finalKey in this._data) || this._types[finalKey] !== 'set') {
|
|
2464
|
+
this._data[finalKey] = new Set();
|
|
2465
|
+
this._types[finalKey] = 'set';
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
const set = this._data[finalKey];
|
|
2469
|
+
let addedCount = 0;
|
|
2470
|
+
for (const member of finalMembers) {
|
|
2471
|
+
const memberStr = String(member);
|
|
2472
|
+
if (!set.has(memberStr)) {
|
|
2473
|
+
set.add(memberStr);
|
|
2474
|
+
addedCount++;
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
this.emit('sadd:after', {
|
|
2479
|
+
key: finalKey,
|
|
2480
|
+
members: finalMembers,
|
|
2481
|
+
result: addedCount
|
|
2482
|
+
});
|
|
2483
|
+
return addedCount;
|
|
2484
|
+
} catch (error) {
|
|
2485
|
+
this.log('error', `添加集合成员失败 [${key}]:`, error);
|
|
2486
|
+
this.emit('sadd:error', { key, members, error });
|
|
2487
|
+
return 0;
|
|
2488
|
+
}
|
|
2489
|
+
};
|
|
2490
|
+
|
|
2491
|
+
/**
|
|
2492
|
+
* 从集合中移除成员
|
|
2493
|
+
* @param {String} key 键
|
|
2494
|
+
* @param {String|Array} members 成员或成员数组
|
|
2495
|
+
* @returns {Promise<Number>} 移除的成员数量
|
|
2496
|
+
*/
|
|
2497
|
+
CacheBase.prototype.srem = async function (key, members) {
|
|
2498
|
+
try {
|
|
2499
|
+
const ctx = {
|
|
2500
|
+
key,
|
|
2501
|
+
members: Array.isArray(members) ? members : [members],
|
|
2502
|
+
cancel: false
|
|
2503
|
+
};
|
|
2504
|
+
this.emit('srem:before', ctx);
|
|
2505
|
+
|
|
2506
|
+
if (ctx.cancel) {
|
|
2507
|
+
return 0;
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
const {
|
|
2511
|
+
key: finalKey,
|
|
2512
|
+
members: finalMembers
|
|
2513
|
+
} = ctx;
|
|
2514
|
+
|
|
2515
|
+
if (!(finalKey in this._data) || this._types[finalKey] !== 'set') {
|
|
2516
|
+
return 0;
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
const set = this._data[finalKey];
|
|
2520
|
+
let removedCount = 0;
|
|
2521
|
+
for (const member of finalMembers) {
|
|
2522
|
+
const memberStr = String(member);
|
|
2523
|
+
if (set.has(memberStr)) {
|
|
2524
|
+
set.delete(memberStr);
|
|
2525
|
+
removedCount++;
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
// 如果集合为空,删除整个键
|
|
2530
|
+
if (set.size === 0) {
|
|
2531
|
+
delete this._data[finalKey];
|
|
2532
|
+
delete this._types[finalKey];
|
|
2533
|
+
delete this._expires[finalKey];
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
this.emit('srem:after', {
|
|
2537
|
+
key: finalKey,
|
|
2538
|
+
members: finalMembers,
|
|
2539
|
+
result: removedCount
|
|
2540
|
+
});
|
|
2541
|
+
return removedCount;
|
|
2542
|
+
} catch (error) {
|
|
2543
|
+
this.log('error', `移除集合成员失败 [${key}]:`, error);
|
|
2544
|
+
this.emit('srem:error', { key, members, error });
|
|
2545
|
+
return 0;
|
|
2546
|
+
}
|
|
2547
|
+
};
|
|
2548
|
+
|
|
2549
|
+
/**
|
|
2550
|
+
* 检查成员是否在集合中
|
|
2551
|
+
* @param {String} key 键
|
|
2552
|
+
* @param {String} member 成员
|
|
2553
|
+
* @returns {Promise<Boolean>} 是否存在
|
|
2554
|
+
*/
|
|
2555
|
+
CacheBase.prototype.sismember = async function (key, member) {
|
|
2556
|
+
try {
|
|
2557
|
+
this.emit('sismember:before', { key, member });
|
|
2558
|
+
|
|
2559
|
+
if (!key || typeof key !== 'string') {
|
|
2560
|
+
throw new TypeError('key必须是非空字符串');
|
|
2561
|
+
}
|
|
2562
|
+
if (member === undefined || member === null) {
|
|
2563
|
+
throw new TypeError('member不能为undefined或null');
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
if (this._isExpired(key)) {
|
|
2567
|
+
await this.del(key);
|
|
2568
|
+
return false;
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
if (!(key in this._data) || this._types[key] !== 'set') {
|
|
2572
|
+
return false;
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
const memberStr = String(member);
|
|
2576
|
+
const result = this._data[key].has(memberStr);
|
|
2577
|
+
this.emit('sismember:after', { key, member, result });
|
|
2578
|
+
return result;
|
|
2579
|
+
} catch (error) {
|
|
2580
|
+
this.emit('sismember:error', { key, member, error });
|
|
2581
|
+
this.log('error', `检查集合成员失败 [${key}:${member}]:`, error);
|
|
2582
|
+
return false;
|
|
2583
|
+
}
|
|
2584
|
+
};
|
|
2585
|
+
|
|
2586
|
+
/**
|
|
2587
|
+
* 获取集合中的所有成员
|
|
2588
|
+
* @param {String} key 键
|
|
2589
|
+
* @returns {Promise<Array>} 成员数组
|
|
2590
|
+
*/
|
|
2591
|
+
CacheBase.prototype.smembers = async function (key) {
|
|
2592
|
+
try {
|
|
2593
|
+
this.emit('smembers:before', { key });
|
|
2594
|
+
|
|
2595
|
+
if (this._isExpired(key)) {
|
|
2596
|
+
await this.del(key);
|
|
2597
|
+
return [];
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
if (!(key in this._data) || this._types[key] !== 'set') {
|
|
2601
|
+
return [];
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
const result = Array.from(this._data[key]);
|
|
2605
|
+
this.emit('smembers:after', { key, result });
|
|
2606
|
+
return result;
|
|
2607
|
+
} catch (error) {
|
|
2608
|
+
this.log('error', `获取集合所有成员失败 [${key}]:`, error);
|
|
2609
|
+
this.emit('smembers:error', { key, error });
|
|
2610
|
+
return [];
|
|
2611
|
+
}
|
|
2612
|
+
};
|
|
2613
|
+
|
|
2614
|
+
/**
|
|
2615
|
+
* 获取集合中的成员数量
|
|
2616
|
+
* @param {String} key 键
|
|
2617
|
+
* @returns {Promise<Number>} 成员数量
|
|
2618
|
+
*/
|
|
2619
|
+
CacheBase.prototype.scard = async function (key) {
|
|
2620
|
+
try {
|
|
2621
|
+
if (this._isExpired(key)) {
|
|
2622
|
+
await this.del(key);
|
|
2623
|
+
return 0;
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
if (!(key in this._data) || this._types[key] !== 'set') {
|
|
2627
|
+
return 0;
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
return this._data[key].size;
|
|
2631
|
+
} catch (error) {
|
|
2632
|
+
this.log('error', `获取集合成员数量失败 [${key}]:`, error);
|
|
2633
|
+
return 0;
|
|
2634
|
+
}
|
|
2635
|
+
};
|
|
2636
|
+
|
|
2637
|
+
/**
|
|
2638
|
+
* 获取多个集合的交集
|
|
2639
|
+
* @param {...String} keys 键名列表
|
|
2640
|
+
* @returns {Promise<Array>} 交集成员数组
|
|
2641
|
+
*/
|
|
2642
|
+
CacheBase.prototype.sinter = async function (...keys) {
|
|
2643
|
+
try {
|
|
2644
|
+
if (keys.length === 0) {
|
|
2645
|
+
return [];
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
const sets = [];
|
|
2649
|
+
for (const key of keys) {
|
|
2650
|
+
if (this._isExpired(key)) {
|
|
2651
|
+
await this.del(key);
|
|
2652
|
+
return [];
|
|
2653
|
+
}
|
|
2654
|
+
if (!(key in this._data) || this._types[key] !== 'set') {
|
|
2655
|
+
return [];
|
|
2656
|
+
}
|
|
2657
|
+
sets.push(this._data[key]);
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
const result = Array.from(sets[0]).filter(member =>
|
|
2661
|
+
sets.every(set => set.has(member))
|
|
2662
|
+
);
|
|
2663
|
+
return result;
|
|
2664
|
+
} catch (error) {
|
|
2665
|
+
this.log('error', '获取集合交集失败:', error);
|
|
2666
|
+
return [];
|
|
2667
|
+
}
|
|
2668
|
+
};
|
|
2669
|
+
|
|
2670
|
+
/**
|
|
2671
|
+
* 获取多个集合的并集
|
|
2672
|
+
* @param {...String} keys 键名列表
|
|
2673
|
+
* @returns {Promise<Array>} 并集成员数组
|
|
2674
|
+
*/
|
|
2675
|
+
CacheBase.prototype.sunion = async function (...keys) {
|
|
2676
|
+
try {
|
|
2677
|
+
if (keys.length === 0) {
|
|
2678
|
+
return [];
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
const resultSet = new Set();
|
|
2682
|
+
for (const key of keys) {
|
|
2683
|
+
if (this._isExpired(key)) {
|
|
2684
|
+
await this.del(key);
|
|
2685
|
+
continue;
|
|
2686
|
+
}
|
|
2687
|
+
if (key in this._data && this._types[key] === 'set') {
|
|
2688
|
+
for (const member of this._data[key]) {
|
|
2689
|
+
resultSet.add(member);
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
return Array.from(resultSet);
|
|
2695
|
+
} catch (error) {
|
|
2696
|
+
this.log('error', '获取集合并集失败:', error);
|
|
2697
|
+
return [];
|
|
2698
|
+
}
|
|
2699
|
+
};
|
|
2700
|
+
|
|
2701
|
+
/**
|
|
2702
|
+
* 获取多个集合的差集(第一个集合减去其余集合)
|
|
2703
|
+
* @param {...String} keys 键名列表
|
|
2704
|
+
* @returns {Promise<Array>} 差集成员数组
|
|
2705
|
+
*/
|
|
2706
|
+
CacheBase.prototype.sdiff = async function (...keys) {
|
|
2707
|
+
try {
|
|
2708
|
+
if (keys.length === 0) {
|
|
2709
|
+
return [];
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
const firstKey = keys[0];
|
|
2713
|
+
if (this._isExpired(firstKey)) {
|
|
2714
|
+
await this.del(firstKey);
|
|
2715
|
+
return [];
|
|
2716
|
+
}
|
|
2717
|
+
if (!(firstKey in this._data) || this._types[firstKey] !== 'set') {
|
|
2718
|
+
return [];
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
const firstSet = this._data[firstKey];
|
|
2722
|
+
const otherSets = [];
|
|
2723
|
+
for (let i = 1; i < keys.length; i++) {
|
|
2724
|
+
const key = keys[i];
|
|
2725
|
+
if (this._isExpired(key)) {
|
|
2726
|
+
await this.del(key);
|
|
2727
|
+
continue;
|
|
2728
|
+
}
|
|
2729
|
+
if (key in this._data && this._types[key] === 'set') {
|
|
2730
|
+
otherSets.push(this._data[key]);
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
const result = Array.from(firstSet).filter(member =>
|
|
2735
|
+
!otherSets.some(set => set.has(member))
|
|
2736
|
+
);
|
|
2737
|
+
return result;
|
|
2738
|
+
} catch (error) {
|
|
2739
|
+
this.log('error', '获取集合差集失败:', error);
|
|
2740
|
+
return [];
|
|
2741
|
+
}
|
|
2742
|
+
};
|
|
2743
|
+
|
|
2744
|
+
// ==================== 有序集合操作 ====================
|
|
2745
|
+
|
|
2746
|
+
/**
|
|
2747
|
+
* 添加成员到有序集合
|
|
2748
|
+
* @param {String} key 键
|
|
2749
|
+
* @param {Number} score 分数
|
|
2750
|
+
* @param {String} member 成员
|
|
2751
|
+
* @returns {Promise<Boolean>} 是否成功(新成员返回true,更新返回false)
|
|
2752
|
+
*/
|
|
2753
|
+
CacheBase.prototype.zadd = async function (key, score, member) {
|
|
2754
|
+
try {
|
|
2755
|
+
const ctx = {
|
|
2756
|
+
key,
|
|
2757
|
+
score,
|
|
2758
|
+
member,
|
|
2759
|
+
cancel: false
|
|
2760
|
+
};
|
|
2761
|
+
this.emit('zadd:before', ctx);
|
|
2762
|
+
|
|
2763
|
+
if (ctx.cancel) {
|
|
2764
|
+
return false;
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
const {
|
|
2768
|
+
key: finalKey,
|
|
2769
|
+
score: finalScore,
|
|
2770
|
+
member: finalMember
|
|
2771
|
+
} = ctx;
|
|
2772
|
+
|
|
2773
|
+
// 检查是否过期
|
|
2774
|
+
if (this._isExpired(finalKey)) {
|
|
2775
|
+
await this.del(finalKey);
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
// 初始化有序集合
|
|
2779
|
+
if (!(finalKey in this._data) || this._types[finalKey] !== 'zset') {
|
|
2780
|
+
this._data[finalKey] = new Map();
|
|
2781
|
+
this._types[finalKey] = 'zset';
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
const map = this._data[finalKey];
|
|
2785
|
+
const memberStr = String(finalMember);
|
|
2786
|
+
const isNew = !map.has(memberStr);
|
|
2787
|
+
map.set(memberStr, Number(finalScore));
|
|
2788
|
+
|
|
2789
|
+
this.emit('zadd:after', {
|
|
2790
|
+
key: finalKey,
|
|
2791
|
+
score: finalScore,
|
|
2792
|
+
member: finalMember,
|
|
2793
|
+
success: isNew
|
|
2794
|
+
});
|
|
2795
|
+
return isNew;
|
|
2796
|
+
} catch (error) {
|
|
2797
|
+
this.log('error', `添加有序集合成员失败 [${key}]:`, error);
|
|
2798
|
+
this.emit('zadd:error', { key, score, member, error });
|
|
2799
|
+
return false;
|
|
2800
|
+
}
|
|
2801
|
+
};
|
|
2802
|
+
|
|
2803
|
+
/**
|
|
2804
|
+
* 从有序集合中移除成员
|
|
2805
|
+
* @param {String} key 键
|
|
2806
|
+
* @param {String|Array} members 成员或成员数组
|
|
2807
|
+
* @returns {Promise<Number>} 移除的成员数量
|
|
2808
|
+
*/
|
|
2809
|
+
CacheBase.prototype.zrem = async function (key, members) {
|
|
2810
|
+
try {
|
|
2811
|
+
const ctx = {
|
|
2812
|
+
key,
|
|
2813
|
+
members: Array.isArray(members) ? members : [members],
|
|
2814
|
+
cancel: false
|
|
2815
|
+
};
|
|
2816
|
+
this.emit('zrem:before', ctx);
|
|
2817
|
+
|
|
2818
|
+
if (ctx.cancel) {
|
|
2819
|
+
return 0;
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
const {
|
|
2823
|
+
key: finalKey,
|
|
2824
|
+
members: finalMembers
|
|
2825
|
+
} = ctx;
|
|
2826
|
+
|
|
2827
|
+
if (!(finalKey in this._data) || this._types[finalKey] !== 'zset') {
|
|
2828
|
+
return 0;
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2831
|
+
const map = this._data[finalKey];
|
|
2832
|
+
let removedCount = 0;
|
|
2833
|
+
for (const member of finalMembers) {
|
|
2834
|
+
const memberStr = String(member);
|
|
2835
|
+
if (map.has(memberStr)) {
|
|
2836
|
+
map.delete(memberStr);
|
|
2837
|
+
removedCount++;
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2841
|
+
// 如果有序集合为空,删除整个键
|
|
2842
|
+
if (map.size === 0) {
|
|
2843
|
+
delete this._data[finalKey];
|
|
2844
|
+
delete this._types[finalKey];
|
|
2845
|
+
delete this._expires[finalKey];
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
this.emit('zrem:after', {
|
|
2849
|
+
key: finalKey,
|
|
2850
|
+
members: finalMembers,
|
|
2851
|
+
result: removedCount
|
|
2852
|
+
});
|
|
2853
|
+
return removedCount;
|
|
2854
|
+
} catch (error) {
|
|
2855
|
+
this.log('error', `移除有序集合成员失败 [${key}]:`, error);
|
|
2856
|
+
this.emit('zrem:error', { key, members, error });
|
|
2857
|
+
return 0;
|
|
2858
|
+
}
|
|
2859
|
+
};
|
|
2860
|
+
|
|
2861
|
+
/**
|
|
2862
|
+
* 获取有序集合中成员的分数
|
|
2863
|
+
* @param {String} key 键
|
|
2864
|
+
* @param {String} member 成员
|
|
2865
|
+
* @returns {Promise<Number|null>} 分数,不存在返回null
|
|
2866
|
+
*/
|
|
2867
|
+
CacheBase.prototype.zscore = async function (key, member) {
|
|
2868
|
+
try {
|
|
2869
|
+
if (this._isExpired(key)) {
|
|
2870
|
+
await this.del(key);
|
|
2871
|
+
return null;
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
if (!(key in this._data) || this._types[key] !== 'zset') {
|
|
2875
|
+
return null;
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
const memberStr = String(member);
|
|
2879
|
+
const map = this._data[key];
|
|
2880
|
+
if (!map.has(memberStr)) {
|
|
2881
|
+
return null;
|
|
2882
|
+
}
|
|
2883
|
+
return map.get(memberStr);
|
|
2884
|
+
} catch (error) {
|
|
2885
|
+
this.log('error', `获取有序集合成员分数失败 [${key}:${member}]:`, error);
|
|
2886
|
+
return null;
|
|
2887
|
+
}
|
|
2888
|
+
};
|
|
2889
|
+
|
|
2890
|
+
/**
|
|
2891
|
+
* 获取有序集合中成员的排名(分数从小到大)
|
|
2892
|
+
* @param {String} key 键
|
|
2893
|
+
* @param {String} member 成员
|
|
2894
|
+
* @returns {Promise<Number|null>} 排名,不存在返回null
|
|
2895
|
+
*/
|
|
2896
|
+
CacheBase.prototype.zrank = async function (key, member) {
|
|
2897
|
+
try {
|
|
2898
|
+
if (this._isExpired(key)) {
|
|
2899
|
+
await this.del(key);
|
|
2900
|
+
return null;
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2903
|
+
if (!(key in this._data) || this._types[key] !== 'zset') {
|
|
2904
|
+
return null;
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
const memberStr = String(member);
|
|
2908
|
+
const map = this._data[key];
|
|
2909
|
+
if (!map.has(memberStr)) {
|
|
2910
|
+
return null;
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
// 按分数从小到大排序
|
|
2914
|
+
const sorted = Array.from(map.entries()).sort((a, b) => a[1] - b[1]);
|
|
2915
|
+
const rank = sorted.findIndex(([m]) => m === memberStr);
|
|
2916
|
+
return rank >= 0 ? rank : null;
|
|
2917
|
+
} catch (error) {
|
|
2918
|
+
this.log('error', `获取有序集合成员排名失败 [${key}:${member}]:`, error);
|
|
2919
|
+
return null;
|
|
2920
|
+
}
|
|
2921
|
+
};
|
|
2922
|
+
|
|
2923
|
+
/**
|
|
2924
|
+
* 获取有序集合中指定范围的成员
|
|
2925
|
+
* @param {String} key 键
|
|
2926
|
+
* @param {Number} start 起始索引
|
|
2927
|
+
* @param {Number} stop 结束索引
|
|
2928
|
+
* @param {Boolean} [withScores=false] 是否返回分数
|
|
2929
|
+
* @returns {Promise<Array>} 成员数组
|
|
2930
|
+
*/
|
|
2931
|
+
CacheBase.prototype.zrange = async function (key, start, stop, withScores = false) {
|
|
2932
|
+
try {
|
|
2933
|
+
if (this._isExpired(key)) {
|
|
2934
|
+
await this.del(key);
|
|
2935
|
+
return [];
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2938
|
+
if (!(key in this._data) || this._types[key] !== 'zset') {
|
|
2939
|
+
return [];
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2942
|
+
const map = this._data[key];
|
|
2943
|
+
// 按分数从小到大排序
|
|
2944
|
+
const sorted = Array.from(map.entries()).sort((a, b) => a[1] - b[1]);
|
|
2945
|
+
const len = sorted.length;
|
|
2946
|
+
|
|
2947
|
+
// 处理负索引
|
|
2948
|
+
let startIndex = start < 0 ? Math.max(0, len + start) : start;
|
|
2949
|
+
let endIndex = stop < 0 ? Math.max(0, len + stop + 1) : stop + 1;
|
|
2950
|
+
|
|
2951
|
+
if (startIndex >= len) {
|
|
2952
|
+
return [];
|
|
2953
|
+
}
|
|
2954
|
+
|
|
2955
|
+
const slice = sorted.slice(startIndex, endIndex);
|
|
2956
|
+
if (withScores) {
|
|
2957
|
+
return slice.map(([member, score]) => ({ member, score }));
|
|
2958
|
+
}
|
|
2959
|
+
return slice.map(([member]) => member);
|
|
2960
|
+
} catch (error) {
|
|
2961
|
+
this.log('error', `获取有序集合范围成员失败 [${key}]:`, error);
|
|
2962
|
+
return [];
|
|
2963
|
+
}
|
|
2964
|
+
};
|
|
2965
|
+
|
|
2966
|
+
/**
|
|
2967
|
+
* 获取有序集合中的成员数量
|
|
2968
|
+
* @param {String} key 键
|
|
2969
|
+
* @returns {Promise<Number>} 成员数量
|
|
2970
|
+
*/
|
|
2971
|
+
CacheBase.prototype.zcard = async function (key) {
|
|
2972
|
+
try {
|
|
2973
|
+
if (this._isExpired(key)) {
|
|
2974
|
+
await this.del(key);
|
|
2975
|
+
return 0;
|
|
2976
|
+
}
|
|
2977
|
+
|
|
2978
|
+
if (!(key in this._data) || this._types[key] !== 'zset') {
|
|
2979
|
+
return 0;
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
return this._data[key].size;
|
|
2983
|
+
} catch (error) {
|
|
2984
|
+
this.log('error', `获取有序集合成员数量失败 [${key}]:`, error);
|
|
2985
|
+
return 0;
|
|
2986
|
+
}
|
|
2987
|
+
};
|
|
2988
|
+
|
|
2989
|
+
/**
|
|
2990
|
+
* 增加有序集合成员的分数
|
|
2991
|
+
* @param {String} key 键
|
|
2992
|
+
* @param {Number} increment 增量
|
|
2993
|
+
* @param {String} member 成员
|
|
2994
|
+
* @returns {Promise<Number>} 增加后的分数
|
|
2995
|
+
*/
|
|
2996
|
+
CacheBase.prototype.zincrby = async function (key, increment, member) {
|
|
2997
|
+
try {
|
|
2998
|
+
// 检查是否过期
|
|
2999
|
+
if (this._isExpired(key)) {
|
|
3000
|
+
await this.del(key);
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
// 初始化有序集合
|
|
3004
|
+
if (!(key in this._data) || this._types[key] !== 'zset') {
|
|
3005
|
+
this._data[key] = new Map();
|
|
3006
|
+
this._types[key] = 'zset';
|
|
3007
|
+
}
|
|
3008
|
+
|
|
3009
|
+
const map = this._data[key];
|
|
3010
|
+
const memberStr = String(member);
|
|
3011
|
+
const currentScore = map.has(memberStr) ? map.get(memberStr) : 0;
|
|
3012
|
+
const newScore = currentScore + Number(increment);
|
|
3013
|
+
map.set(memberStr, newScore);
|
|
3014
|
+
|
|
3015
|
+
return newScore;
|
|
3016
|
+
} catch (error) {
|
|
3017
|
+
this.log('error', `增加有序集合成员分数失败 [${key}:${member}]:`, error);
|
|
3018
|
+
return null;
|
|
3019
|
+
}
|
|
3020
|
+
};
|
|
3021
|
+
|
|
3022
|
+
// ==================== 发布订阅 ====================
|
|
3023
|
+
|
|
3024
|
+
/**
|
|
3025
|
+
* 消息订阅
|
|
3026
|
+
* @param {String} channel 订阅的频道
|
|
3027
|
+
* @param {Function} func 操作回调函数
|
|
3028
|
+
* @returns {Promise<Boolean>} 订阅结果
|
|
3029
|
+
*/
|
|
3030
|
+
CacheBase.prototype.subscribe = async function (channel, func) {
|
|
3031
|
+
if (!channel || typeof channel !== 'string') {
|
|
3032
|
+
throw new TypeError('channel必须是非空字符串');
|
|
3033
|
+
}
|
|
3034
|
+
if (typeof func !== 'function') {
|
|
3035
|
+
throw new TypeError('func必须是函数');
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
try {
|
|
3039
|
+
this._channels[channel] = func;
|
|
3040
|
+
return true;
|
|
3041
|
+
} catch (err) {
|
|
3042
|
+
this.log('error', `订阅频道 ${channel} 失败:`, err);
|
|
3043
|
+
return false;
|
|
3044
|
+
}
|
|
3045
|
+
};
|
|
3046
|
+
|
|
3047
|
+
/**
|
|
3048
|
+
* 取消订阅
|
|
3049
|
+
* @param {String} channel 订阅的频道
|
|
3050
|
+
* @returns {Promise<Boolean>} 取消订阅结果
|
|
3051
|
+
*/
|
|
3052
|
+
CacheBase.prototype.unsubscribe = async function (channel) {
|
|
3053
|
+
if (!channel || typeof channel !== 'string') {
|
|
3054
|
+
throw new TypeError('channel必须是非空字符串');
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
try {
|
|
3058
|
+
delete this._channels[channel];
|
|
3059
|
+
return true;
|
|
3060
|
+
} catch (err) {
|
|
3061
|
+
this.log('error', `取消订阅频道 ${channel} 失败:`, err);
|
|
3062
|
+
return false;
|
|
3063
|
+
}
|
|
3064
|
+
};
|
|
3065
|
+
|
|
3066
|
+
/**
|
|
3067
|
+
* 发布消息
|
|
3068
|
+
* @param {String} channel 频道
|
|
3069
|
+
* @param {String} message 消息
|
|
3070
|
+
* @returns {Promise<Number>} 接收消息的订阅者数量
|
|
3071
|
+
*/
|
|
3072
|
+
CacheBase.prototype.publish = async function (channel, message) {
|
|
3073
|
+
if (!channel || typeof channel !== 'string') {
|
|
3074
|
+
throw new TypeError('channel必须是非空字符串');
|
|
3075
|
+
}
|
|
3076
|
+
if (message === undefined || message === null) {
|
|
3077
|
+
throw new TypeError('message不能为undefined或null');
|
|
3078
|
+
}
|
|
3079
|
+
|
|
3080
|
+
try {
|
|
3081
|
+
const func = this._channels[channel];
|
|
3082
|
+
if (func) {
|
|
3083
|
+
try {
|
|
3084
|
+
func(message);
|
|
3085
|
+
} catch (err) {
|
|
3086
|
+
this.log('error', `处理频道 ${channel} 的消息时出错:`, err);
|
|
3087
|
+
}
|
|
3088
|
+
return 1;
|
|
3089
|
+
}
|
|
3090
|
+
return 0;
|
|
3091
|
+
} catch (err) {
|
|
3092
|
+
this.log('error', `发布消息到频道 ${channel} 失败:`, err);
|
|
3093
|
+
return 0;
|
|
3094
|
+
}
|
|
3095
|
+
};
|
|
3096
|
+
|
|
3097
|
+
/**
|
|
3098
|
+
* 订阅消息处理, 如果没有订阅函数,则其他的订阅消息会进入该函数
|
|
3099
|
+
* @param {String} channel 频道
|
|
3100
|
+
* @param {String} message 消息
|
|
3101
|
+
*/
|
|
3102
|
+
CacheBase.prototype.main = async function (channel, message) {
|
|
3103
|
+
// 默认实现,可由子类重写
|
|
3104
|
+
this.log('debug', `收到未处理的消息 [${channel}]:`, message);
|
|
3105
|
+
};
|
|
3106
|
+
|
|
3107
|
+
/**
|
|
3108
|
+
* 消息通知
|
|
3109
|
+
* @param {String} channel 频道
|
|
3110
|
+
* @param {String} message 消息
|
|
3111
|
+
*/
|
|
3112
|
+
CacheBase.prototype.message = function (channel, message) {
|
|
3113
|
+
var func = this._channels[channel];
|
|
3114
|
+
if (func) {
|
|
3115
|
+
try {
|
|
3116
|
+
func(message);
|
|
3117
|
+
} catch (err) {
|
|
3118
|
+
this.log('error', `处理频道 ${channel} 的消息时出错:`, err);
|
|
3119
|
+
}
|
|
3120
|
+
} else {
|
|
3121
|
+
this.main(channel, message);
|
|
3122
|
+
}
|
|
3123
|
+
};
|
|
3124
|
+
|
|
3125
|
+
module.exports = CacheBase;
|