lumisjs 1.0.0
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/LICENSE +21 -0
- package/README.md +274 -0
- package/SECURITY.md +160 -0
- package/index.js +90 -0
- package/package.json +68 -0
- package/src/cache/CacheManager.js +393 -0
- package/src/cache/MemoryAdapter.js +259 -0
- package/src/cache/MultiLevelCache.js +362 -0
- package/src/cache/RedisAdapter.js +329 -0
- package/src/cache/SQLiteAdapter.js +280 -0
- package/src/cache/index.js +15 -0
- package/src/client/Client.js +554 -0
- package/src/client/ShardingManager.js +274 -0
- package/src/config/ConfigValidator.js +172 -0
- package/src/config/index.js +7 -0
- package/src/dashboard/DashboardManager.js +362 -0
- package/src/datagen/DataGenerator.js +47 -0
- package/src/datagen/apis/GraphqlMocker.js +16 -0
- package/src/datagen/apis/OpenApiMocker.js +18 -0
- package/src/datagen/cli.js +130 -0
- package/src/datagen/formatters/csv.js +45 -0
- package/src/datagen/formatters/index.js +15 -0
- package/src/datagen/formatters/json.js +33 -0
- package/src/datagen/formatters/mongo.js +22 -0
- package/src/datagen/formatters/sql.js +32 -0
- package/src/datagen/index.js +17 -0
- package/src/datagen/plugin/PluginManager.js +43 -0
- package/src/datagen/plugins/example-plugin.js +21 -0
- package/src/datagen/schema/SchemaGenerator.js +172 -0
- package/src/datagen/schema/loadSchema.js +14 -0
- package/src/datagen/utils/seed.js +26 -0
- package/src/di/ServiceContainer.js +229 -0
- package/src/errors/ErrorCodes.js +110 -0
- package/src/errors/LumisError.js +85 -0
- package/src/game/EconomyManager.js +161 -0
- package/src/game/GameSessionManager.js +76 -0
- package/src/game/GuildManager.js +197 -0
- package/src/game/InventoryManager.js +140 -0
- package/src/game/LevelingSystem.js +194 -0
- package/src/game/MusicManager.js +587 -0
- package/src/health/HealthChecker.js +170 -0
- package/src/health/index.js +7 -0
- package/src/performance/PerformanceMonitor.js +244 -0
- package/src/performance/index.js +7 -0
- package/src/security/InputSanitizer.js +185 -0
- package/src/security/SecurityMiddleware.js +231 -0
- package/src/security/index.js +9 -0
- package/src/shutdown/GracefulShutdown.js +159 -0
- package/src/shutdown/index.js +7 -0
- package/src/utils/StructuredLogger.js +118 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { EventEmitter } = require('node:events');
|
|
4
|
+
const MemoryAdapter = require('./MemoryAdapter');
|
|
5
|
+
const Logger = require('../utils/Logger');
|
|
6
|
+
const { LumisError } = require('../errors/LumisError');
|
|
7
|
+
const { ErrorCodes } = require('../errors/ErrorCodes');
|
|
8
|
+
|
|
9
|
+
class CacheManager extends EventEmitter {
|
|
10
|
+
/**
|
|
11
|
+
* Unified cache interface that delegates to a pluggable adapter.
|
|
12
|
+
* Enhanced with metrics, monitoring, and advanced features.
|
|
13
|
+
*
|
|
14
|
+
* @param {object} [options]
|
|
15
|
+
* @param {'memory'|'redis'|'sqlite'|object} [options.adapter='memory']
|
|
16
|
+
* @param {object} [options.maxSize] Passed to MemoryAdapter
|
|
17
|
+
* @param {number} [options.ttl] Default TTL in seconds
|
|
18
|
+
* @param {boolean} [options.enableMetrics=true] Enable cache metrics
|
|
19
|
+
* @param {boolean} [options.enableEvents=true] Enable cache events
|
|
20
|
+
*/
|
|
21
|
+
constructor(options = {}) {
|
|
22
|
+
super();
|
|
23
|
+
this.options = options;
|
|
24
|
+
this.logger = new Logger({ prefix: 'Cache', level: 'info' });
|
|
25
|
+
|
|
26
|
+
const adapterName = options.adapter || 'memory';
|
|
27
|
+
this.adapter = this._createAdapter(adapterName, options);
|
|
28
|
+
this.logger.info(`Cache adapter: ${typeof adapterName === 'string' ? adapterName : 'custom'}`);
|
|
29
|
+
|
|
30
|
+
// Metrics tracking
|
|
31
|
+
this.enableMetrics = options.enableMetrics !== false;
|
|
32
|
+
this.metrics = {
|
|
33
|
+
hits: 0,
|
|
34
|
+
misses: 0,
|
|
35
|
+
sets: 0,
|
|
36
|
+
deletes: 0,
|
|
37
|
+
gets: 0,
|
|
38
|
+
evictions: 0,
|
|
39
|
+
startTime: Date.now(),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Event emission
|
|
43
|
+
this.enableEvents = options.enableEvents !== false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
_createAdapter(name, options) {
|
|
47
|
+
if (typeof name === 'object' && name !== null) {
|
|
48
|
+
// Custom adapter passed directly — validate the interface
|
|
49
|
+
const required = ['get', 'set', 'delete', 'has', 'clear', 'keys'];
|
|
50
|
+
const missing = required.filter((m) => typeof name[m] !== 'function');
|
|
51
|
+
if (missing.length) {
|
|
52
|
+
throw new LumisError(
|
|
53
|
+
ErrorCodes.CACHE_ADAPTER_ERROR,
|
|
54
|
+
`Custom adapter is missing methods: ${missing.join(', ')}`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return name;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
switch (name) {
|
|
61
|
+
case 'memory':
|
|
62
|
+
return new MemoryAdapter(options);
|
|
63
|
+
|
|
64
|
+
case 'redis': {
|
|
65
|
+
const RedisAdapter = require('./RedisAdapter');
|
|
66
|
+
return new RedisAdapter(options);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
case 'sqlite': {
|
|
70
|
+
const SQLiteAdapter = require('./SQLiteAdapter');
|
|
71
|
+
return new SQLiteAdapter(options);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
default:
|
|
75
|
+
throw new LumisError(ErrorCodes.CACHE_ADAPTER_NOT_FOUND, name);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Core API ──────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
async get(key) {
|
|
82
|
+
this._trackMetric('gets');
|
|
83
|
+
const value = await this.adapter.get(key);
|
|
84
|
+
|
|
85
|
+
if (this.enableMetrics) {
|
|
86
|
+
if (value !== undefined) {
|
|
87
|
+
this.metrics.hits++;
|
|
88
|
+
} else {
|
|
89
|
+
this.metrics.misses++;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (this.enableEvents) {
|
|
94
|
+
this.emit('get', { key, hit: value !== undefined });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async set(key, value, ttl) {
|
|
101
|
+
this._trackMetric('sets');
|
|
102
|
+
await this.adapter.set(key, value, ttl);
|
|
103
|
+
|
|
104
|
+
if (this.enableEvents) {
|
|
105
|
+
this.emit('set', { key, ttl });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async delete(key) {
|
|
110
|
+
this._trackMetric('deletes');
|
|
111
|
+
const result = await this.adapter.delete(key);
|
|
112
|
+
|
|
113
|
+
if (this.enableEvents) {
|
|
114
|
+
this.emit('delete', { key, existed: result });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async has(key) {
|
|
121
|
+
return this.adapter.has(key);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async clear() {
|
|
125
|
+
await this.adapter.clear();
|
|
126
|
+
|
|
127
|
+
if (this.enableEvents) {
|
|
128
|
+
this.emit('clear');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async size() {
|
|
133
|
+
return this.adapter.size();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async values() {
|
|
137
|
+
return this.adapter.values();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async keys() {
|
|
141
|
+
return this.adapter.keys();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async filter(fn) {
|
|
145
|
+
return this.adapter.filter(fn);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── Utilities ─────────────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Return existing value for key, or compute, store, and return defaultValue.
|
|
152
|
+
* @template T
|
|
153
|
+
* @param {string} key
|
|
154
|
+
* @param {T|(() => T|Promise<T>)} defaultValue
|
|
155
|
+
* @param {number} [ttl]
|
|
156
|
+
* @returns {Promise<T>}
|
|
157
|
+
*/
|
|
158
|
+
async ensure(key, defaultValue, ttl) {
|
|
159
|
+
const existing = await this.get(key);
|
|
160
|
+
if (existing !== undefined) return existing;
|
|
161
|
+
|
|
162
|
+
const value =
|
|
163
|
+
typeof defaultValue === 'function' ? await defaultValue() : defaultValue;
|
|
164
|
+
await this.set(key, value, ttl);
|
|
165
|
+
|
|
166
|
+
if (this.enableEvents) {
|
|
167
|
+
this.emit('ensure', { key, computed: true });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return value;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get the value and delete the key atomically (pop).
|
|
175
|
+
* @param {string} key
|
|
176
|
+
* @returns {Promise<any>}
|
|
177
|
+
*/
|
|
178
|
+
async take(key) {
|
|
179
|
+
const value = await this.get(key);
|
|
180
|
+
if (value !== undefined) {
|
|
181
|
+
await this.delete(key);
|
|
182
|
+
if (this.enableEvents) {
|
|
183
|
+
this.emit('take', { key });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return value;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get multiple keys at once.
|
|
191
|
+
* @param {string[]} keys
|
|
192
|
+
* @returns {Promise<Map<string, any>>}
|
|
193
|
+
*/
|
|
194
|
+
async getMany(keys) {
|
|
195
|
+
const result = new Map();
|
|
196
|
+
await Promise.all(
|
|
197
|
+
keys.map(async (key) => {
|
|
198
|
+
const val = await this.get(key);
|
|
199
|
+
if (val !== undefined) result.set(key, val);
|
|
200
|
+
})
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
if (this.enableEvents) {
|
|
204
|
+
this.emit('getMany', { keys, count: result.size });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Set multiple key/value pairs at once.
|
|
212
|
+
* @param {Array<[string, any, number?]>} entries - [key, value, ttl?]
|
|
213
|
+
* @returns {Promise<void>}
|
|
214
|
+
*/
|
|
215
|
+
async setMany(entries) {
|
|
216
|
+
await Promise.all(entries.map(([key, value, ttl]) => this.set(key, value, ttl)));
|
|
217
|
+
|
|
218
|
+
if (this.enableEvents) {
|
|
219
|
+
this.emit('setMany', { count: entries.length });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async destroy() {
|
|
224
|
+
if (typeof this.adapter.destroy === 'function') {
|
|
225
|
+
await this.adapter.destroy();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (this.enableEvents) {
|
|
229
|
+
this.emit('destroy');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
this.removeAllListeners();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ── Metrics & Monitoring ────────────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Get cache metrics.
|
|
239
|
+
* @returns {object}
|
|
240
|
+
*/
|
|
241
|
+
getMetrics() {
|
|
242
|
+
if (!this.enableMetrics) return null;
|
|
243
|
+
|
|
244
|
+
const uptime = Date.now() - this.metrics.startTime;
|
|
245
|
+
const totalRequests = this.metrics.hits + this.metrics.misses;
|
|
246
|
+
const hitRate = totalRequests > 0 ? (this.metrics.hits / totalRequests) * 100 : 0;
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
...this.metrics,
|
|
250
|
+
uptime,
|
|
251
|
+
totalRequests,
|
|
252
|
+
hitRate: hitRate.toFixed(2) + '%',
|
|
253
|
+
missRate: (100 - hitRate).toFixed(2) + '%',
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Reset cache metrics.
|
|
259
|
+
*/
|
|
260
|
+
resetMetrics() {
|
|
261
|
+
this.metrics = {
|
|
262
|
+
hits: 0,
|
|
263
|
+
misses: 0,
|
|
264
|
+
sets: 0,
|
|
265
|
+
deletes: 0,
|
|
266
|
+
gets: 0,
|
|
267
|
+
evictions: 0,
|
|
268
|
+
startTime: Date.now(),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Get cache statistics summary.
|
|
274
|
+
* @returns {Promise<object>}
|
|
275
|
+
*/
|
|
276
|
+
async getStats() {
|
|
277
|
+
const size = await this.size();
|
|
278
|
+
const metrics = this.getMetrics();
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
size,
|
|
282
|
+
adapter: this.adapter.constructor.name,
|
|
283
|
+
metrics,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ── Advanced Features ───────────────────────────────────────────────────────
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Execute multiple operations atomically (transaction-like).
|
|
291
|
+
* @param {Function} fn - Function that receives cache instance
|
|
292
|
+
* @returns {Promise<any>}
|
|
293
|
+
*/
|
|
294
|
+
async transaction(fn) {
|
|
295
|
+
// For memory adapter, this is synchronous
|
|
296
|
+
// For distributed adapters, this would need proper transaction support
|
|
297
|
+
return fn(this);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Invalidate keys matching a pattern.
|
|
302
|
+
* @param {string} pattern - Glob pattern (e.g., 'user:*')
|
|
303
|
+
* @returns {Promise<number>} Number of deleted keys
|
|
304
|
+
*/
|
|
305
|
+
async invalidatePattern(pattern) {
|
|
306
|
+
const allKeys = await this.keys();
|
|
307
|
+
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*').replace(/\?/g, '.') + '$');
|
|
308
|
+
const keysToDelete = allKeys.filter(key => regex.test(key));
|
|
309
|
+
|
|
310
|
+
await Promise.all(keysToDelete.map(key => this.delete(key)));
|
|
311
|
+
|
|
312
|
+
if (this.enableEvents) {
|
|
313
|
+
this.emit('invalidatePattern', { pattern, count: keysToDelete.length });
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return keysToDelete.length;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Warm up cache with preloaded data.
|
|
321
|
+
* @param {Map<string, any>} data - Data to preload
|
|
322
|
+
* @param {number} [ttl] - Default TTL for all entries
|
|
323
|
+
* @returns {Promise<void>}
|
|
324
|
+
*/
|
|
325
|
+
async warmup(data, ttl) {
|
|
326
|
+
const entries = Array.from(data.entries()).map(([key, value]) => [key, value, ttl]);
|
|
327
|
+
await this.setMany(entries);
|
|
328
|
+
|
|
329
|
+
if (this.enableEvents) {
|
|
330
|
+
this.emit('warmup', { count: entries.length });
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Get or set with sliding expiration.
|
|
336
|
+
* Extends TTL on each access.
|
|
337
|
+
* @param {string} key
|
|
338
|
+
* @param {Function} factory - Function to generate value if missing
|
|
339
|
+
* @param {number} ttl - TTL in seconds
|
|
340
|
+
* @returns {Promise<any>}
|
|
341
|
+
*/
|
|
342
|
+
async getOrSet(key, factory, ttl) {
|
|
343
|
+
const value = await this.get(key);
|
|
344
|
+
if (value !== undefined) {
|
|
345
|
+
// Extend TTL on hit
|
|
346
|
+
await this.set(key, value, ttl);
|
|
347
|
+
return value;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const newValue = await factory();
|
|
351
|
+
await this.set(key, newValue, ttl);
|
|
352
|
+
return newValue;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Backup cache data to object.
|
|
357
|
+
* @returns {Promise<object>}
|
|
358
|
+
*/
|
|
359
|
+
async backup() {
|
|
360
|
+
const keys = await this.keys();
|
|
361
|
+
const backup = {};
|
|
362
|
+
|
|
363
|
+
for (const key of keys) {
|
|
364
|
+
backup[key] = await this.get(key);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return backup;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Restore cache data from backup object.
|
|
372
|
+
* @param {object} backup - Backup data
|
|
373
|
+
* @param {number} [ttl] - TTL for restored entries
|
|
374
|
+
* @returns {Promise<void>}
|
|
375
|
+
*/
|
|
376
|
+
async restore(backup, ttl) {
|
|
377
|
+
const entries = Object.entries(backup).map(([key, value]) => [key, value, ttl]);
|
|
378
|
+
await this.setMany(entries);
|
|
379
|
+
|
|
380
|
+
if (this.enableEvents) {
|
|
381
|
+
this.emit('restore', { count: entries.length });
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** @private */
|
|
386
|
+
_trackMetric(metric) {
|
|
387
|
+
if (this.enableMetrics) {
|
|
388
|
+
this.metrics[metric]++;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
module.exports = CacheManager;
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
class MemoryAdapter {
|
|
4
|
+
/**
|
|
5
|
+
* LRU-capable in-memory cache adapter with TTL support.
|
|
6
|
+
* @param {object} [options]
|
|
7
|
+
* @param {number} [options.maxSize=0] Max entries; 0 = unlimited
|
|
8
|
+
* @param {number} [options.ttl=0] Default TTL in seconds; 0 = no expiry
|
|
9
|
+
*/
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this._store = new Map();
|
|
12
|
+
this.maxSize = options.maxSize ?? 0;
|
|
13
|
+
this.defaultTTL = options.ttl ?? 0;
|
|
14
|
+
// LRU order: leftmost = oldest, rightmost = newest
|
|
15
|
+
this._accessOrder = [];
|
|
16
|
+
// Periodic cleanup interval (every 5 min by default)
|
|
17
|
+
this._cleanupInterval = null;
|
|
18
|
+
const cleanupMs = (options.cleanupIntervalSec ?? 300) * 1000;
|
|
19
|
+
if (cleanupMs > 0) {
|
|
20
|
+
this._cleanupInterval = setInterval(() => this._cleanup(), cleanupMs);
|
|
21
|
+
if (this._cleanupInterval.unref) this._cleanupInterval.unref();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async get(key) {
|
|
26
|
+
const entry = this._store.get(key);
|
|
27
|
+
if (!entry) {
|
|
28
|
+
this._missCount++;
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (entry.expiresAt !== null && Date.now() > entry.expiresAt) {
|
|
33
|
+
this._store.delete(key);
|
|
34
|
+
this._removeFromAccessOrder(key);
|
|
35
|
+
this._missCount++;
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this._hitCount++;
|
|
40
|
+
this._touchAccessOrder(key);
|
|
41
|
+
return entry.value;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async set(key, value, ttl) {
|
|
45
|
+
if (this.maxSize > 0 && !this._store.has(key) && this._store.size >= this.maxSize) {
|
|
46
|
+
this._evict();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const effectiveTTL = ttl ?? this.defaultTTL;
|
|
50
|
+
const expiresAt = effectiveTTL > 0 ? Date.now() + effectiveTTL * 1000 : null;
|
|
51
|
+
|
|
52
|
+
this._store.set(key, { value, expiresAt, createdAt: Date.now() });
|
|
53
|
+
this._touchAccessOrder(key);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async delete(key) {
|
|
57
|
+
this._removeFromAccessOrder(key);
|
|
58
|
+
return this._store.delete(key);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async has(key) {
|
|
62
|
+
const value = await this.get(key);
|
|
63
|
+
return value !== undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async clear() {
|
|
67
|
+
this._store.clear();
|
|
68
|
+
this._accessOrder = [];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async size() {
|
|
72
|
+
await this._cleanup();
|
|
73
|
+
return this._store.size;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async values() {
|
|
77
|
+
await this._cleanup();
|
|
78
|
+
return [...this._store.values()].map((e) => e.value);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async keys() {
|
|
82
|
+
await this._cleanup();
|
|
83
|
+
return [...this._store.keys()];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async filter(fn) {
|
|
87
|
+
await this._cleanup();
|
|
88
|
+
const results = [];
|
|
89
|
+
for (const [key, entry] of this._store) {
|
|
90
|
+
if (fn(entry.value, key)) results.push(entry.value);
|
|
91
|
+
}
|
|
92
|
+
return results;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async forEach(fn) {
|
|
96
|
+
await this._cleanup();
|
|
97
|
+
for (const [key, entry] of this._store) {
|
|
98
|
+
fn(entry.value, key);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── Advanced Memory Features ────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get multiple keys at once.
|
|
106
|
+
* @param {string[]} keys
|
|
107
|
+
* @returns {Promise<Map<string, any>>}
|
|
108
|
+
*/
|
|
109
|
+
async getMany(keys) {
|
|
110
|
+
const result = new Map();
|
|
111
|
+
for (const key of keys) {
|
|
112
|
+
const value = await this.get(key);
|
|
113
|
+
if (value !== undefined) result.set(key, value);
|
|
114
|
+
}
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Set multiple keys at once.
|
|
120
|
+
* @param {Array<[string, any, number?]>} entries
|
|
121
|
+
* @returns {Promise<void>}
|
|
122
|
+
*/
|
|
123
|
+
async setMany(entries) {
|
|
124
|
+
for (const [key, value, ttl] of entries) {
|
|
125
|
+
await this.set(key, value, ttl);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get cache statistics.
|
|
131
|
+
* @returns {object}
|
|
132
|
+
*/
|
|
133
|
+
getStats() {
|
|
134
|
+
const now = Date.now();
|
|
135
|
+
let expiredCount = 0;
|
|
136
|
+
let totalSize = 0;
|
|
137
|
+
|
|
138
|
+
for (const [key, entry] of this._store) {
|
|
139
|
+
if (entry.expiresAt !== null && now > entry.expiresAt) {
|
|
140
|
+
expiredCount++;
|
|
141
|
+
}
|
|
142
|
+
totalSize += JSON.stringify(entry.value).length;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
size: this._store.size,
|
|
147
|
+
maxSize: this.maxSize,
|
|
148
|
+
expiredCount,
|
|
149
|
+
memoryUsage: totalSize,
|
|
150
|
+
hitRate: this._calculateHitRate(),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get keys sorted by access time (LRU order).
|
|
156
|
+
* @param {number} [limit]
|
|
157
|
+
* @returns {string[]}
|
|
158
|
+
*/
|
|
159
|
+
getLRUKeys(limit) {
|
|
160
|
+
const keys = limit ? this._accessOrder.slice(-limit) : [...this._accessOrder];
|
|
161
|
+
return keys.reverse();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Peek at a value without updating access time.
|
|
166
|
+
* @param {string} key
|
|
167
|
+
* @returns {any}
|
|
168
|
+
*/
|
|
169
|
+
async peek(key) {
|
|
170
|
+
const entry = this._store.get(key);
|
|
171
|
+
if (!entry) return undefined;
|
|
172
|
+
|
|
173
|
+
if (entry.expiresAt !== null && Date.now() > entry.expiresAt) {
|
|
174
|
+
this._store.delete(key);
|
|
175
|
+
this._removeFromAccessOrder(key);
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return entry.value;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get time until expiration for a key.
|
|
184
|
+
* @param {string} key
|
|
185
|
+
* @returns {number|null} TTL in milliseconds, or null if no expiry
|
|
186
|
+
*/
|
|
187
|
+
async getTTL(key) {
|
|
188
|
+
const entry = this._store.get(key);
|
|
189
|
+
if (!entry || entry.expiresAt === null) return null;
|
|
190
|
+
|
|
191
|
+
const remaining = entry.expiresAt - Date.now();
|
|
192
|
+
return remaining > 0 ? remaining : 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Set TTL on existing key.
|
|
197
|
+
* @param {string} key
|
|
198
|
+
* @param {number} ttl TTL in seconds
|
|
199
|
+
* @returns {boolean}
|
|
200
|
+
*/
|
|
201
|
+
async setTTL(key, ttl) {
|
|
202
|
+
const entry = this._store.get(key);
|
|
203
|
+
if (!entry) return false;
|
|
204
|
+
|
|
205
|
+
entry.expiresAt = ttl > 0 ? Date.now() + ttl * 1000 : null;
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** @private */
|
|
210
|
+
_hitCount = 0;
|
|
211
|
+
/** @private */
|
|
212
|
+
_missCount = 0;
|
|
213
|
+
|
|
214
|
+
_calculateHitRate() {
|
|
215
|
+
const total = this._hitCount + this._missCount;
|
|
216
|
+
return total > 0 ? (this._hitCount / total) * 100 : 0;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Evict the least-recently-used entry. */
|
|
220
|
+
_evict() {
|
|
221
|
+
if (this._accessOrder.length === 0) return;
|
|
222
|
+
const oldestKey = this._accessOrder.shift();
|
|
223
|
+
this._store.delete(oldestKey);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
_touchAccessOrder(key) {
|
|
227
|
+
const idx = this._accessOrder.indexOf(key);
|
|
228
|
+
if (idx !== -1) this._accessOrder.splice(idx, 1);
|
|
229
|
+
this._accessOrder.push(key);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
_removeFromAccessOrder(key) {
|
|
233
|
+
const idx = this._accessOrder.indexOf(key);
|
|
234
|
+
if (idx !== -1) this._accessOrder.splice(idx, 1);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async _cleanup() {
|
|
238
|
+
const now = Date.now();
|
|
239
|
+
for (const [key, entry] of this._store) {
|
|
240
|
+
if (entry.expiresAt !== null && now > entry.expiresAt) {
|
|
241
|
+
this._store.delete(key);
|
|
242
|
+
this._removeFromAccessOrder(key);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async destroy() {
|
|
248
|
+
if (this._cleanupInterval) {
|
|
249
|
+
clearInterval(this._cleanupInterval);
|
|
250
|
+
this._cleanupInterval = null;
|
|
251
|
+
}
|
|
252
|
+
this._store.clear();
|
|
253
|
+
this._accessOrder = [];
|
|
254
|
+
this._hitCount = 0;
|
|
255
|
+
this._missCount = 0;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
module.exports = MemoryAdapter;
|