@chidchanun/bcp 0.2.14 → 0.2.16

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.
@@ -0,0 +1,1554 @@
1
+ // packages/cache/src/index.ts
2
+ import {
3
+ AsyncLocalStorage
4
+ } from "node:async_hooks";
5
+ import {
6
+ createHash
7
+ } from "node:crypto";
8
+ var CACHE_RUNTIME_SYMBOL = /* @__PURE__ */ Symbol.for(
9
+ "bcp.framework.cache.runtime"
10
+ );
11
+ var globalRecord = globalThis;
12
+ var runtimeState = globalRecord[CACHE_RUNTIME_SYMBOL];
13
+ if (!runtimeState) {
14
+ runtimeState = {
15
+ persistentCache: /* @__PURE__ */ new Map(),
16
+ inFlight: /* @__PURE__ */ new Map(),
17
+ requestStorage: new AsyncLocalStorage()
18
+ };
19
+ globalRecord[CACHE_RUNTIME_SYMBOL] = runtimeState;
20
+ }
21
+ var persistentCache = runtimeState.persistentCache;
22
+ var inFlight = runtimeState.inFlight;
23
+ var requestStorage = runtimeState.requestStorage;
24
+ function dedupe(fn, key) {
25
+ const namespace = key ?? fn.name ?? "anonymous";
26
+ return async (...args) => {
27
+ const context = ensureRequestContext();
28
+ const cacheKey = `request:${namespace}:${hashArguments(args)}`;
29
+ const existing = context.memo.get(
30
+ cacheKey
31
+ );
32
+ if (existing) {
33
+ return await existing;
34
+ }
35
+ const promise = Promise.resolve(
36
+ fn(...args)
37
+ );
38
+ context.memo.set(
39
+ cacheKey,
40
+ promise
41
+ );
42
+ try {
43
+ return await promise;
44
+ } catch (error) {
45
+ context.memo.delete(
46
+ cacheKey
47
+ );
48
+ throw error;
49
+ }
50
+ };
51
+ }
52
+ function cache(fn, options = {}) {
53
+ const namespace = options.key ?? fn.name;
54
+ if (!namespace) {
55
+ throw new Error(
56
+ "BCP Framework: cache() requires options.key when the wrapped function has no name."
57
+ );
58
+ }
59
+ validateRevalidate(
60
+ options.revalidate
61
+ );
62
+ validateTags(
63
+ options.tags
64
+ );
65
+ return async (...args) => {
66
+ if (options.revalidate === 0) {
67
+ return await fn(
68
+ ...args
69
+ );
70
+ }
71
+ const cacheKey = `data:${namespace}:${hashArguments(args)}`;
72
+ const existingEntry = readCacheEntry(
73
+ cacheKey
74
+ );
75
+ if (existingEntry.hit) {
76
+ return existingEntry.value;
77
+ }
78
+ const pending = inFlight.get(
79
+ cacheKey
80
+ );
81
+ if (pending) {
82
+ return await pending;
83
+ }
84
+ const promise = Promise.resolve(
85
+ fn(...args)
86
+ );
87
+ inFlight.set(
88
+ cacheKey,
89
+ promise
90
+ );
91
+ try {
92
+ const value = await promise;
93
+ const currentPath = requestStorage.getStore()?.pathname ?? null;
94
+ const paths = new Set(
95
+ (options.paths ?? []).map(
96
+ normalizePath
97
+ )
98
+ );
99
+ if (currentPath) {
100
+ paths.add(
101
+ currentPath
102
+ );
103
+ }
104
+ setCacheEntry(
105
+ cacheKey,
106
+ value,
107
+ {
108
+ revalidate: options.revalidate,
109
+ tags: options.tags,
110
+ paths: Array.from(
111
+ paths
112
+ )
113
+ }
114
+ );
115
+ return value;
116
+ } finally {
117
+ inFlight.delete(
118
+ cacheKey
119
+ );
120
+ }
121
+ };
122
+ }
123
+ function setCacheEntry(key, value, options = {}) {
124
+ validateRevalidate(
125
+ options.revalidate
126
+ );
127
+ validateTags(
128
+ options.tags
129
+ );
130
+ if (options.revalidate === 0) {
131
+ persistentCache.delete(
132
+ key
133
+ );
134
+ return;
135
+ }
136
+ const expiresAt = options.revalidate === void 0 || options.revalidate === false ? null : Date.now() + options.revalidate * 1e3;
137
+ persistentCache.set(
138
+ key,
139
+ {
140
+ value,
141
+ expiresAt,
142
+ tags: new Set(
143
+ options.tags ?? []
144
+ ),
145
+ paths: new Set(
146
+ (options.paths ?? []).map(
147
+ normalizePath
148
+ )
149
+ )
150
+ }
151
+ );
152
+ }
153
+ function revalidateTag(tag) {
154
+ const normalized = normalizeTag(tag);
155
+ let removed = 0;
156
+ for (const [
157
+ key,
158
+ entry
159
+ ] of persistentCache) {
160
+ if (entry.tags.has(
161
+ normalized
162
+ )) {
163
+ persistentCache.delete(
164
+ key
165
+ );
166
+ removed++;
167
+ }
168
+ }
169
+ return removed;
170
+ }
171
+ function revalidatePath(pathname) {
172
+ const normalized = normalizePath(pathname);
173
+ let removed = 0;
174
+ for (const [
175
+ key,
176
+ entry
177
+ ] of persistentCache) {
178
+ const matches = Array.from(
179
+ entry.paths
180
+ ).some(
181
+ (cachedPath) => pathMatches(
182
+ cachedPath,
183
+ normalized
184
+ )
185
+ );
186
+ if (matches) {
187
+ persistentCache.delete(
188
+ key
189
+ );
190
+ removed++;
191
+ }
192
+ }
193
+ return removed;
194
+ }
195
+ function clearCache() {
196
+ persistentCache.clear();
197
+ inFlight.clear();
198
+ }
199
+ function getCacheStats() {
200
+ pruneExpiredEntries();
201
+ return {
202
+ entries: persistentCache.size,
203
+ inFlight: inFlight.size
204
+ };
205
+ }
206
+ function normalizeRevalidate(value) {
207
+ if (value === void 0) {
208
+ return void 0;
209
+ }
210
+ if (value === false) {
211
+ return false;
212
+ }
213
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
214
+ return value;
215
+ }
216
+ throw new Error(
217
+ "BCP Framework: revalidate must be false or a non-negative number of seconds."
218
+ );
219
+ }
220
+ function readCacheEntry(key) {
221
+ const entry = persistentCache.get(
222
+ key
223
+ );
224
+ if (!entry) {
225
+ return {
226
+ hit: false
227
+ };
228
+ }
229
+ if (entry.expiresAt !== null && entry.expiresAt <= Date.now()) {
230
+ persistentCache.delete(
231
+ key
232
+ );
233
+ return {
234
+ hit: false
235
+ };
236
+ }
237
+ return {
238
+ hit: true,
239
+ value: entry.value
240
+ };
241
+ }
242
+ function ensureRequestContext() {
243
+ const existing = requestStorage.getStore();
244
+ if (existing) {
245
+ return existing;
246
+ }
247
+ const context = {
248
+ pathname: null,
249
+ memo: /* @__PURE__ */ new Map()
250
+ };
251
+ requestStorage.enterWith(
252
+ context
253
+ );
254
+ return context;
255
+ }
256
+ function pruneExpiredEntries() {
257
+ const now = Date.now();
258
+ for (const [
259
+ key,
260
+ entry
261
+ ] of persistentCache) {
262
+ if (entry.expiresAt !== null && entry.expiresAt <= now) {
263
+ persistentCache.delete(
264
+ key
265
+ );
266
+ }
267
+ }
268
+ }
269
+ function validateRevalidate(value) {
270
+ normalizeRevalidate(
271
+ value
272
+ );
273
+ }
274
+ function validateTags(tags) {
275
+ for (const tag of tags ?? []) {
276
+ normalizeTag(
277
+ tag
278
+ );
279
+ }
280
+ }
281
+ function normalizeTag(value) {
282
+ const normalized = value.trim();
283
+ if (!normalized) {
284
+ throw new Error(
285
+ "BCP Framework: cache tags cannot be empty."
286
+ );
287
+ }
288
+ return normalized;
289
+ }
290
+ function normalizePath(value) {
291
+ let pathname = value.trim();
292
+ if (!pathname) {
293
+ return "/";
294
+ }
295
+ try {
296
+ if (pathname.startsWith(
297
+ "http://"
298
+ ) || pathname.startsWith(
299
+ "https://"
300
+ )) {
301
+ pathname = new URL(
302
+ pathname
303
+ ).pathname;
304
+ }
305
+ } catch {
306
+ }
307
+ pathname = pathname.split("?")[0].split("#")[0];
308
+ if (!pathname.startsWith("/")) {
309
+ pathname = `/${pathname}`;
310
+ }
311
+ if (pathname.length > 1 && pathname.endsWith("/")) {
312
+ pathname = pathname.slice(
313
+ 0,
314
+ -1
315
+ );
316
+ }
317
+ return pathname;
318
+ }
319
+ function pathMatches(cachedPath, invalidatedPath) {
320
+ if (invalidatedPath === "/") {
321
+ return true;
322
+ }
323
+ return cachedPath === invalidatedPath || cachedPath.startsWith(
324
+ `${invalidatedPath}/`
325
+ );
326
+ }
327
+ function hashArguments(args) {
328
+ return createHash(
329
+ "sha256"
330
+ ).update(
331
+ stableSerialize(
332
+ args
333
+ )
334
+ ).digest(
335
+ "hex"
336
+ ).slice(
337
+ 0,
338
+ 24
339
+ );
340
+ }
341
+ function stableSerialize(value, seen = /* @__PURE__ */ new WeakSet()) {
342
+ if (value === null) {
343
+ return "null";
344
+ }
345
+ switch (typeof value) {
346
+ case "undefined":
347
+ return "undefined";
348
+ case "string":
349
+ return JSON.stringify(
350
+ value
351
+ );
352
+ case "number":
353
+ case "boolean":
354
+ return String(value);
355
+ case "bigint":
356
+ return `${value.toString()}n`;
357
+ case "symbol":
358
+ case "function":
359
+ throw new Error(
360
+ `BCP Framework: cache keys cannot include ${typeof value} values.`
361
+ );
362
+ case "object":
363
+ break;
364
+ default:
365
+ return String(value);
366
+ }
367
+ const object = value;
368
+ if (seen.has(object)) {
369
+ throw new Error(
370
+ "BCP Framework: cache keys cannot include circular values."
371
+ );
372
+ }
373
+ seen.add(object);
374
+ try {
375
+ if (value instanceof Date) {
376
+ return `Date(${value.toISOString()})`;
377
+ }
378
+ if (Array.isArray(value)) {
379
+ return `[${value.map(
380
+ (item) => stableSerialize(
381
+ item,
382
+ seen
383
+ )
384
+ ).join(",")}]`;
385
+ }
386
+ const record = value;
387
+ return `{${Object.keys(record).sort().map(
388
+ (key) => `${JSON.stringify(key)}:${stableSerialize(
389
+ record[key],
390
+ seen
391
+ )}`
392
+ ).join(",")}}`;
393
+ } finally {
394
+ seen.delete(object);
395
+ }
396
+ }
397
+
398
+ // packages/cache/src/platform-v2.ts
399
+ import {
400
+ randomUUID
401
+ } from "node:crypto";
402
+ var DEFAULT_LOCK_TTL_MS = 3e4;
403
+ var DEFAULT_WAIT_TIMEOUT_MS = 5e3;
404
+ var DEFAULT_POLL_INTERVAL_MS = 25;
405
+ function createMemoryCacheAdapter(options = {}) {
406
+ const now = options.now ?? Date.now;
407
+ const records = /* @__PURE__ */ new Map();
408
+ const adapter = {
409
+ get size() {
410
+ prune();
411
+ return records.size;
412
+ },
413
+ async get(rawKey) {
414
+ const key = normalizeKey(rawKey);
415
+ const entry = records.get(key);
416
+ if (!entry) {
417
+ return null;
418
+ }
419
+ if (isExpired(entry, now())) {
420
+ records.delete(key);
421
+ return null;
422
+ }
423
+ return cloneEntry(entry);
424
+ },
425
+ async set(rawKey, value, setOptions = {}) {
426
+ const key = normalizeKey(rawKey);
427
+ const timestamp = now();
428
+ assertFiniteTimestamp(timestamp, "cache timestamp");
429
+ const current = records.get(key);
430
+ const expiresAt = normalizeExpiresAt(
431
+ setOptions.expiresAt
432
+ );
433
+ records.set(key, {
434
+ key,
435
+ value,
436
+ expiresAt,
437
+ tags: normalizeTags(setOptions.tags),
438
+ paths: normalizePaths(setOptions.paths),
439
+ createdAt: current?.createdAt ?? timestamp,
440
+ updatedAt: timestamp
441
+ });
442
+ },
443
+ async delete(rawKey) {
444
+ return records.delete(
445
+ normalizeKey(rawKey)
446
+ );
447
+ },
448
+ async clear() {
449
+ const count = records.size;
450
+ records.clear();
451
+ return count;
452
+ },
453
+ async revalidateTag(rawTag) {
454
+ const tag = normalizeTag2(rawTag);
455
+ let removed = 0;
456
+ for (const [key, record] of records) {
457
+ if (record.tags.includes(tag)) {
458
+ records.delete(key);
459
+ removed++;
460
+ }
461
+ }
462
+ return removed;
463
+ },
464
+ async revalidatePath(rawPath) {
465
+ const pathname = normalizePath2(rawPath);
466
+ let removed = 0;
467
+ for (const [key, record] of records) {
468
+ if (record.paths.some(
469
+ (path) => pathMatches2(path, pathname)
470
+ )) {
471
+ records.delete(key);
472
+ removed++;
473
+ }
474
+ }
475
+ return removed;
476
+ },
477
+ async entries() {
478
+ prune();
479
+ return records.size;
480
+ }
481
+ };
482
+ return adapter;
483
+ function prune() {
484
+ const timestamp = now();
485
+ for (const [key, record] of records) {
486
+ if (isExpired(record, timestamp)) {
487
+ records.delete(key);
488
+ }
489
+ }
490
+ }
491
+ }
492
+ function createMemoryCacheLockAdapter(options = {}) {
493
+ const now = options.now ?? Date.now;
494
+ const locks = /* @__PURE__ */ new Map();
495
+ const adapter = {
496
+ get size() {
497
+ prune();
498
+ return locks.size;
499
+ },
500
+ async acquire(rawKey, rawOwnerId, ttlMs) {
501
+ const key = normalizeKey(rawKey);
502
+ const ownerId = normalizeOwnerId(rawOwnerId);
503
+ const ttl = positiveInteger(ttlMs, "lock ttlMs");
504
+ const timestamp = now();
505
+ const current = locks.get(key);
506
+ if (current && current.expiresAt > timestamp) {
507
+ return false;
508
+ }
509
+ locks.set(key, {
510
+ ownerId,
511
+ expiresAt: timestamp + ttl
512
+ });
513
+ return true;
514
+ },
515
+ async extend(rawKey, rawOwnerId, ttlMs) {
516
+ const key = normalizeKey(rawKey);
517
+ const ownerId = normalizeOwnerId(rawOwnerId);
518
+ const ttl = positiveInteger(ttlMs, "lock ttlMs");
519
+ const timestamp = now();
520
+ const current = locks.get(key);
521
+ if (!current || current.ownerId !== ownerId || current.expiresAt <= timestamp) {
522
+ return false;
523
+ }
524
+ current.expiresAt = timestamp + ttl;
525
+ return true;
526
+ },
527
+ async release(rawKey, rawOwnerId) {
528
+ const key = normalizeKey(rawKey);
529
+ const ownerId = normalizeOwnerId(rawOwnerId);
530
+ const current = locks.get(key);
531
+ if (!current || current.ownerId !== ownerId) {
532
+ return false;
533
+ }
534
+ locks.delete(key);
535
+ return true;
536
+ }
537
+ };
538
+ return adapter;
539
+ function prune() {
540
+ const timestamp = now();
541
+ for (const [key, record] of locks) {
542
+ if (record.expiresAt <= timestamp) {
543
+ locks.delete(key);
544
+ }
545
+ }
546
+ }
547
+ }
548
+ function createCacheStore(options = {}) {
549
+ const adapter = options.adapter ?? createMemoryCacheAdapter({
550
+ now: options.now
551
+ });
552
+ const lock = options.lock;
553
+ const now = options.now ?? Date.now;
554
+ const idFactory = options.idFactory ?? randomUUID;
555
+ const metrics = options.metrics;
556
+ const inFlight2 = /* @__PURE__ */ new Map();
557
+ const counters = {
558
+ hits: 0,
559
+ misses: 0,
560
+ sets: 0,
561
+ deletes: 0,
562
+ invalidations: 0,
563
+ loads: 0,
564
+ loadErrors: 0,
565
+ lockAcquired: 0,
566
+ lockContentions: 0,
567
+ lockTimeouts: 0
568
+ };
569
+ let closed = false;
570
+ const store = {
571
+ adapter,
572
+ lock,
573
+ async get(rawKey) {
574
+ assertOpen();
575
+ const key = normalizeKey(rawKey);
576
+ const entry = await adapter.get(key);
577
+ if (!entry) {
578
+ count("misses", "miss");
579
+ return void 0;
580
+ }
581
+ if (entry.expiresAt !== null && entry.expiresAt <= now()) {
582
+ await adapter.delete(key);
583
+ count("misses", "miss");
584
+ return void 0;
585
+ }
586
+ count("hits", "hit");
587
+ return entry.value;
588
+ },
589
+ async set(rawKey, value, setOptions = {}) {
590
+ assertOpen();
591
+ const key = normalizeKey(rawKey);
592
+ if (setOptions.ttlMs === 0) {
593
+ await store.delete(key);
594
+ return;
595
+ }
596
+ const expiresAt = setOptions.ttlMs === void 0 || setOptions.ttlMs === false ? null : now() + positiveInteger(
597
+ setOptions.ttlMs,
598
+ "cache ttlMs"
599
+ );
600
+ await adapter.set(
601
+ key,
602
+ value,
603
+ {
604
+ expiresAt,
605
+ tags: normalizeTags(setOptions.tags),
606
+ paths: normalizePaths(setOptions.paths)
607
+ }
608
+ );
609
+ count("sets", "set");
610
+ },
611
+ async delete(rawKey) {
612
+ assertOpen();
613
+ const removed = await adapter.delete(
614
+ normalizeKey(rawKey)
615
+ );
616
+ if (removed) {
617
+ count("deletes", "delete");
618
+ }
619
+ return removed;
620
+ },
621
+ async clear() {
622
+ assertOpen();
623
+ const removed = await adapter.clear();
624
+ if (removed > 0) {
625
+ counters.invalidations += removed;
626
+ metrics?.record("invalidate", removed);
627
+ }
628
+ return removed;
629
+ },
630
+ async revalidateTag(rawTag) {
631
+ assertOpen();
632
+ const removed = await adapter.revalidateTag(
633
+ normalizeTag2(rawTag)
634
+ );
635
+ if (removed > 0) {
636
+ counters.invalidations += removed;
637
+ metrics?.record("invalidate", removed);
638
+ }
639
+ return removed;
640
+ },
641
+ async revalidatePath(rawPath) {
642
+ assertOpen();
643
+ const removed = await adapter.revalidatePath(
644
+ normalizePath2(rawPath)
645
+ );
646
+ if (removed > 0) {
647
+ counters.invalidations += removed;
648
+ metrics?.record("invalidate", removed);
649
+ }
650
+ return removed;
651
+ },
652
+ async getOrSet(rawKey, loader, loadOptions = {}) {
653
+ assertOpen();
654
+ if (typeof loader !== "function") {
655
+ throw new TypeError(
656
+ "BCP Cache: loader must be a function."
657
+ );
658
+ }
659
+ const key = normalizeKey(rawKey);
660
+ const cached = await store.get(key);
661
+ if (cached !== void 0) {
662
+ return cached;
663
+ }
664
+ const pending = inFlight2.get(key);
665
+ if (pending) {
666
+ return await pending;
667
+ }
668
+ const promise = loadWithStampedeProtection(
669
+ key,
670
+ loader,
671
+ loadOptions
672
+ );
673
+ inFlight2.set(key, promise);
674
+ metrics?.setInFlight?.(inFlight2.size);
675
+ try {
676
+ return await promise;
677
+ } finally {
678
+ inFlight2.delete(key);
679
+ metrics?.setInFlight?.(inFlight2.size);
680
+ }
681
+ },
682
+ async stats() {
683
+ assertOpen();
684
+ return {
685
+ ...counters,
686
+ inFlight: inFlight2.size,
687
+ adapterEntries: adapter.entries ? await adapter.entries() : void 0
688
+ };
689
+ },
690
+ async close() {
691
+ if (closed) {
692
+ return;
693
+ }
694
+ closed = true;
695
+ inFlight2.clear();
696
+ metrics?.setInFlight?.(0);
697
+ const lockClose = lock && !Object.is(lock, adapter) ? lock.close?.() : void 0;
698
+ await Promise.all([
699
+ adapter.close?.(),
700
+ lockClose
701
+ ]);
702
+ }
703
+ };
704
+ return store;
705
+ async function loadWithStampedeProtection(key, loader, loadOptions) {
706
+ if (!lock) {
707
+ return executeLoader(
708
+ key,
709
+ loader,
710
+ loadOptions
711
+ );
712
+ }
713
+ const lockTtlMs = positiveInteger(
714
+ loadOptions.lockTtlMs ?? DEFAULT_LOCK_TTL_MS,
715
+ "lock ttlMs"
716
+ );
717
+ const waitTimeoutMs = nonNegativeInteger(
718
+ loadOptions.waitTimeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS,
719
+ "lock waitTimeoutMs"
720
+ );
721
+ const pollIntervalMs = positiveInteger(
722
+ loadOptions.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,
723
+ "lock pollIntervalMs"
724
+ );
725
+ const ownerId = normalizeOwnerId(idFactory());
726
+ const lockKey = `cache-load:${key}`;
727
+ if (await lock.acquire(
728
+ lockKey,
729
+ ownerId,
730
+ lockTtlMs
731
+ )) {
732
+ count(
733
+ "lockAcquired",
734
+ "lock-acquired"
735
+ );
736
+ return executeUnderLock(
737
+ key,
738
+ ownerId,
739
+ lockKey,
740
+ lockTtlMs,
741
+ loader,
742
+ loadOptions
743
+ );
744
+ }
745
+ count(
746
+ "lockContentions",
747
+ "lock-contention"
748
+ );
749
+ const waitStartedAt = Date.now();
750
+ while (Date.now() - waitStartedAt < waitTimeoutMs) {
751
+ await sleep(pollIntervalMs);
752
+ const cached = await store.get(key);
753
+ if (cached !== void 0) {
754
+ return cached;
755
+ }
756
+ }
757
+ count("lockTimeouts", "lock-timeout");
758
+ if (loadOptions.onLockTimeout === "error") {
759
+ throw new Error(
760
+ `BCP Cache: timed out waiting for distributed cache lock for "${key}".`
761
+ );
762
+ }
763
+ if (await lock.acquire(
764
+ lockKey,
765
+ ownerId,
766
+ lockTtlMs
767
+ )) {
768
+ count(
769
+ "lockAcquired",
770
+ "lock-acquired"
771
+ );
772
+ return executeUnderLock(
773
+ key,
774
+ ownerId,
775
+ lockKey,
776
+ lockTtlMs,
777
+ loader,
778
+ loadOptions
779
+ );
780
+ }
781
+ return executeLoader(
782
+ key,
783
+ loader,
784
+ loadOptions
785
+ );
786
+ }
787
+ async function executeUnderLock(key, ownerId, lockKey, lockTtlMs, loader, loadOptions) {
788
+ let timer;
789
+ let active = true;
790
+ if (lock?.extend) {
791
+ const intervalMs = Math.max(
792
+ 1,
793
+ Math.floor(lockTtlMs / 3)
794
+ );
795
+ const heartbeat = async () => {
796
+ if (!active) {
797
+ return;
798
+ }
799
+ try {
800
+ await lock.extend?.(
801
+ lockKey,
802
+ ownerId,
803
+ lockTtlMs
804
+ );
805
+ } catch {
806
+ } finally {
807
+ if (active) {
808
+ timer = setTimeout(
809
+ heartbeat,
810
+ intervalMs
811
+ );
812
+ }
813
+ }
814
+ };
815
+ timer = setTimeout(
816
+ heartbeat,
817
+ intervalMs
818
+ );
819
+ }
820
+ try {
821
+ const cached = await store.get(key);
822
+ if (cached !== void 0) {
823
+ return cached;
824
+ }
825
+ return await executeLoader(
826
+ key,
827
+ loader,
828
+ loadOptions
829
+ );
830
+ } finally {
831
+ active = false;
832
+ if (timer) {
833
+ clearTimeout(timer);
834
+ }
835
+ await lock?.release(
836
+ lockKey,
837
+ ownerId
838
+ );
839
+ }
840
+ }
841
+ async function executeLoader(key, loader, loadOptions) {
842
+ count("loads", "load");
843
+ try {
844
+ const value = await loader();
845
+ await store.set(
846
+ key,
847
+ value,
848
+ loadOptions
849
+ );
850
+ return value;
851
+ } catch (error) {
852
+ count(
853
+ "loadErrors",
854
+ "load-error"
855
+ );
856
+ throw error;
857
+ }
858
+ }
859
+ function count(key, event, value = 1) {
860
+ counters[key] += value;
861
+ metrics?.record(event, value);
862
+ }
863
+ function assertOpen() {
864
+ if (closed) {
865
+ throw new Error(
866
+ "BCP Cache: cache store is closed."
867
+ );
868
+ }
869
+ }
870
+ }
871
+ function createCacheMetrics(registry, options = {}) {
872
+ const prefix = normalizeMetricPrefix(
873
+ options.prefix ?? "bcp_cache"
874
+ );
875
+ const operations = registry.counter(
876
+ `${prefix}_operations_total`,
877
+ {
878
+ help: "BCP Cache operations by event.",
879
+ labelNames: [
880
+ "event"
881
+ ]
882
+ }
883
+ );
884
+ const inFlight2 = registry.gauge(
885
+ `${prefix}_in_flight`,
886
+ {
887
+ help: "BCP Cache local in-flight loaders."
888
+ }
889
+ );
890
+ return {
891
+ record(event, value = 1) {
892
+ operations.inc(
893
+ value,
894
+ {
895
+ event
896
+ }
897
+ );
898
+ },
899
+ setInFlight(value) {
900
+ inFlight2.set(value);
901
+ }
902
+ };
903
+ }
904
+ function createRedisCacheAdapter(options) {
905
+ assertRedisClient(options.client);
906
+ const namespace = normalizeNamespace(
907
+ options.namespace ?? "bcp:{cache}"
908
+ );
909
+ const now = options.now ?? Date.now;
910
+ const serialize = options.serialize ?? JSON.stringify;
911
+ const deserialize = options.deserialize ?? JSON.parse;
912
+ const client = options.client;
913
+ const adapter = {
914
+ namespace,
915
+ async get(rawKey) {
916
+ const key = normalizeKey(rawKey);
917
+ const entryKey = redisEntryKey(
918
+ namespace,
919
+ key
920
+ );
921
+ const raw = redisString(
922
+ await client.sendCommand([
923
+ "GET",
924
+ entryKey
925
+ ])
926
+ );
927
+ if (raw === null) {
928
+ await client.sendCommand([
929
+ "SREM",
930
+ redisAllKeysKey(namespace),
931
+ entryKey
932
+ ]);
933
+ return null;
934
+ }
935
+ const stored = parseStoredRecord(raw);
936
+ if (stored.expiresAt !== null && stored.expiresAt <= now()) {
937
+ await deleteStoredRecord(
938
+ stored,
939
+ entryKey
940
+ );
941
+ return null;
942
+ }
943
+ return {
944
+ key: stored.key,
945
+ value: deserialize(stored.value),
946
+ expiresAt: stored.expiresAt,
947
+ tags: [...stored.tags],
948
+ paths: [...stored.paths],
949
+ createdAt: stored.createdAt,
950
+ updatedAt: stored.updatedAt
951
+ };
952
+ },
953
+ async set(rawKey, value, setOptions = {}) {
954
+ const key = normalizeKey(rawKey);
955
+ const entryKey = redisEntryKey(
956
+ namespace,
957
+ key
958
+ );
959
+ const timestamp = now();
960
+ const tags = normalizeTags(setOptions.tags);
961
+ const paths = normalizePaths(setOptions.paths);
962
+ const current = await adapter.get(key);
963
+ const stored = {
964
+ key,
965
+ value: serializeValue(
966
+ value,
967
+ serialize
968
+ ),
969
+ expiresAt: normalizeExpiresAt(
970
+ setOptions.expiresAt
971
+ ),
972
+ tags,
973
+ paths,
974
+ tagIndexKeys: tags.map(
975
+ (tag) => redisTagKey(
976
+ namespace,
977
+ tag
978
+ )
979
+ ),
980
+ pathIndexPairs: paths.map((pathname) => ({
981
+ path: pathname,
982
+ key: redisPathKey(
983
+ namespace,
984
+ pathname
985
+ )
986
+ })),
987
+ createdAt: current?.createdAt ?? timestamp,
988
+ updatedAt: timestamp
989
+ };
990
+ const ttlMs = stored.expiresAt === null ? 0 : Math.max(
991
+ 1,
992
+ stored.expiresAt - timestamp
993
+ );
994
+ await evalRedis(
995
+ client,
996
+ REDIS_SET_SCRIPT,
997
+ [
998
+ entryKey,
999
+ redisAllKeysKey(namespace),
1000
+ redisPathsIndexKey(namespace)
1001
+ ],
1002
+ [
1003
+ JSON.stringify(stored),
1004
+ String(ttlMs)
1005
+ ]
1006
+ );
1007
+ },
1008
+ async delete(rawKey) {
1009
+ const key = normalizeKey(rawKey);
1010
+ const entryKey = redisEntryKey(
1011
+ namespace,
1012
+ key
1013
+ );
1014
+ const result = await evalRedis(
1015
+ client,
1016
+ REDIS_DELETE_SCRIPT,
1017
+ [
1018
+ entryKey,
1019
+ redisAllKeysKey(namespace),
1020
+ redisPathsIndexKey(namespace)
1021
+ ],
1022
+ []
1023
+ );
1024
+ return Number(result) > 0;
1025
+ },
1026
+ async clear() {
1027
+ const entryKeys = redisStringArray(
1028
+ await client.sendCommand([
1029
+ "SMEMBERS",
1030
+ redisAllKeysKey(namespace)
1031
+ ])
1032
+ );
1033
+ let removed = 0;
1034
+ for (const entryKey of entryKeys) {
1035
+ const result = await evalRedis(
1036
+ client,
1037
+ REDIS_DELETE_SCRIPT,
1038
+ [
1039
+ entryKey,
1040
+ redisAllKeysKey(namespace),
1041
+ redisPathsIndexKey(namespace)
1042
+ ],
1043
+ []
1044
+ );
1045
+ removed += Number(result) > 0 ? 1 : 0;
1046
+ }
1047
+ await client.sendCommand([
1048
+ "DEL",
1049
+ redisAllKeysKey(namespace),
1050
+ redisPathsIndexKey(namespace)
1051
+ ]);
1052
+ return removed;
1053
+ },
1054
+ async revalidateTag(rawTag) {
1055
+ const tag = normalizeTag2(rawTag);
1056
+ const tagKey = redisTagKey(
1057
+ namespace,
1058
+ tag
1059
+ );
1060
+ const result = await evalRedis(
1061
+ client,
1062
+ REDIS_INVALIDATE_SET_SCRIPT,
1063
+ [
1064
+ tagKey,
1065
+ redisAllKeysKey(namespace),
1066
+ redisPathsIndexKey(namespace)
1067
+ ],
1068
+ []
1069
+ );
1070
+ return Number(result) || 0;
1071
+ },
1072
+ async revalidatePath(rawPath) {
1073
+ const pathname = normalizePath2(rawPath);
1074
+ const registeredPaths = redisStringArray(
1075
+ await client.sendCommand([
1076
+ "SMEMBERS",
1077
+ redisPathsIndexKey(namespace)
1078
+ ])
1079
+ );
1080
+ const matches = registeredPaths.filter(
1081
+ (path) => pathMatches2(path, pathname)
1082
+ ).map((path) => ({
1083
+ path,
1084
+ key: redisPathKey(
1085
+ namespace,
1086
+ path
1087
+ )
1088
+ }));
1089
+ if (matches.length === 0) {
1090
+ return 0;
1091
+ }
1092
+ const result = await evalRedis(
1093
+ client,
1094
+ REDIS_INVALIDATE_PATHS_SCRIPT,
1095
+ [
1096
+ redisAllKeysKey(namespace),
1097
+ redisPathsIndexKey(namespace)
1098
+ ],
1099
+ [
1100
+ JSON.stringify(matches)
1101
+ ]
1102
+ );
1103
+ return Number(result) || 0;
1104
+ },
1105
+ async entries() {
1106
+ return Number(
1107
+ await client.sendCommand([
1108
+ "SCARD",
1109
+ redisAllKeysKey(namespace)
1110
+ ])
1111
+ ) || 0;
1112
+ },
1113
+ async close() {
1114
+ await options.close?.();
1115
+ }
1116
+ };
1117
+ return adapter;
1118
+ async function deleteStoredRecord(_stored, entryKey) {
1119
+ await evalRedis(
1120
+ client,
1121
+ REDIS_DELETE_SCRIPT,
1122
+ [
1123
+ entryKey,
1124
+ redisAllKeysKey(namespace),
1125
+ redisPathsIndexKey(namespace)
1126
+ ],
1127
+ []
1128
+ );
1129
+ }
1130
+ }
1131
+ function createRedisCacheLockAdapter(options) {
1132
+ assertRedisClient(options.client);
1133
+ const namespace = normalizeNamespace(
1134
+ options.namespace ?? "bcp:{cache}"
1135
+ );
1136
+ const client = options.client;
1137
+ return {
1138
+ namespace,
1139
+ async acquire(rawKey, rawOwnerId, ttlMs) {
1140
+ const key = redisLockKey(
1141
+ namespace,
1142
+ normalizeKey(rawKey)
1143
+ );
1144
+ const ownerId = normalizeOwnerId(rawOwnerId);
1145
+ const ttl = positiveInteger(
1146
+ ttlMs,
1147
+ "lock ttlMs"
1148
+ );
1149
+ const result = redisString(
1150
+ await client.sendCommand([
1151
+ "SET",
1152
+ key,
1153
+ ownerId,
1154
+ "NX",
1155
+ "PX",
1156
+ String(ttl)
1157
+ ])
1158
+ );
1159
+ return result === "OK";
1160
+ },
1161
+ async extend(rawKey, rawOwnerId, ttlMs) {
1162
+ const result = await evalRedis(
1163
+ client,
1164
+ REDIS_LOCK_EXTEND_SCRIPT,
1165
+ [
1166
+ redisLockKey(
1167
+ namespace,
1168
+ normalizeKey(rawKey)
1169
+ )
1170
+ ],
1171
+ [
1172
+ normalizeOwnerId(rawOwnerId),
1173
+ String(
1174
+ positiveInteger(
1175
+ ttlMs,
1176
+ "lock ttlMs"
1177
+ )
1178
+ )
1179
+ ]
1180
+ );
1181
+ return Number(result) > 0;
1182
+ },
1183
+ async release(rawKey, rawOwnerId) {
1184
+ const result = await evalRedis(
1185
+ client,
1186
+ REDIS_LOCK_RELEASE_SCRIPT,
1187
+ [
1188
+ redisLockKey(
1189
+ namespace,
1190
+ normalizeKey(rawKey)
1191
+ )
1192
+ ],
1193
+ [
1194
+ normalizeOwnerId(rawOwnerId)
1195
+ ]
1196
+ );
1197
+ return Number(result) > 0;
1198
+ },
1199
+ async close() {
1200
+ await options.close?.();
1201
+ }
1202
+ };
1203
+ }
1204
+ function cloneEntry(entry) {
1205
+ return {
1206
+ ...entry,
1207
+ tags: [...entry.tags],
1208
+ paths: [...entry.paths]
1209
+ };
1210
+ }
1211
+ function isExpired(entry, now) {
1212
+ return entry.expiresAt !== null && entry.expiresAt <= now;
1213
+ }
1214
+ function normalizeExpiresAt(value) {
1215
+ if (value === void 0 || value === null) {
1216
+ return null;
1217
+ }
1218
+ assertFiniteTimestamp(value, "cache expiresAt");
1219
+ return value;
1220
+ }
1221
+ function normalizeKey(value) {
1222
+ const normalized = String(value ?? "").trim();
1223
+ if (!normalized) {
1224
+ throw new TypeError(
1225
+ "BCP Cache: key must be a non-empty string."
1226
+ );
1227
+ }
1228
+ return normalized;
1229
+ }
1230
+ function normalizeOwnerId(value) {
1231
+ const normalized = String(value ?? "").trim();
1232
+ if (!normalized) {
1233
+ throw new TypeError(
1234
+ "BCP Cache: lock ownerId must be a non-empty string."
1235
+ );
1236
+ }
1237
+ return normalized;
1238
+ }
1239
+ function normalizeTag2(value) {
1240
+ const normalized = String(value ?? "").trim();
1241
+ if (!normalized) {
1242
+ throw new TypeError(
1243
+ "BCP Cache: cache tags cannot be empty."
1244
+ );
1245
+ }
1246
+ return normalized;
1247
+ }
1248
+ function normalizeTags(values) {
1249
+ return Array.from(
1250
+ new Set(
1251
+ (values ?? []).map(normalizeTag2)
1252
+ )
1253
+ ).sort();
1254
+ }
1255
+ function normalizePath2(value) {
1256
+ let pathname = String(value ?? "").trim();
1257
+ if (!pathname) {
1258
+ return "/";
1259
+ }
1260
+ try {
1261
+ if (pathname.startsWith("http://") || pathname.startsWith("https://")) {
1262
+ pathname = new URL(pathname).pathname;
1263
+ }
1264
+ } catch {
1265
+ }
1266
+ pathname = pathname.split("?")[0].split("#")[0];
1267
+ if (!pathname.startsWith("/")) {
1268
+ pathname = `/${pathname}`;
1269
+ }
1270
+ if (pathname.length > 1 && pathname.endsWith("/")) {
1271
+ pathname = pathname.slice(0, -1);
1272
+ }
1273
+ return pathname;
1274
+ }
1275
+ function normalizePaths(values) {
1276
+ return Array.from(
1277
+ new Set(
1278
+ (values ?? []).map(normalizePath2)
1279
+ )
1280
+ ).sort();
1281
+ }
1282
+ function pathMatches2(cachedPath, invalidatedPath) {
1283
+ if (invalidatedPath === "/") {
1284
+ return true;
1285
+ }
1286
+ return cachedPath === invalidatedPath || cachedPath.startsWith(
1287
+ `${invalidatedPath}/`
1288
+ );
1289
+ }
1290
+ function positiveInteger(value, field) {
1291
+ if (!Number.isInteger(value) || value <= 0) {
1292
+ throw new TypeError(
1293
+ `BCP Cache: ${field} must be a positive integer.`
1294
+ );
1295
+ }
1296
+ return value;
1297
+ }
1298
+ function nonNegativeInteger(value, field) {
1299
+ if (!Number.isInteger(value) || value < 0) {
1300
+ throw new TypeError(
1301
+ `BCP Cache: ${field} must be a non-negative integer.`
1302
+ );
1303
+ }
1304
+ return value;
1305
+ }
1306
+ function assertFiniteTimestamp(value, field) {
1307
+ if (!Number.isFinite(value)) {
1308
+ throw new TypeError(
1309
+ `BCP Cache: ${field} must be a finite number.`
1310
+ );
1311
+ }
1312
+ }
1313
+ function sleep(ms) {
1314
+ return new Promise((resolve) => {
1315
+ setTimeout(resolve, ms);
1316
+ });
1317
+ }
1318
+ function normalizeMetricPrefix(value) {
1319
+ const normalized = value.trim();
1320
+ if (!/^[a-zA-Z_:][a-zA-Z0-9_:]*$/.test(normalized)) {
1321
+ throw new TypeError(
1322
+ "BCP Cache: metrics prefix contains unsupported characters."
1323
+ );
1324
+ }
1325
+ return normalized;
1326
+ }
1327
+ function normalizeNamespace(value) {
1328
+ const normalized = value.trim();
1329
+ if (!normalized) {
1330
+ throw new TypeError(
1331
+ "BCP Cache: Redis namespace must be a non-empty string."
1332
+ );
1333
+ }
1334
+ return normalized.replace(/:+$/g, "");
1335
+ }
1336
+ function encodeRedisPart(value) {
1337
+ return encodeURIComponent(value);
1338
+ }
1339
+ function redisEntryKey(namespace, key) {
1340
+ return `${namespace}:entry:${encodeRedisPart(key)}`;
1341
+ }
1342
+ function redisTagKey(namespace, tag) {
1343
+ return `${namespace}:tag:${encodeRedisPart(tag)}`;
1344
+ }
1345
+ function redisPathKey(namespace, pathname) {
1346
+ return `${namespace}:path:${encodeRedisPart(pathname)}`;
1347
+ }
1348
+ function redisAllKeysKey(namespace) {
1349
+ return `${namespace}:entries`;
1350
+ }
1351
+ function redisPathsIndexKey(namespace) {
1352
+ return `${namespace}:paths`;
1353
+ }
1354
+ function redisLockKey(namespace, key) {
1355
+ return `${namespace}:lock:${encodeRedisPart(key)}`;
1356
+ }
1357
+ function serializeValue(value, serialize) {
1358
+ const result = serialize(value);
1359
+ if (typeof result !== "string") {
1360
+ throw new TypeError(
1361
+ "BCP Cache: Redis serializer must return a string."
1362
+ );
1363
+ }
1364
+ return result;
1365
+ }
1366
+ function parseStoredRecord(value) {
1367
+ const parsed = JSON.parse(value);
1368
+ if (!parsed || typeof parsed.key !== "string" || typeof parsed.value !== "string" || !Array.isArray(parsed.tags) || !Array.isArray(parsed.paths) || !Array.isArray(parsed.tagIndexKeys) || !Array.isArray(parsed.pathIndexPairs) || typeof parsed.createdAt !== "number" || typeof parsed.updatedAt !== "number") {
1369
+ throw new Error(
1370
+ "BCP Cache: invalid Redis cache record."
1371
+ );
1372
+ }
1373
+ return parsed;
1374
+ }
1375
+ function assertRedisClient(client) {
1376
+ if (!client || typeof client.sendCommand !== "function") {
1377
+ throw new TypeError(
1378
+ "BCP Cache: Redis adapter requires a sendCommand client."
1379
+ );
1380
+ }
1381
+ }
1382
+ function redisString(value) {
1383
+ if (value === null || value === void 0) {
1384
+ return null;
1385
+ }
1386
+ if (typeof value === "string") {
1387
+ return value;
1388
+ }
1389
+ if (value instanceof Uint8Array) {
1390
+ return new TextDecoder().decode(value);
1391
+ }
1392
+ return String(value);
1393
+ }
1394
+ function redisStringArray(value) {
1395
+ if (!Array.isArray(value)) {
1396
+ return [];
1397
+ }
1398
+ return value.map(redisString).filter(
1399
+ (item) => item !== null
1400
+ );
1401
+ }
1402
+ async function evalRedis(client, script, keys, args) {
1403
+ return client.sendCommand([
1404
+ "EVAL",
1405
+ script,
1406
+ String(keys.length),
1407
+ ...keys,
1408
+ ...args
1409
+ ]);
1410
+ }
1411
+ var REDIS_SET_SCRIPT = `
1412
+ local old = redis.call('GET', KEYS[1])
1413
+ if old then
1414
+ local previous = cjson.decode(old)
1415
+ for _, tagKey in ipairs(previous.tagIndexKeys or {}) do
1416
+ redis.call('SREM', tagKey, KEYS[1])
1417
+ if redis.call('SCARD', tagKey) == 0 then redis.call('DEL', tagKey) end
1418
+ end
1419
+ for _, pair in ipairs(previous.pathIndexPairs or {}) do
1420
+ redis.call('SREM', pair.key, KEYS[1])
1421
+ if redis.call('SCARD', pair.key) == 0 then
1422
+ redis.call('DEL', pair.key)
1423
+ redis.call('SREM', KEYS[3], pair.path)
1424
+ end
1425
+ end
1426
+ end
1427
+ local record = cjson.decode(ARGV[1])
1428
+ if tonumber(ARGV[2]) > 0 then
1429
+ redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2])
1430
+ else
1431
+ redis.call('SET', KEYS[1], ARGV[1])
1432
+ end
1433
+ redis.call('SADD', KEYS[2], KEYS[1])
1434
+ for _, tagKey in ipairs(record.tagIndexKeys or {}) do
1435
+ redis.call('SADD', tagKey, KEYS[1])
1436
+ end
1437
+ for _, pair in ipairs(record.pathIndexPairs or {}) do
1438
+ redis.call('SADD', pair.key, KEYS[1])
1439
+ redis.call('SADD', KEYS[3], pair.path)
1440
+ end
1441
+ return 1
1442
+ `;
1443
+ var REDIS_DELETE_SCRIPT = `
1444
+ local raw = redis.call('GET', KEYS[1])
1445
+ if not raw then
1446
+ redis.call('SREM', KEYS[2], KEYS[1])
1447
+ return 0
1448
+ end
1449
+ local record = cjson.decode(raw)
1450
+ for _, tagKey in ipairs(record.tagIndexKeys or {}) do
1451
+ redis.call('SREM', tagKey, KEYS[1])
1452
+ if redis.call('SCARD', tagKey) == 0 then redis.call('DEL', tagKey) end
1453
+ end
1454
+ for _, pair in ipairs(record.pathIndexPairs or {}) do
1455
+ redis.call('SREM', pair.key, KEYS[1])
1456
+ if redis.call('SCARD', pair.key) == 0 then
1457
+ redis.call('DEL', pair.key)
1458
+ redis.call('SREM', KEYS[3], pair.path)
1459
+ end
1460
+ end
1461
+ redis.call('DEL', KEYS[1])
1462
+ redis.call('SREM', KEYS[2], KEYS[1])
1463
+ return 1
1464
+ `;
1465
+ var REDIS_INVALIDATE_SET_SCRIPT = `
1466
+ local members = redis.call('SMEMBERS', KEYS[1])
1467
+ local removed = 0
1468
+ for _, entryKey in ipairs(members) do
1469
+ local raw = redis.call('GET', entryKey)
1470
+ if raw then
1471
+ local record = cjson.decode(raw)
1472
+ for _, tagKey in ipairs(record.tagIndexKeys or {}) do
1473
+ redis.call('SREM', tagKey, entryKey)
1474
+ if redis.call('SCARD', tagKey) == 0 then redis.call('DEL', tagKey) end
1475
+ end
1476
+ for _, pair in ipairs(record.pathIndexPairs or {}) do
1477
+ redis.call('SREM', pair.key, entryKey)
1478
+ if redis.call('SCARD', pair.key) == 0 then
1479
+ redis.call('DEL', pair.key)
1480
+ redis.call('SREM', KEYS[3], pair.path)
1481
+ end
1482
+ end
1483
+ redis.call('DEL', entryKey)
1484
+ redis.call('SREM', KEYS[2], entryKey)
1485
+ removed = removed + 1
1486
+ else
1487
+ redis.call('SREM', KEYS[2], entryKey)
1488
+ end
1489
+ end
1490
+ redis.call('DEL', KEYS[1])
1491
+ return removed
1492
+ `;
1493
+ var REDIS_INVALIDATE_PATHS_SCRIPT = `
1494
+ local pairs = cjson.decode(ARGV[1])
1495
+ local seen = {}
1496
+ local removed = 0
1497
+ for _, pair in ipairs(pairs) do
1498
+ local members = redis.call('SMEMBERS', pair.key)
1499
+ for _, entryKey in ipairs(members) do
1500
+ if not seen[entryKey] then
1501
+ seen[entryKey] = true
1502
+ local raw = redis.call('GET', entryKey)
1503
+ if raw then
1504
+ local record = cjson.decode(raw)
1505
+ for _, tagKey in ipairs(record.tagIndexKeys or {}) do
1506
+ redis.call('SREM', tagKey, entryKey)
1507
+ if redis.call('SCARD', tagKey) == 0 then redis.call('DEL', tagKey) end
1508
+ end
1509
+ for _, pathPair in ipairs(record.pathIndexPairs or {}) do
1510
+ redis.call('SREM', pathPair.key, entryKey)
1511
+ if redis.call('SCARD', pathPair.key) == 0 then
1512
+ redis.call('DEL', pathPair.key)
1513
+ redis.call('SREM', KEYS[2], pathPair.path)
1514
+ end
1515
+ end
1516
+ redis.call('DEL', entryKey)
1517
+ redis.call('SREM', KEYS[1], entryKey)
1518
+ removed = removed + 1
1519
+ else
1520
+ redis.call('SREM', KEYS[1], entryKey)
1521
+ end
1522
+ end
1523
+ end
1524
+ redis.call('DEL', pair.key)
1525
+ redis.call('SREM', KEYS[2], pair.path)
1526
+ end
1527
+ return removed
1528
+ `;
1529
+ var REDIS_LOCK_RELEASE_SCRIPT = `
1530
+ if redis.call('GET', KEYS[1]) == ARGV[1] then
1531
+ return redis.call('DEL', KEYS[1])
1532
+ end
1533
+ return 0
1534
+ `;
1535
+ var REDIS_LOCK_EXTEND_SCRIPT = `
1536
+ if redis.call('GET', KEYS[1]) == ARGV[1] then
1537
+ return redis.call('PEXPIRE', KEYS[1], ARGV[2])
1538
+ end
1539
+ return 0
1540
+ `;
1541
+ export {
1542
+ cache,
1543
+ clearCache,
1544
+ createCacheMetrics,
1545
+ createCacheStore,
1546
+ createMemoryCacheAdapter,
1547
+ createMemoryCacheLockAdapter,
1548
+ createRedisCacheAdapter,
1549
+ createRedisCacheLockAdapter,
1550
+ dedupe,
1551
+ getCacheStats,
1552
+ revalidatePath,
1553
+ revalidateTag
1554
+ };