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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +274 -0
  3. package/SECURITY.md +160 -0
  4. package/index.js +90 -0
  5. package/package.json +68 -0
  6. package/src/cache/CacheManager.js +393 -0
  7. package/src/cache/MemoryAdapter.js +259 -0
  8. package/src/cache/MultiLevelCache.js +362 -0
  9. package/src/cache/RedisAdapter.js +329 -0
  10. package/src/cache/SQLiteAdapter.js +280 -0
  11. package/src/cache/index.js +15 -0
  12. package/src/client/Client.js +554 -0
  13. package/src/client/ShardingManager.js +274 -0
  14. package/src/config/ConfigValidator.js +172 -0
  15. package/src/config/index.js +7 -0
  16. package/src/dashboard/DashboardManager.js +362 -0
  17. package/src/datagen/DataGenerator.js +47 -0
  18. package/src/datagen/apis/GraphqlMocker.js +16 -0
  19. package/src/datagen/apis/OpenApiMocker.js +18 -0
  20. package/src/datagen/cli.js +130 -0
  21. package/src/datagen/formatters/csv.js +45 -0
  22. package/src/datagen/formatters/index.js +15 -0
  23. package/src/datagen/formatters/json.js +33 -0
  24. package/src/datagen/formatters/mongo.js +22 -0
  25. package/src/datagen/formatters/sql.js +32 -0
  26. package/src/datagen/index.js +17 -0
  27. package/src/datagen/plugin/PluginManager.js +43 -0
  28. package/src/datagen/plugins/example-plugin.js +21 -0
  29. package/src/datagen/schema/SchemaGenerator.js +172 -0
  30. package/src/datagen/schema/loadSchema.js +14 -0
  31. package/src/datagen/utils/seed.js +26 -0
  32. package/src/di/ServiceContainer.js +229 -0
  33. package/src/errors/ErrorCodes.js +110 -0
  34. package/src/errors/LumisError.js +85 -0
  35. package/src/game/EconomyManager.js +161 -0
  36. package/src/game/GameSessionManager.js +76 -0
  37. package/src/game/GuildManager.js +197 -0
  38. package/src/game/InventoryManager.js +140 -0
  39. package/src/game/LevelingSystem.js +194 -0
  40. package/src/game/MusicManager.js +587 -0
  41. package/src/health/HealthChecker.js +170 -0
  42. package/src/health/index.js +7 -0
  43. package/src/performance/PerformanceMonitor.js +244 -0
  44. package/src/performance/index.js +7 -0
  45. package/src/security/InputSanitizer.js +185 -0
  46. package/src/security/SecurityMiddleware.js +231 -0
  47. package/src/security/index.js +9 -0
  48. package/src/shutdown/GracefulShutdown.js +159 -0
  49. package/src/shutdown/index.js +7 -0
  50. 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;