@lara-node/cache 0.1.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/dist/index.cjs ADDED
@@ -0,0 +1,796 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+ //#endregion
24
+ let fs = require("fs");
25
+ fs = __toESM(fs, 1);
26
+ let path = require("path");
27
+ path = __toESM(path, 1);
28
+ let url = require("url");
29
+ let crypto = require("crypto");
30
+ crypto = __toESM(crypto, 1);
31
+ let _lara_node_db = require("@lara-node/db");
32
+ let _lara_node_core = require("@lara-node/core");
33
+ //#region src/Models/Cache.ts
34
+ var Cache$1 = class extends _lara_node_db.Model {
35
+ static {
36
+ this.table = "cache_store";
37
+ }
38
+ static {
39
+ this.primaryKey = "k";
40
+ }
41
+ static {
42
+ this.autoIncrement = false;
43
+ }
44
+ static {
45
+ this.fillable = [
46
+ "k",
47
+ "v",
48
+ "expires_at"
49
+ ];
50
+ }
51
+ static {
52
+ this.hidden = [];
53
+ }
54
+ constructor(attributes = {}) {
55
+ super(attributes);
56
+ }
57
+ };
58
+ //#endregion
59
+ //#region src/RateLimiter.ts
60
+ /**
61
+ * Rate Limiter - Laravel-style rate limiting using cache drivers
62
+ *
63
+ * Supports multiple algorithms:
64
+ * - Fixed Window: Simple counter reset after window expires
65
+ * - Sliding Window: More accurate rate limiting using timestamps
66
+ * - Token Bucket: Gradual replenishment of tokens
67
+ *
68
+ * Usage:
69
+ * import { RateLimiter } from '@/cache/RateLimiter';
70
+ *
71
+ * // Check if too many attempts
72
+ * const limiter = new RateLimiter();
73
+ * if (await limiter.tooManyAttempts('login:user@example.com', 5, 60)) {
74
+ * const retryAfter = await limiter.availableIn('login:user@example.com', 60);
75
+ * throw new Error(`Too many attempts. Retry after ${retryAfter} seconds.`);
76
+ * }
77
+ * await limiter.hit('login:user@example.com', 60);
78
+ */
79
+ /**
80
+ * Rate Limiter class using cache backend
81
+ */
82
+ var RateLimiter = class {
83
+ constructor(config = {}) {
84
+ this.prefix = config.prefix ?? "rate_limiter:";
85
+ this.defaultMaxAttempts = config.maxAttempts ?? 60;
86
+ this.defaultDecaySeconds = config.decaySeconds ?? 60;
87
+ }
88
+ /**
89
+ * Get the cache key for a given key
90
+ */
91
+ cacheKey(key) {
92
+ return `${this.prefix}${key}`;
93
+ }
94
+ /**
95
+ * Get the timer cache key for a given key (stores the reset timestamp)
96
+ */
97
+ timerKey(key) {
98
+ return `${this.prefix}${key}:timer`;
99
+ }
100
+ /**
101
+ * Determine if the given key has been "accessed" too many times
102
+ */
103
+ async tooManyAttempts(key, maxAttempts, _decaySeconds) {
104
+ const max = maxAttempts ?? this.defaultMaxAttempts;
105
+ if (await this.attempts(key) >= max) {
106
+ if (await Cache.has(this.timerKey(key))) return true;
107
+ await this.resetAttempts(key);
108
+ }
109
+ return false;
110
+ }
111
+ /**
112
+ * Increment the counter for a given key
113
+ * Returns the new number of attempts
114
+ */
115
+ async hit(key, decaySeconds) {
116
+ const decay = decaySeconds ?? this.defaultDecaySeconds;
117
+ const cKey = this.cacheKey(key);
118
+ const tKey = this.timerKey(key);
119
+ const newAttempts = await this.attempts(key) + 1;
120
+ if (!await Cache.has(tKey)) {
121
+ const expiresAt = Math.floor(Date.now() / 1e3) + decay;
122
+ await Cache.set(tKey, expiresAt, decay);
123
+ await Cache.set(cKey, newAttempts, decay);
124
+ } else {
125
+ const expiresAt = await Cache.get(tKey);
126
+ const remainingTtl = expiresAt ? Math.max(1, expiresAt - Math.floor(Date.now() / 1e3)) : decay;
127
+ await Cache.set(cKey, newAttempts, remainingTtl);
128
+ }
129
+ return newAttempts;
130
+ }
131
+ /**
132
+ * Get the number of attempts for the given key
133
+ */
134
+ async attempts(key) {
135
+ const val = await Cache.get(this.cacheKey(key));
136
+ return typeof val === "number" ? val : parseInt(val, 10) || 0;
137
+ }
138
+ /**
139
+ * Reset the number of attempts for the given key
140
+ */
141
+ async resetAttempts(key) {
142
+ await Cache.del(this.cacheKey(key));
143
+ await Cache.del(this.timerKey(key));
144
+ return true;
145
+ }
146
+ /**
147
+ * Get the number of retries remaining
148
+ */
149
+ async retriesLeft(key, maxAttempts) {
150
+ const max = maxAttempts ?? this.defaultMaxAttempts;
151
+ const attempts = await this.attempts(key);
152
+ return Math.max(0, max - attempts);
153
+ }
154
+ /**
155
+ * Clear the hits and lockout timer for the given key
156
+ */
157
+ async clear(key) {
158
+ await this.resetAttempts(key);
159
+ }
160
+ /**
161
+ * Get the number of seconds until the key is accessible again
162
+ */
163
+ async availableIn(key, _decaySeconds) {
164
+ const expiresAt = await Cache.get(this.timerKey(key));
165
+ if (!expiresAt) return 0;
166
+ const now = Math.floor(Date.now() / 1e3);
167
+ return Math.max(0, expiresAt - now);
168
+ }
169
+ /**
170
+ * Get the timestamp when the key becomes available again
171
+ */
172
+ async availableAt(key) {
173
+ return await Cache.get(this.timerKey(key)) || Math.floor(Date.now() / 1e3);
174
+ }
175
+ /**
176
+ * Attempt to execute a callback if the rate limit allows
177
+ * Returns the callback result or throws if rate limited
178
+ */
179
+ async attempt(key, maxAttempts, callback, decaySeconds) {
180
+ const decay = decaySeconds ?? this.defaultDecaySeconds;
181
+ if (await this.tooManyAttempts(key, maxAttempts, decay)) {
182
+ const retryAfter = await this.availableIn(key, decay);
183
+ throw new RateLimitExceededException(`Too many attempts. Please retry after ${retryAfter} seconds.`, retryAfter, maxAttempts);
184
+ }
185
+ await this.hit(key, decay);
186
+ return callback();
187
+ }
188
+ /**
189
+ * Get complete rate limit info for a key
190
+ */
191
+ async getInfo(key, maxAttempts, decaySeconds) {
192
+ const max = maxAttempts ?? this.defaultMaxAttempts;
193
+ const decay = decaySeconds ?? this.defaultDecaySeconds;
194
+ const attempts = await this.attempts(key);
195
+ const remaining = Math.max(0, max - attempts);
196
+ const retryAfter = await this.availableIn(key, decay);
197
+ const resetsAt = await this.availableAt(key);
198
+ return {
199
+ limited: attempts >= max && retryAfter > 0,
200
+ attempts,
201
+ maxAttempts: max,
202
+ remaining,
203
+ retryAfter,
204
+ resetsAt
205
+ };
206
+ }
207
+ /**
208
+ * Execute a callback with rate limiting, using a limiter definition
209
+ */
210
+ async limiter(name, key, maxAttempts, decaySeconds, callback) {
211
+ const fullKey = `${name}:${key}`;
212
+ return this.attempt(fullKey, maxAttempts, callback, decaySeconds);
213
+ }
214
+ };
215
+ /**
216
+ * Exception thrown when rate limit is exceeded
217
+ */
218
+ var RateLimitExceededException = class extends Error {
219
+ constructor(message, retryAfter, maxAttempts) {
220
+ super(message);
221
+ this.statusCode = 429;
222
+ this.name = "RateLimitExceededException";
223
+ this.retryAfter = retryAfter;
224
+ this.maxAttempts = maxAttempts;
225
+ }
226
+ };
227
+ const namedLimiters = /* @__PURE__ */ new Map();
228
+ /**
229
+ * Register a named rate limiter
230
+ */
231
+ function defineRateLimiter(name, config) {
232
+ namedLimiters.set(name, config);
233
+ }
234
+ /**
235
+ * Get a named rate limiter configuration
236
+ */
237
+ function getNamedLimiter(name) {
238
+ const limiter = namedLimiters.get(name);
239
+ return limiter ? limiter() : null;
240
+ }
241
+ const rateLimiter = new RateLimiter();
242
+ const RateLimiterFacade = {
243
+ /** Check if too many attempts have been made */
244
+ tooManyAttempts: (key, maxAttempts, decaySeconds) => rateLimiter.tooManyAttempts(key, maxAttempts, decaySeconds),
245
+ /** Increment the attempt counter */
246
+ hit: (key, decaySeconds) => rateLimiter.hit(key, decaySeconds),
247
+ /** Get current number of attempts */
248
+ attempts: (key) => rateLimiter.attempts(key),
249
+ /** Reset attempt counter */
250
+ resetAttempts: (key) => rateLimiter.resetAttempts(key),
251
+ /** Get remaining retries */
252
+ retriesLeft: (key, maxAttempts) => rateLimiter.retriesLeft(key, maxAttempts),
253
+ /** Clear rate limiter for key */
254
+ clear: (key) => rateLimiter.clear(key),
255
+ /** Get seconds until rate limit resets */
256
+ availableIn: (key, decaySeconds) => rateLimiter.availableIn(key, decaySeconds),
257
+ /** Get timestamp when rate limit resets */
258
+ availableAt: (key) => rateLimiter.availableAt(key),
259
+ /** Attempt to execute callback with rate limiting */
260
+ attempt: (key, maxAttempts, callback, decaySeconds) => rateLimiter.attempt(key, maxAttempts, callback, decaySeconds),
261
+ /** Get rate limit info for a key */
262
+ getInfo: (key, maxAttempts, decaySeconds) => rateLimiter.getInfo(key, maxAttempts, decaySeconds),
263
+ /** Define a named rate limiter */
264
+ define: defineRateLimiter,
265
+ /** Get named limiter config */
266
+ limiter: getNamedLimiter,
267
+ /** Execute with named limiter */
268
+ for: async (name, key, callback) => {
269
+ const config = getNamedLimiter(name);
270
+ if (!config) throw new Error(`Rate limiter [${name}] is not defined.`);
271
+ return rateLimiter.attempt(`${name}:${key}`, config.maxAttempts, callback, config.decaySeconds);
272
+ }
273
+ };
274
+ //#endregion
275
+ //#region src/CacheManager.ts
276
+ const __dirname$1 = path.default.dirname((0, url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
277
+ let _cacheWatchHook = null;
278
+ function setCacheWatchHook(hook) {
279
+ _cacheWatchHook = hook;
280
+ }
281
+ const CacheWatcher = { record: (e) => {
282
+ if (_cacheWatchHook) try {
283
+ _cacheWatchHook(e);
284
+ } catch {}
285
+ } };
286
+ const APP_KEY = process.env.APP_KEY || "";
287
+ const CIPHER = "aes-256-cbc";
288
+ let ENCRYPTION_ENABLED = false;
289
+ let ENCRYPTION_KEY = null;
290
+ function deriveKeyFromAppKey(appKey) {
291
+ if (!appKey) return Buffer.alloc(0);
292
+ if (appKey.startsWith("base64:")) {
293
+ const b = appKey.slice(7);
294
+ return Buffer.from(b, "base64");
295
+ }
296
+ const buf = Buffer.from(appKey, "utf8");
297
+ if (buf.length === 32) return buf;
298
+ return crypto.default.createHash("sha256").update(buf).digest();
299
+ }
300
+ if (APP_KEY) try {
301
+ ENCRYPTION_KEY = deriveKeyFromAppKey(APP_KEY);
302
+ if (ENCRYPTION_KEY.length !== 32) {
303
+ console.warn("APP_KEY provided but did not yield 32 bytes; derived key length:", ENCRYPTION_KEY.length, "— disabling encryption");
304
+ ENCRYPTION_ENABLED = false;
305
+ } else ENCRYPTION_ENABLED = true;
306
+ } catch (e) {
307
+ console.warn("Failed to initialize cache encryption, proceeding without encryption:", e);
308
+ ENCRYPTION_ENABLED = false;
309
+ }
310
+ else console.warn("No APP_KEY set; cache encryption is disabled. Set APP_KEY in your .env to enable encryption of cached values.");
311
+ function hmacFor(ivB64, valueB64) {
312
+ if (!ENCRYPTION_KEY) return "";
313
+ return crypto.default.createHmac("sha256", ENCRYPTION_KEY).update(ivB64 + "|" + valueB64).digest("hex");
314
+ }
315
+ function encryptRaw(plain) {
316
+ if (!ENCRYPTION_ENABLED || !ENCRYPTION_KEY || ENCRYPTION_KEY.length !== 32) return plain;
317
+ const iv = crypto.default.randomBytes(16);
318
+ const cipher = crypto.default.createCipheriv(CIPHER, ENCRYPTION_KEY, iv);
319
+ const encrypted = Buffer.concat([cipher.update(Buffer.from(plain, "utf8")), cipher.final()]);
320
+ const ivB = iv.toString("base64");
321
+ const valB = encrypted.toString("base64");
322
+ const payload = {
323
+ iv: ivB,
324
+ value: valB,
325
+ mac: hmacFor(ivB, valB)
326
+ };
327
+ return JSON.stringify(payload);
328
+ }
329
+ function decryptRaw(payloadStr) {
330
+ if (!ENCRYPTION_ENABLED || !ENCRYPTION_KEY || ENCRYPTION_KEY.length !== 32) return payloadStr;
331
+ let payload;
332
+ try {
333
+ payload = JSON.parse(payloadStr);
334
+ } catch (e) {
335
+ return null;
336
+ }
337
+ if (!payload || !payload.iv || !payload.value || !payload.mac) return null;
338
+ const expected = hmacFor(payload.iv, payload.value);
339
+ if (!crypto.default.timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(payload.mac, "hex"))) throw new Error("Cache decryption failed: invalid MAC");
340
+ const iv = Buffer.from(payload.iv, "base64");
341
+ const enc = Buffer.from(payload.value, "base64");
342
+ const decipher = crypto.default.createDecipheriv(CIPHER, ENCRYPTION_KEY, iv);
343
+ return Buffer.concat([decipher.update(enc), decipher.final()]).toString("utf8");
344
+ }
345
+ const CACHE_PREFIX = process.env.CACHE_PREFIX ? String(process.env.CACHE_PREFIX) : process.env.APP_NAME || "app";
346
+ function prefixed(key) {
347
+ if (!CACHE_PREFIX) return key;
348
+ return `${CACHE_PREFIX}:${key}`;
349
+ }
350
+ function stripPrefix(fullKey) {
351
+ if (!CACHE_PREFIX) return fullKey;
352
+ return fullKey.startsWith(CACHE_PREFIX + ":") ? fullKey.slice(CACHE_PREFIX.length + 1) : fullKey;
353
+ }
354
+ function generateCacheKey(...parts) {
355
+ return parts.filter((p) => p !== void 0 && p !== null).map((p) => p instanceof Date ? p.toISOString() : String(p).trim().replace(/\s+/g, "_")).join(":");
356
+ }
357
+ var FileCache = class {
358
+ constructor(baseDir) {
359
+ this.initialized = false;
360
+ this.dir = baseDir || path.default.resolve(__dirname$1, "../../tmp/cache");
361
+ }
362
+ async init() {
363
+ if (this.initialized) return;
364
+ await fs.default.promises.mkdir(this.dir, { recursive: true });
365
+ this.initialized = true;
366
+ }
367
+ filePath(key) {
368
+ const safe = encodeURIComponent(prefixed(key));
369
+ return path.default.join(this.dir, `${safe}.json`);
370
+ }
371
+ async get(key) {
372
+ await this.init();
373
+ const p = this.filePath(key);
374
+ try {
375
+ const raw = await fs.default.promises.readFile(p, "utf8");
376
+ const parsed = JSON.parse(raw);
377
+ if (parsed.expiresAt && Date.now() > parsed.expiresAt) {
378
+ try {
379
+ await fs.default.promises.unlink(p);
380
+ } catch (e) {}
381
+ return null;
382
+ }
383
+ const stored = parsed.value;
384
+ if (typeof stored === "string") {
385
+ const dec = decryptRaw(stored);
386
+ if (dec !== null) try {
387
+ return JSON.parse(dec);
388
+ } catch (e) {
389
+ return dec;
390
+ }
391
+ return stored;
392
+ }
393
+ return parsed.value;
394
+ } catch (e) {
395
+ return null;
396
+ }
397
+ }
398
+ async set(key, value, ttlSeconds) {
399
+ await this.init();
400
+ const p = this.filePath(key);
401
+ const expiresAt = ttlSeconds ? Date.now() + ttlSeconds * 1e3 : null;
402
+ const payload = {
403
+ value: encryptRaw(typeof value === "string" ? value : JSON.stringify(value)),
404
+ expiresAt
405
+ };
406
+ await fs.default.promises.writeFile(p, JSON.stringify(payload), "utf8");
407
+ }
408
+ async del(key) {
409
+ await this.init();
410
+ const p = this.filePath(key);
411
+ try {
412
+ await fs.default.promises.unlink(p);
413
+ return true;
414
+ } catch (e) {
415
+ return false;
416
+ }
417
+ }
418
+ async has(key) {
419
+ const v = await this.get(key);
420
+ return v !== null && v !== void 0;
421
+ }
422
+ async clear() {
423
+ await this.init();
424
+ const files = await fs.default.promises.readdir(this.dir);
425
+ await Promise.all(files.map((f) => fs.default.promises.unlink(path.default.join(this.dir, f)).catch(() => {})));
426
+ }
427
+ async keys() {
428
+ await this.init();
429
+ return (await fs.default.promises.readdir(this.dir)).filter((f) => f.endsWith(".json")).map((f) => decodeURIComponent(f.replace(/\.json$/, ""))).map(stripPrefix);
430
+ }
431
+ };
432
+ var DBCache = class {
433
+ constructor() {
434
+ this.initialized = false;
435
+ }
436
+ async init() {
437
+ if (this.initialized) return;
438
+ this.initialized = true;
439
+ }
440
+ now() {
441
+ return Date.now();
442
+ }
443
+ async get(key) {
444
+ await this.init();
445
+ const record = await Cache$1.where("k", prefixed(key)).first();
446
+ if (!record) return null;
447
+ const expiresAt = record.getAttribute ? record.getAttribute("expires_at") : record.expires_at;
448
+ if (expiresAt && this.now() > Number(expiresAt)) {
449
+ await this.del(key);
450
+ return null;
451
+ }
452
+ let rawVal = record.getAttribute ? record.getAttribute("v") : record.v;
453
+ if (typeof rawVal === "string") {
454
+ const dec = decryptRaw(rawVal);
455
+ if (dec !== null) try {
456
+ return JSON.parse(dec);
457
+ } catch {
458
+ return dec;
459
+ }
460
+ try {
461
+ return JSON.parse(rawVal);
462
+ } catch {
463
+ return rawVal;
464
+ }
465
+ }
466
+ return rawVal;
467
+ }
468
+ async set(key, value, ttlSeconds) {
469
+ await this.init();
470
+ const expiresAt = ttlSeconds ? Date.now() + ttlSeconds * 1e3 : null;
471
+ const stored = encryptRaw(typeof value === "string" ? value : JSON.stringify(value));
472
+ let record = await Cache$1.where("k", prefixed(key)).first();
473
+ if (record) if (record.setAttribute) {
474
+ record.setAttribute("v", stored);
475
+ record.setAttribute("expires_at", expiresAt);
476
+ await record.save();
477
+ } else {
478
+ record.v = stored;
479
+ record.expires_at = expiresAt;
480
+ await record.save();
481
+ }
482
+ else await Cache$1.create({
483
+ k: prefixed(key),
484
+ v: stored,
485
+ expires_at: expiresAt
486
+ });
487
+ }
488
+ async del(key) {
489
+ await this.init();
490
+ const record = await Cache$1.where("k", prefixed(key)).first();
491
+ if (!record) return false;
492
+ await record.delete(true);
493
+ return true;
494
+ }
495
+ async has(key) {
496
+ const v = await this.get(key);
497
+ return v !== null && v !== void 0;
498
+ }
499
+ async clear() {
500
+ await this.init();
501
+ const all = await Cache$1.query().get();
502
+ for (const rec of all) try {
503
+ await rec.delete(true);
504
+ } catch {}
505
+ }
506
+ async keys() {
507
+ await this.init();
508
+ return (await Cache$1.query().get()).map((r) => r.getAttribute ? r.getAttribute("k") : r.k).map(stripPrefix);
509
+ }
510
+ };
511
+ var RedisCache = class {
512
+ constructor() {
513
+ this.client = null;
514
+ this.initialized = false;
515
+ }
516
+ async init() {
517
+ if (this.initialized) return;
518
+ let createClient;
519
+ try {
520
+ createClient = (await import("redis")).createClient;
521
+ } catch (e) {
522
+ throw new Error("Redis driver selected (CACHE_DRIVER=redis) but \"redis\" package is not installed. Install it with `npm install redis`.");
523
+ }
524
+ const redisUrl = process.env.REDIS_URL || void 0;
525
+ const host = process.env.REDIS_HOST || void 0;
526
+ const port = process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT, 10) : void 0;
527
+ const password = process.env.REDIS_PASSWORD || void 0;
528
+ const opts = {};
529
+ if (redisUrl) opts.url = redisUrl;
530
+ if (host) {
531
+ opts.socket = { host };
532
+ if (port) opts.socket.port = port;
533
+ }
534
+ if (password) opts.password = password;
535
+ this.client = createClient(opts);
536
+ if (typeof this.client.connect === "function") await this.client.connect();
537
+ this.initialized = true;
538
+ }
539
+ async get(key) {
540
+ await this.init();
541
+ const res = await this.client.get(prefixed(key));
542
+ if (res === null) return null;
543
+ const dec = decryptRaw(res);
544
+ if (dec !== null) try {
545
+ return JSON.parse(dec);
546
+ } catch (e) {
547
+ return dec;
548
+ }
549
+ try {
550
+ return JSON.parse(res);
551
+ } catch (e) {
552
+ return res;
553
+ }
554
+ }
555
+ async set(key, value, ttlSeconds) {
556
+ await this.init();
557
+ const stored = encryptRaw(typeof value === "string" ? value : JSON.stringify(value));
558
+ if (ttlSeconds && ttlSeconds > 0) await this.client.set(prefixed(key), stored, { EX: ttlSeconds });
559
+ else await this.client.set(prefixed(key), stored);
560
+ }
561
+ async del(key) {
562
+ await this.init();
563
+ return await this.client.del(prefixed(key)) > 0;
564
+ }
565
+ async has(key) {
566
+ await this.init();
567
+ const exists = await this.client.exists(prefixed(key));
568
+ return exists === 1 || exists === true || exists > 0;
569
+ }
570
+ async clear() {
571
+ await this.init();
572
+ const pattern = CACHE_PREFIX ? `${CACHE_PREFIX}:*` : "*";
573
+ let cursor = "0";
574
+ do {
575
+ const res = await this.client.scan(cursor, {
576
+ MATCH: pattern,
577
+ COUNT: 200
578
+ });
579
+ cursor = res.cursor || res[0];
580
+ const keys = res.keys || res[1];
581
+ if (keys.length > 0) await this.client.del(keys);
582
+ } while (cursor !== "0" && String(cursor) !== "0");
583
+ }
584
+ async keys() {
585
+ await this.init();
586
+ const pattern = CACHE_PREFIX ? `${CACHE_PREFIX}:*` : "*";
587
+ const out = [];
588
+ let cursor = "0";
589
+ do {
590
+ const res = await this.client.scan(cursor, {
591
+ MATCH: pattern,
592
+ COUNT: 100
593
+ });
594
+ cursor = res.cursor || res[0];
595
+ const keys = res.keys || res[1];
596
+ for (const k of keys) out.push(stripPrefix(k));
597
+ } while (cursor !== "0");
598
+ return out;
599
+ }
600
+ };
601
+ var CacheManager = class {
602
+ constructor() {
603
+ this.driver = null;
604
+ this.initializing = null;
605
+ }
606
+ createDriver() {
607
+ const driver = (process.env.CACHE_DRIVER || "file").toLowerCase();
608
+ if (driver === "redis") return new RedisCache();
609
+ if (driver === "database" || driver === "db") return new DBCache();
610
+ return new FileCache();
611
+ }
612
+ async ensureInit() {
613
+ if (this.driver) return;
614
+ if (!this.initializing) {
615
+ this.driver = this.createDriver();
616
+ this.initializing = (async () => {
617
+ await this.driver.init();
618
+ this.initializing = null;
619
+ })();
620
+ }
621
+ await this.initializing;
622
+ }
623
+ async init() {
624
+ await this.ensureInit();
625
+ }
626
+ async get(key) {
627
+ await this.ensureInit();
628
+ return this.driver.get(key);
629
+ }
630
+ async set(key, value, ttlSeconds) {
631
+ await this.ensureInit();
632
+ return this.driver.set(key, value, ttlSeconds);
633
+ }
634
+ async del(key) {
635
+ await this.ensureInit();
636
+ return this.driver.del(key);
637
+ }
638
+ async has(key) {
639
+ await this.ensureInit();
640
+ return this.driver.has(key);
641
+ }
642
+ async clear() {
643
+ await this.ensureInit();
644
+ return this.driver.clear();
645
+ }
646
+ async keys() {
647
+ await this.ensureInit();
648
+ return this.driver.keys();
649
+ }
650
+ };
651
+ const manager = new CacheManager();
652
+ const Cache = {
653
+ get: async (k) => {
654
+ const value = await manager.get(k);
655
+ CacheWatcher.record({
656
+ type: "get",
657
+ key: k,
658
+ hit: value !== null,
659
+ value
660
+ });
661
+ return value;
662
+ },
663
+ set: async (k, v, ttlSeconds) => {
664
+ await manager.set(k, v, ttlSeconds);
665
+ CacheWatcher.record({
666
+ type: "set",
667
+ key: k,
668
+ value: v,
669
+ ttlSeconds
670
+ });
671
+ },
672
+ del: async (k) => {
673
+ const result = await manager.del(k);
674
+ CacheWatcher.record({
675
+ type: "del",
676
+ key: k
677
+ });
678
+ return result;
679
+ },
680
+ has: async (k) => {
681
+ const result = await manager.has(k);
682
+ CacheWatcher.record({
683
+ type: "has",
684
+ key: k,
685
+ hit: result
686
+ });
687
+ return result;
688
+ },
689
+ clear: async () => {
690
+ await manager.clear();
691
+ CacheWatcher.record({
692
+ type: "clear",
693
+ key: ""
694
+ });
695
+ },
696
+ keys: () => manager.keys(),
697
+ forget: async (k) => {
698
+ const result = await manager.del(k);
699
+ CacheWatcher.record({
700
+ type: "del",
701
+ key: k
702
+ });
703
+ return result;
704
+ },
705
+ flush: async () => {
706
+ await manager.clear();
707
+ CacheWatcher.record({
708
+ type: "clear",
709
+ key: ""
710
+ });
711
+ },
712
+ remember: async (key, ttlSeconds, callback) => {
713
+ const cached = await manager.get(key);
714
+ if (cached !== null) {
715
+ CacheWatcher.record({
716
+ type: "get",
717
+ key,
718
+ hit: true,
719
+ value: cached,
720
+ ttlSeconds
721
+ });
722
+ return cached;
723
+ }
724
+ CacheWatcher.record({
725
+ type: "get",
726
+ key,
727
+ hit: false,
728
+ ttlSeconds
729
+ });
730
+ const value = await callback();
731
+ await manager.set(key, value, ttlSeconds);
732
+ CacheWatcher.record({
733
+ type: "set",
734
+ key,
735
+ value,
736
+ ttlSeconds
737
+ });
738
+ return value;
739
+ }
740
+ };
741
+ const getCacheDriverName = () => {
742
+ return (process.env.CACHE_DRIVER || "file").toLowerCase();
743
+ };
744
+ const getCacheDriver = () => manager;
745
+ const initCache = async () => manager.init();
746
+ const cacheGet = async (k) => manager.get(k);
747
+ const cacheSet = async (k, v, ttlSeconds) => manager.set(k, v, ttlSeconds);
748
+ const cacheDel = async (k) => manager.del(k);
749
+ const cacheHas = async (k) => manager.has(k);
750
+ const cacheClear = async () => manager.clear();
751
+ const cacheKeys = async () => manager.keys();
752
+ const cacheDelPrefix = async (prefix) => {
753
+ const matches = (await manager.keys()).filter((k) => k.startsWith(prefix));
754
+ await Promise.all(matches.map((k) => manager.del(k).catch(() => {})));
755
+ return matches.length;
756
+ };
757
+ //#endregion
758
+ //#region src/CacheServiceProvider.ts
759
+ var CacheServiceProvider = class extends _lara_node_core.ServiceProvider {
760
+ register() {
761
+ this.container.singleton("cache", () => getCacheDriver());
762
+ }
763
+ async boot() {
764
+ const skip = (process.env.SKIP_CACHE ?? "").toLowerCase();
765
+ if (skip === "1" || skip === "true") {
766
+ console.warn("[Cache] SKIP_CACHE set — skipping cache initialization");
767
+ return;
768
+ }
769
+ try {
770
+ await initCache();
771
+ console.log(`[Cache] Initialized (driver=${getCacheDriverName()})`);
772
+ } catch (err) {
773
+ console.error("[Cache] Initialization failed:", err.message);
774
+ }
775
+ }
776
+ };
777
+ //#endregion
778
+ exports.Cache = Cache;
779
+ exports.CacheServiceProvider = CacheServiceProvider;
780
+ exports.RateLimitExceededException = RateLimitExceededException;
781
+ exports.RateLimiter = RateLimiter;
782
+ exports.RateLimiterFacade = RateLimiterFacade;
783
+ exports.cacheClear = cacheClear;
784
+ exports.cacheDel = cacheDel;
785
+ exports.cacheDelPrefix = cacheDelPrefix;
786
+ exports.cacheGet = cacheGet;
787
+ exports.cacheHas = cacheHas;
788
+ exports.cacheKeys = cacheKeys;
789
+ exports.cacheSet = cacheSet;
790
+ exports.defineRateLimiter = defineRateLimiter;
791
+ exports.generateCacheKey = generateCacheKey;
792
+ exports.getCacheDriver = getCacheDriver;
793
+ exports.getCacheDriverName = getCacheDriverName;
794
+ exports.getNamedLimiter = getNamedLimiter;
795
+ exports.initCache = initCache;
796
+ exports.setCacheWatchHook = setCacheWatchHook;