raffel 1.1.73 → 1.1.74

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 (62) hide show
  1. package/dist/cache/fs-layer.d.ts.map +1 -1
  2. package/dist/cache/fs-layer.js +25 -2
  3. package/dist/cache/fs-layer.js.map +1 -1
  4. package/dist/cache/index.d.ts +5 -5
  5. package/dist/cache/index.d.ts.map +1 -1
  6. package/dist/cache/index.js +2 -2
  7. package/dist/cache/index.js.map +1 -1
  8. package/dist/cache/key.d.ts +28 -7
  9. package/dist/cache/key.d.ts.map +1 -1
  10. package/dist/cache/key.js +191 -18
  11. package/dist/cache/key.js.map +1 -1
  12. package/dist/cache/redis-layer.d.ts +6 -0
  13. package/dist/cache/redis-layer.d.ts.map +1 -1
  14. package/dist/cache/redis-layer.js +150 -33
  15. package/dist/cache/redis-layer.js.map +1 -1
  16. package/dist/cache/server-runtime.d.ts +22 -9
  17. package/dist/cache/server-runtime.d.ts.map +1 -1
  18. package/dist/cache/server-runtime.js +190 -22
  19. package/dist/cache/server-runtime.js.map +1 -1
  20. package/dist/cache/tiered.d.ts +43 -2
  21. package/dist/cache/tiered.d.ts.map +1 -1
  22. package/dist/cache/tiered.js +457 -136
  23. package/dist/cache/tiered.js.map +1 -1
  24. package/dist/http/cache.d.ts +10 -1
  25. package/dist/http/cache.d.ts.map +1 -1
  26. package/dist/http/cache.js +105 -37
  27. package/dist/http/cache.js.map +1 -1
  28. package/dist/index.d.ts +2 -2
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +1 -1
  31. package/dist/index.js.map +1 -1
  32. package/dist/middleware/auth/oauth2-client-credentials.d.ts.map +1 -1
  33. package/dist/middleware/auth/oauth2-client-credentials.js +3 -0
  34. package/dist/middleware/auth/oauth2-client-credentials.js.map +1 -1
  35. package/dist/middleware/auth/oauth2.d.ts.map +1 -1
  36. package/dist/middleware/auth/oauth2.js +8 -0
  37. package/dist/middleware/auth/oauth2.js.map +1 -1
  38. package/dist/middleware/auth.d.ts +2 -0
  39. package/dist/middleware/auth.d.ts.map +1 -1
  40. package/dist/middleware/auth.js +62 -0
  41. package/dist/middleware/auth.js.map +1 -1
  42. package/dist/server/builder.d.ts.map +1 -1
  43. package/dist/server/builder.js +1 -0
  44. package/dist/server/builder.js.map +1 -1
  45. package/dist/server/fs-routes/middleware-processor.d.ts.map +1 -1
  46. package/dist/server/fs-routes/middleware-processor.js +15 -0
  47. package/dist/server/fs-routes/middleware-processor.js.map +1 -1
  48. package/dist/types/context.d.ts +2 -0
  49. package/dist/types/context.d.ts.map +1 -1
  50. package/dist/types/context.js +3 -0
  51. package/dist/types/context.js.map +1 -1
  52. package/dist/ui/cache/fs-layer.d.ts.map +1 -1
  53. package/dist/ui/cache/key.d.ts +28 -7
  54. package/dist/ui/cache/key.d.ts.map +1 -1
  55. package/dist/ui/cache/server-runtime.d.ts +22 -9
  56. package/dist/ui/cache/server-runtime.d.ts.map +1 -1
  57. package/dist/ui/cache/tiered.d.ts +43 -2
  58. package/dist/ui/cache/tiered.d.ts.map +1 -1
  59. package/dist/ui/server/fs-routes/middleware-processor.d.ts.map +1 -1
  60. package/dist/ui/types/context.d.ts +2 -0
  61. package/dist/ui/types/context.d.ts.map +1 -1
  62. package/package.json +1 -1
@@ -10,8 +10,20 @@ export function isCacheRecord(value) {
10
10
  && Number.isFinite(record.version)
11
11
  && (record.staleUntil === undefined || Number.isFinite(record.staleUntil))
12
12
  && (record.sizeBytes === undefined || Number.isFinite(record.sizeBytes))
13
+ && (record.tags === undefined || (Array.isArray(record.tags) && record.tags.every((tag) => typeof tag === 'string')))
13
14
  && 'value' in record;
14
15
  }
16
+ /** Encode and frame a logical namespace for collision-free physical key prefixes. */
17
+ export function cacheNamespacePrefix(namespace) {
18
+ if (!namespace)
19
+ throw new Error('Cache namespace cannot be empty');
20
+ try {
21
+ return `${encodeURIComponent(namespace)}:`;
22
+ }
23
+ catch {
24
+ throw new Error('Cache namespace must contain valid Unicode');
25
+ }
26
+ }
15
27
  function layerDuration(configured, layer, fallback) {
16
28
  if (typeof configured === 'number')
17
29
  return configured;
@@ -71,6 +83,8 @@ export function createMemoryCacheLayer(options) {
71
83
  throw new Error('Memory cache expirationResolutionMs must be greater than zero');
72
84
  }
73
85
  const entries = new Map();
86
+ const entrySizes = new Map();
87
+ const tagIndex = new Map();
74
88
  const maxEntries = options.maxEntries ?? DEFAULT_L1_MAX_ENTRIES;
75
89
  const maxMemoryBytes = options.maxMemoryBytes ?? DEFAULT_L1_MAX_MEMORY_BYTES;
76
90
  let currentMemoryBytes = 0;
@@ -82,9 +96,17 @@ export function createMemoryCacheLayer(options) {
82
96
  });
83
97
  function remove(key) {
84
98
  const record = entries.get(key);
85
- if (record)
86
- currentMemoryBytes -= record.sizeBytes ?? 0;
99
+ if (record) {
100
+ currentMemoryBytes -= entrySizes.get(key) ?? 0;
101
+ for (const tag of record.tags ?? []) {
102
+ const keys = tagIndex.get(tag);
103
+ keys?.delete(key);
104
+ if (keys?.size === 0)
105
+ tagIndex.delete(tag);
106
+ }
107
+ }
87
108
  entries.delete(key);
109
+ entrySizes.delete(key);
88
110
  wheel.cancel(key);
89
111
  }
90
112
  function evictUntilFits(incomingBytes) {
@@ -101,6 +123,10 @@ export function createMemoryCacheLayer(options) {
101
123
  }
102
124
  return {
103
125
  id: options.id,
126
+ capabilities: {
127
+ prefixInvalidation: 'scan',
128
+ tagInvalidation: 'indexed',
129
+ },
104
130
  synchronous: true,
105
131
  ttlMs: options.ttlMs,
106
132
  get(key) {
@@ -122,20 +148,32 @@ export function createMemoryCacheLayer(options) {
122
148
  return record;
123
149
  },
124
150
  set(key, record, ttlMs, staleMs = 0) {
125
- const sizeBytes = record.sizeBytes ?? logicalSize(record.value);
126
- if (sizeBytes === undefined)
151
+ const valueSizeBytes = record.sizeBytes ?? logicalSize(record.value);
152
+ if (valueSizeBytes === undefined)
127
153
  return;
154
+ const tags = [...new Set((record.tags ?? []).map((tag) => tag.trim()).filter(Boolean))];
155
+ const sizeBytes = valueSizeBytes + tags.reduce((total, tag) => total + Buffer.byteLength(tag) + 16, 0);
128
156
  remove(key);
129
157
  if (!evictUntilFits(sizeBytes))
130
158
  return;
131
159
  const expiresAt = Date.now() + ttlMs;
132
160
  const stored = {
133
161
  ...record,
134
- sizeBytes,
162
+ sizeBytes: valueSizeBytes,
163
+ tags: tags.length > 0 ? tags : undefined,
135
164
  expiresAt,
136
165
  staleUntil: staleMs > 0 ? expiresAt + staleMs : undefined,
137
166
  };
138
167
  entries.set(key, stored);
168
+ entrySizes.set(key, sizeBytes);
169
+ for (const tag of stored.tags ?? []) {
170
+ let keys = tagIndex.get(tag);
171
+ if (!keys) {
172
+ keys = new Set();
173
+ tagIndex.set(tag, keys);
174
+ }
175
+ keys.add(key);
176
+ }
139
177
  currentMemoryBytes += sizeBytes;
140
178
  wheel.schedule(key, stored.staleUntil ?? stored.expiresAt, stored.version);
141
179
  },
@@ -143,11 +181,27 @@ export function createMemoryCacheLayer(options) {
143
181
  remove(key);
144
182
  },
145
183
  clearNamespace(namespace) {
184
+ const prefix = cacheNamespacePrefix(namespace);
146
185
  for (const key of entries.keys()) {
147
- if (key.startsWith(`${namespace}:`))
186
+ if (key.startsWith(prefix))
148
187
  remove(key);
149
188
  }
150
189
  },
190
+ invalidateTag(tag, namespace) {
191
+ const prefix = cacheNamespacePrefix(namespace);
192
+ const keys = [...(tagIndex.get(tag) ?? [])]
193
+ .filter((key) => key.startsWith(prefix));
194
+ for (const key of keys)
195
+ remove(key);
196
+ return { mode: 'physical', deleted: keys.length };
197
+ },
198
+ invalidatePrefix(logicalPrefix, namespace) {
199
+ const prefix = cacheNamespacePrefix(namespace) + logicalPrefix;
200
+ const keys = [...entries.keys()].filter((key) => key.startsWith(prefix));
201
+ for (const key of keys)
202
+ remove(key);
203
+ return { mode: 'physical', deleted: keys.length };
204
+ },
151
205
  stats() {
152
206
  return { totalItems: entries.size, memoryUsageBytes: currentMemoryBytes, hits, misses };
153
207
  },
@@ -165,20 +219,32 @@ export function createTieredCache(options) {
165
219
  if (options.operationTimeoutMs !== undefined && options.operationTimeoutMs <= 0) {
166
220
  throw new Error('Tiered cache operationTimeoutMs must be greater than zero');
167
221
  }
222
+ const configuredLayerIds = new Set();
168
223
  for (const layer of options.layers) {
224
+ if (configuredLayerIds.has(layer.id)) {
225
+ throw new Error(`Cache layer id "${layer.id}" is duplicated`);
226
+ }
227
+ configuredLayerIds.add(layer.id);
169
228
  if (layer.readTimeoutMs !== undefined && layer.readTimeoutMs <= 0) {
170
229
  throw new Error(`Cache layer "${layer.id}" readTimeoutMs must be greater than zero`);
171
230
  }
172
231
  if (layer.operationTimeoutMs !== undefined && layer.operationTimeoutMs <= 0) {
173
232
  throw new Error(`Cache layer "${layer.id}" operationTimeoutMs must be greater than zero`);
174
233
  }
234
+ if (layer.capabilities?.distributedFillFencing &&
235
+ !(layer.beginFill && layer.isFillCurrent && layer.commitFill && layer.bumpGeneration)) {
236
+ throw new Error(`Cache layer "${layer.id}" distributed fill fencing contract is incomplete`);
237
+ }
175
238
  }
176
- const prefix = `${options.namespace}:`;
239
+ const prefix = cacheNamespacePrefix(options.namespace);
177
240
  const layers = [...options.layers];
178
- const writeQueues = layers.slice(1).map((layer) => new WriteBehindQueue({
241
+ const writeQueues = layers.map((layer) => new WriteBehindQueue({
179
242
  ...options.writeBehind,
180
243
  ...layer.writeBehind,
181
244
  }));
245
+ const allLayerIndexes = layers.map((_layer, index) => index);
246
+ const layerIndexes = new Map(layers.map((layer, index) => [layer.id, index]));
247
+ const ticketOwner = {};
182
248
  let version = 0;
183
249
  let namespaceEpoch = 0;
184
250
  let clearInProgress;
@@ -307,6 +373,11 @@ export function createTieredCache(options) {
307
373
  releaseKey();
308
374
  });
309
375
  }
376
+ function runOrderedWrite(index, key, task) {
377
+ return layers[index].synchronous
378
+ ? trackForegroundWrite(key, task)
379
+ : writeQueues[index].enqueueBarrier(key, task);
380
+ }
310
381
  async function writeLayer(index, key, record, ttlMs, staleMs, capturedNamespaceEpoch, capturedKeyEpoch, capturedWriteVersion) {
311
382
  if (isLayerFenced(index, key))
312
383
  return;
@@ -338,6 +409,34 @@ export function createTieredCache(options) {
338
409
  };
339
410
  await withDeadline(layer.set(key, record, ttlMs, staleMs), layer.operationTimeoutMs ?? options.operationTimeoutMs ?? DEFAULT_CACHE_OPERATION_TIMEOUT_MS, `Cache layer "${layer.id}" set timed out`, () => { releaseFence = addLayerFence(index, key); }, cleanupLateWrite);
340
411
  }
412
+ async function commitLayerFill(index, key, record, ttlMs, staleMs, token) {
413
+ const layer = layers[index];
414
+ if (isLayerFenced(index, key))
415
+ return false;
416
+ return withDeadline(layer.commitFill(key, record, ttlMs, staleMs, token, options.namespace), layer.operationTimeoutMs ?? options.operationTimeoutMs ?? DEFAULT_CACHE_OPERATION_TIMEOUT_MS, `Cache layer "${layer.id}" fill commit timed out`);
417
+ }
418
+ function usesDistributedFillFencing(layer) {
419
+ return Boolean(layer.capabilities?.distributedFillFencing &&
420
+ layer.beginFill &&
421
+ layer.isFillCurrent &&
422
+ layer.commitFill &&
423
+ layer.bumpGeneration);
424
+ }
425
+ async function bumpLayerGenerations(operation) {
426
+ await Promise.all(layers.map(async (layer, index) => {
427
+ if (!usesDistributedFillFencing(layer))
428
+ return;
429
+ try {
430
+ await withDeadline(layer.bumpGeneration(options.namespace), layer.operationTimeoutMs ?? options.operationTimeoutMs ??
431
+ DEFAULT_CACHE_OPERATION_TIMEOUT_MS, `Cache layer "${layer.id}" generation update timed out`);
432
+ }
433
+ catch (error) {
434
+ markLayerFailure(index);
435
+ options.onLayerError?.(layer.id, operation, error);
436
+ throw error;
437
+ }
438
+ }));
439
+ }
341
440
  async function deleteLayer(index, key) {
342
441
  const layer = layers[index];
343
442
  let releaseFence;
@@ -380,6 +479,22 @@ export function createTieredCache(options) {
380
479
  throw error;
381
480
  }
382
481
  }
482
+ async function invalidateLayerTag(index, tag) {
483
+ const layer = layers[index];
484
+ if (!layer.invalidateTag) {
485
+ throw new Error(`Cache layer "${layer.id}" does not support tag invalidation`);
486
+ }
487
+ return withDeadline(layer.invalidateTag(tag, options.namespace), layer.operationTimeoutMs ?? options.operationTimeoutMs ??
488
+ DEFAULT_CACHE_OPERATION_TIMEOUT_MS, `Cache layer "${layer.id}" tag invalidation timed out`);
489
+ }
490
+ async function invalidateLayerPrefix(index, logicalPrefix) {
491
+ const layer = layers[index];
492
+ if (!layer.invalidatePrefix) {
493
+ throw new Error(`Cache layer "${layer.id}" does not support prefix invalidation`);
494
+ }
495
+ return withDeadline(layer.invalidatePrefix(logicalPrefix, options.namespace), layer.operationTimeoutMs ?? options.operationTimeoutMs ??
496
+ DEFAULT_CACHE_OPERATION_TIMEOUT_MS, `Cache layer "${layer.id}" prefix invalidation timed out`);
497
+ }
383
498
  function guardedRead(layer, key) {
384
499
  const health = readHealth.get(layer) ?? { failures: 0, openUntil: 0 };
385
500
  readHealth.set(layer, health);
@@ -411,143 +526,322 @@ export function createTieredCache(options) {
411
526
  return markFailure(error);
412
527
  }
413
528
  }
414
- return {
415
- async get(key) {
416
- const namespacedKey = prefix + key;
417
- let releaseKey;
418
- const ensureRetained = () => {
419
- releaseKey ??= retainKey(namespacedKey);
420
- };
421
- try {
422
- if (clearInProgress)
423
- await clearInProgress.catch(() => undefined);
424
- const deletion = deletions.get(namespacedKey);
425
- if (deletion)
426
- await deletion.catch(() => undefined);
427
- const capturedNamespaceEpoch = namespaceEpoch;
428
- const capturedKeyEpoch = keyEpochs.get(namespacedKey) ?? 0;
429
- const capturedWriteVersion = keyWriteVersions.get(namespacedKey);
430
- const readIsCurrent = () => namespaceEpoch === capturedNamespaceEpoch &&
431
- (keyEpochs.get(namespacedKey) ?? 0) === capturedKeyEpoch &&
432
- keyWriteVersions.get(namespacedKey) === capturedWriteVersion;
433
- let staleHit;
434
- for (let index = 0; index < layers.length; index++) {
435
- const layer = layers[index];
436
- if (isLayerFenced(index, namespacedKey))
437
- continue;
438
- let record;
529
+ async function getFrom(key, selectedIndexes) {
530
+ const namespacedKey = prefix + key;
531
+ let releaseKey;
532
+ const ensureRetained = () => {
533
+ releaseKey ??= retainKey(namespacedKey);
534
+ };
535
+ try {
536
+ if (clearInProgress)
537
+ await clearInProgress.catch(() => undefined);
538
+ const deletion = deletions.get(namespacedKey);
539
+ if (deletion)
540
+ await deletion.catch(() => undefined);
541
+ const capturedNamespaceEpoch = namespaceEpoch;
542
+ const capturedKeyEpoch = keyEpochs.get(namespacedKey) ?? 0;
543
+ const capturedWriteVersion = keyWriteVersions.get(namespacedKey);
544
+ const readIsCurrent = () => namespaceEpoch === capturedNamespaceEpoch &&
545
+ (keyEpochs.get(namespacedKey) ?? 0) === capturedKeyEpoch &&
546
+ keyWriteVersions.get(namespacedKey) === capturedWriteVersion;
547
+ let staleHit;
548
+ for (let position = 0; position < selectedIndexes.length; position++) {
549
+ const index = selectedIndexes[position];
550
+ const layer = layers[index];
551
+ if (isLayerFenced(index, namespacedKey))
552
+ continue;
553
+ let record;
554
+ try {
555
+ if (layer.synchronous) {
556
+ record = layer.get(namespacedKey);
557
+ }
558
+ else {
559
+ ensureRetained();
560
+ const candidate = guardedRead(layer, namespacedKey);
561
+ record = isThenable(candidate) ? await candidate : candidate;
562
+ }
563
+ }
564
+ catch (error) {
565
+ options.onLayerError?.(layer.id, 'get', error);
566
+ continue;
567
+ }
568
+ if (!readIsCurrent())
569
+ return undefined;
570
+ if (!record)
571
+ continue;
572
+ const now = Date.now();
573
+ if (now >= (record.staleUntil ?? record.expiresAt)) {
574
+ ensureRetained();
439
575
  try {
440
- if (layer.synchronous) {
441
- record = layer.get(namespacedKey);
442
- }
443
- else {
444
- ensureRetained();
445
- const candidate = guardedRead(layer, namespacedKey);
446
- record = isThenable(candidate) ? await candidate : candidate;
447
- }
576
+ await deleteLayer(index, namespacedKey);
448
577
  }
449
578
  catch (error) {
450
- options.onLayerError?.(layer.id, 'get', error);
451
- continue;
579
+ options.onLayerError?.(layer.id, 'delete', error);
452
580
  }
453
- if (!readIsCurrent())
454
- return undefined;
455
- if (!record)
456
- continue;
457
- const now = Date.now();
458
- if (now >= (record.staleUntil ?? record.expiresAt)) {
459
- ensureRetained();
460
- try {
461
- await deleteLayer(index, namespacedKey);
462
- }
463
- catch (error) {
464
- options.onLayerError?.(layer.id, 'delete', error);
581
+ continue;
582
+ }
583
+ const stale = now >= record.expiresAt && now < (record.staleUntil ?? record.expiresAt);
584
+ if (stale) {
585
+ staleHit ??= { value: record.value, layer: layer.id, stale: true };
586
+ continue;
587
+ }
588
+ if (position > 0) {
589
+ await Promise.all(selectedIndexes.slice(0, position).map(async (upperIndex) => {
590
+ const upper = layers[upperIndex];
591
+ await runOrderedWrite(upperIndex, namespacedKey, async () => {
592
+ if (!readIsCurrent())
593
+ return;
594
+ try {
595
+ await writeLayer(upperIndex, namespacedKey, record, upper.ttlMs ?? 60_000, 0, capturedNamespaceEpoch, capturedKeyEpoch, capturedWriteVersion);
596
+ }
597
+ catch (error) {
598
+ options.onLayerError?.(upper.id, 'set', error);
599
+ }
600
+ });
601
+ }));
602
+ }
603
+ if (!readIsCurrent())
604
+ return undefined;
605
+ return { value: record.value, layer: layer.id, stale: false };
606
+ }
607
+ return readIsCurrent() ? staleHit : undefined;
608
+ }
609
+ finally {
610
+ releaseKey?.();
611
+ }
612
+ }
613
+ async function setTo(key, value, writeOptions, selectedIndexes, fillTicket) {
614
+ if (clearInProgress)
615
+ await clearInProgress.catch(() => undefined);
616
+ const namespacedKey = prefix + key;
617
+ const deletion = deletions.get(namespacedKey);
618
+ if (deletion)
619
+ await deletion.catch(() => undefined);
620
+ const releaseKey = retainKey(namespacedKey);
621
+ try {
622
+ const capturedNamespaceEpoch = namespaceEpoch;
623
+ const capturedKeyEpoch = keyEpochs.get(namespacedKey) ?? 0;
624
+ const now = Date.now();
625
+ const sizeBytes = logicalSize(value);
626
+ if (sizeBytes === undefined)
627
+ return false;
628
+ const tags = [...new Set((writeOptions.tags ?? []).map((tag) => tag.trim()).filter(Boolean))];
629
+ const record = {
630
+ value,
631
+ tags: tags.length > 0 ? tags : undefined,
632
+ sizeBytes,
633
+ createdAt: now,
634
+ expiresAt: now,
635
+ version: ++version,
636
+ };
637
+ let allAccepted = true;
638
+ keyWriteVersions.set(namespacedKey, record.version);
639
+ const fencedIndexes = fillTicket
640
+ ? selectedIndexes.filter((index) => usesDistributedFillFencing(layers[index]))
641
+ : [];
642
+ for (const index of fencedIndexes) {
643
+ const layer = layers[index];
644
+ const releaseQueuedKey = retainKey(namespacedKey);
645
+ const write = async () => {
646
+ try {
647
+ const ttlMs = layerDuration(writeOptions.ttlMs, layer, layer.ttlMs ?? 60_000);
648
+ const staleMs = layerDuration(writeOptions.staleMs, layer, 0);
649
+ if (!fillTicket.layerTokens.has(index)) {
650
+ allAccepted = false;
651
+ return;
465
652
  }
466
- continue;
467
- }
468
- const stale = now >= record.expiresAt && now < (record.staleUntil ?? record.expiresAt);
469
- if (stale) {
470
- staleHit ??= { value: record.value, layer: layer.id, stale: true };
471
- continue;
653
+ allAccepted = await commitLayerFill(index, namespacedKey, record, ttlMs, staleMs, fillTicket.layerTokens.get(index));
472
654
  }
473
- if (index > 0) {
474
- await Promise.all(layers.slice(0, index).map(async (upper, upperIndex) => {
475
- await trackForegroundWrite(namespacedKey, async () => {
476
- if (!readIsCurrent())
477
- return;
478
- try {
479
- await writeLayer(upperIndex, namespacedKey, record, upper.ttlMs ?? 60_000, 0, capturedNamespaceEpoch, capturedKeyEpoch, capturedWriteVersion);
480
- }
481
- catch (error) {
482
- options.onLayerError?.(upper.id, 'set', error);
483
- }
484
- });
485
- }));
655
+ catch (error) {
656
+ options.onLayerError?.(layer.id, 'commitFill', error);
657
+ allAccepted = false;
486
658
  }
487
- if (!readIsCurrent())
488
- return undefined;
489
- return { value: record.value, layer: layer.id, stale: false };
659
+ };
660
+ try {
661
+ await writeQueues[index].enqueueBarrier(namespacedKey, write);
490
662
  }
491
- return readIsCurrent() ? staleHit : undefined;
492
- }
493
- finally {
494
- releaseKey?.();
663
+ finally {
664
+ releaseQueuedKey();
665
+ }
666
+ if (!allAccepted)
667
+ return false;
495
668
  }
496
- },
497
- async set(key, value, writeOptions = {}) {
498
- if (clearInProgress)
499
- await clearInProgress.catch(() => undefined);
500
- const namespacedKey = prefix + key;
501
- const deletion = deletions.get(namespacedKey);
502
- if (deletion)
503
- await deletion.catch(() => undefined);
504
- const releaseKey = retainKey(namespacedKey);
505
- try {
506
- const capturedNamespaceEpoch = namespaceEpoch;
507
- const capturedKeyEpoch = keyEpochs.get(namespacedKey) ?? 0;
508
- const now = Date.now();
509
- const sizeBytes = logicalSize(value);
510
- if (sizeBytes === undefined)
511
- return;
512
- const record = {
513
- value,
514
- sizeBytes,
515
- createdAt: now,
516
- expiresAt: now,
517
- version: ++version,
669
+ if (namespaceEpoch !== capturedNamespaceEpoch ||
670
+ (keyEpochs.get(namespacedKey) ?? 0) !== capturedKeyEpoch ||
671
+ keyWriteVersions.get(namespacedKey) !== record.version)
672
+ return false;
673
+ for (const [position, index] of selectedIndexes.entries()) {
674
+ const layer = layers[index];
675
+ if (fencedIndexes.includes(index))
676
+ continue;
677
+ const write = async () => {
678
+ if (namespaceEpoch !== capturedNamespaceEpoch ||
679
+ (keyEpochs.get(namespacedKey) ?? 0) !== capturedKeyEpoch ||
680
+ keyWriteVersions.get(namespacedKey) !== record.version)
681
+ return;
682
+ try {
683
+ const ttlMs = layerDuration(writeOptions.ttlMs, layer, layer.ttlMs ?? 60_000);
684
+ const staleMs = layerDuration(writeOptions.staleMs, layer, 0);
685
+ await writeLayer(index, namespacedKey, record, ttlMs, staleMs, capturedNamespaceEpoch, capturedKeyEpoch, record.version);
686
+ }
687
+ catch (error) {
688
+ options.onLayerError?.(layer.id, 'set', error);
689
+ }
518
690
  };
519
- keyWriteVersions.set(namespacedKey, record.version);
520
- const first = layers[0];
521
- if (first) {
522
- await trackForegroundWrite(namespacedKey, async () => {
523
- try {
524
- await writeLayer(0, namespacedKey, record, layerDuration(writeOptions.ttlMs, first, first.ttlMs ?? 60_000), layerDuration(writeOptions.staleMs, first, 0), capturedNamespaceEpoch, capturedKeyEpoch, record.version);
525
- }
526
- catch (error) {
527
- options.onLayerError?.(first.id, 'set', error);
528
- }
529
- });
691
+ if (position === 0) {
692
+ await runOrderedWrite(index, namespacedKey, write);
530
693
  }
531
- for (let index = 1; index < layers.length; index++) {
532
- const layer = layers[index];
694
+ else {
533
695
  const releaseQueuedKey = retainKey(namespacedKey);
534
- writeQueues[index - 1].enqueue(namespacedKey, async () => {
535
- if (namespaceEpoch !== capturedNamespaceEpoch ||
536
- (keyEpochs.get(namespacedKey) ?? 0) !== capturedKeyEpoch ||
537
- keyWriteVersions.get(namespacedKey) !== record.version)
538
- return;
539
- try {
540
- await writeLayer(index, namespacedKey, record, layerDuration(writeOptions.ttlMs, layer, layer.ttlMs ?? 60_000), layerDuration(writeOptions.staleMs, layer, 0), capturedNamespaceEpoch, capturedKeyEpoch, record.version);
541
- }
542
- catch (error) {
543
- options.onLayerError?.(layer.id, 'set', error);
544
- }
545
- }, releaseQueuedKey);
696
+ writeQueues[index].enqueue(namespacedKey, write, releaseQueuedKey);
546
697
  }
547
698
  }
548
- finally {
549
- releaseKey();
699
+ return allAccepted;
700
+ }
701
+ finally {
702
+ releaseKey();
703
+ }
704
+ }
705
+ function beginFillFor(key, selectedIndexes) {
706
+ const namespacedKey = prefix + key;
707
+ const ticket = {
708
+ owner: ticketOwner,
709
+ key,
710
+ namespacedKey,
711
+ namespaceEpoch,
712
+ keyEpoch: keyEpochs.get(namespacedKey) ?? 0,
713
+ selectedIndexes,
714
+ layerTokens: new Map(),
715
+ captureFailed: false,
716
+ release: retainKey(namespacedKey),
717
+ finished: false,
718
+ ready: Promise.resolve(),
719
+ };
720
+ ticket.ready = Promise.all(selectedIndexes.map(async (index) => {
721
+ const layer = layers[index];
722
+ if (!usesDistributedFillFencing(layer))
723
+ return;
724
+ try {
725
+ const token = await withDeadline(layer.beginFill(namespacedKey, options.namespace), layer.operationTimeoutMs ?? options.operationTimeoutMs ??
726
+ DEFAULT_CACHE_OPERATION_TIMEOUT_MS, `Cache layer "${layer.id}" fill capture timed out`);
727
+ ticket.layerTokens.set(index, token);
550
728
  }
729
+ catch (error) {
730
+ ticket.captureFailed = true;
731
+ options.onLayerError?.(layer.id, 'beginFill', error);
732
+ }
733
+ })).then(() => undefined);
734
+ return ticket;
735
+ }
736
+ function finishFill(ticket) {
737
+ const internal = ticket;
738
+ if (internal.owner !== ticketOwner) {
739
+ throw new Error('Cache fill ticket belongs to a different cache');
740
+ }
741
+ if (!internal.finished) {
742
+ internal.finished = true;
743
+ internal.release();
744
+ }
745
+ return internal;
746
+ }
747
+ function isLocallyCurrent(internal) {
748
+ return !internal.finished && !internal.captureFailed &&
749
+ namespaceEpoch === internal.namespaceEpoch &&
750
+ (keyEpochs.get(internal.namespacedKey) ?? 0) === internal.keyEpoch;
751
+ }
752
+ async function isFillCurrent(ticket) {
753
+ const internal = ticket;
754
+ if (internal.owner !== ticketOwner) {
755
+ throw new Error('Cache fill ticket belongs to a different cache');
756
+ }
757
+ await internal.ready;
758
+ if (!isLocallyCurrent(internal))
759
+ return false;
760
+ for (const [index, token] of internal.layerTokens) {
761
+ const layer = layers[index];
762
+ if (!layer.isFillCurrent)
763
+ continue;
764
+ try {
765
+ const current = await withDeadline(layer.isFillCurrent(token, options.namespace), layer.readTimeoutMs ?? options.readTimeoutMs ?? DEFAULT_CACHE_READ_TIMEOUT_MS, `Cache layer "${layer.id}" fill validation timed out`);
766
+ if (!current)
767
+ return false;
768
+ }
769
+ catch (error) {
770
+ options.onLayerError?.(layer.id, 'beginFill', error);
771
+ return false;
772
+ }
773
+ }
774
+ return true;
775
+ }
776
+ async function commitFill(ticket, value, writeOptions) {
777
+ const internal = ticket;
778
+ try {
779
+ if (internal.owner !== ticketOwner) {
780
+ throw new Error('Cache fill ticket belongs to a different cache');
781
+ }
782
+ await internal.ready;
783
+ if (!isLocallyCurrent(internal))
784
+ return false;
785
+ return setTo(internal.key, value, writeOptions, internal.selectedIndexes, internal);
786
+ }
787
+ finally {
788
+ finishFill(ticket);
789
+ }
790
+ }
791
+ async function invalidateAcrossLayers(operation, invalidate) {
792
+ namespaceEpoch++;
793
+ await bumpLayerGenerations(operation);
794
+ await Promise.all(foregroundWrites.values());
795
+ await Promise.all(writeQueues.map((queue) => queue.drain()));
796
+ const results = await Promise.all(layers.map(async (layer, index) => {
797
+ try {
798
+ return [layer.id, await invalidate(index)];
799
+ }
800
+ catch (error) {
801
+ options.onLayerError?.(layer.id, operation, error);
802
+ throw error;
803
+ }
804
+ }));
805
+ const modes = new Set(results.map(([, result]) => result.mode));
806
+ return {
807
+ mode: modes.size === 1 ? results[0][1].mode : 'mixed',
808
+ deleted: results.reduce((total, [, result]) => total + (result.deleted ?? 0), 0),
809
+ layers: Object.fromEntries(results),
810
+ };
811
+ }
812
+ return {
813
+ get: (key) => getFrom(key, allLayerIndexes),
814
+ set: async (key, value, writeOptions = {}) => {
815
+ await setTo(key, value, writeOptions, allLayerIndexes);
816
+ },
817
+ beginFill: (key) => beginFillFor(key, allLayerIndexes),
818
+ isFillCurrent,
819
+ commitFill: (ticket, value, writeOptions = {}) => commitFill(ticket, value, writeOptions),
820
+ cancelFill: (ticket) => { finishFill(ticket); },
821
+ selectLayers(layerIds) {
822
+ if (layerIds.length === 0)
823
+ throw new Error('A cache layer selection cannot be empty');
824
+ const selectedIndexes = layerIds.map((id) => {
825
+ const index = layerIndexes.get(id);
826
+ if (index === undefined)
827
+ throw new Error(`Cache layer "${id}" does not exist`);
828
+ return index;
829
+ });
830
+ for (let index = 1; index < selectedIndexes.length; index++) {
831
+ if (selectedIndexes[index] <= selectedIndexes[index - 1]) {
832
+ throw new Error('Cache profile layers must preserve their configured order');
833
+ }
834
+ }
835
+ return {
836
+ get: (key) => getFrom(key, selectedIndexes),
837
+ set: async (key, value, writeOptions = {}) => {
838
+ await setTo(key, value, writeOptions, selectedIndexes);
839
+ },
840
+ beginFill: (key) => beginFillFor(key, selectedIndexes),
841
+ isFillCurrent,
842
+ commitFill: (ticket, value, writeOptions = {}) => commitFill(ticket, value, writeOptions),
843
+ cancelFill: (ticket) => { finishFill(ticket); },
844
+ };
551
845
  },
552
846
  async delete(key) {
553
847
  if (clearInProgress)
@@ -561,15 +855,23 @@ export function createTieredCache(options) {
561
855
  keyWriteVersions.delete(namespacedKey);
562
856
  const operation = (async () => {
563
857
  const errors = [];
564
- await foregroundWrites.get(namespacedKey);
565
858
  try {
566
- await deleteLayer(0, namespacedKey);
859
+ await bumpLayerGenerations('delete');
567
860
  }
568
861
  catch (error) {
569
- options.onLayerError?.(layers[0].id, 'delete', error);
570
862
  errors.push(error);
571
863
  }
572
- await Promise.all(layers.slice(1).map((layer, index) => writeQueues[index].enqueueBarrier(namespacedKey, async () => {
864
+ await foregroundWrites.get(namespacedKey);
865
+ await writeQueues[0].enqueueBarrier(namespacedKey, async () => {
866
+ try {
867
+ await deleteLayer(0, namespacedKey);
868
+ }
869
+ catch (error) {
870
+ options.onLayerError?.(layers[0].id, 'delete', error);
871
+ errors.push(error);
872
+ }
873
+ });
874
+ await Promise.all(layers.slice(1).map((layer, index) => writeQueues[index + 1].enqueueBarrier(namespacedKey, async () => {
573
875
  try {
574
876
  await deleteLayer(index + 1, namespacedKey);
575
877
  }
@@ -593,6 +895,18 @@ export function createTieredCache(options) {
593
895
  releaseKey();
594
896
  }
595
897
  },
898
+ async invalidateTag(rawTag) {
899
+ const tag = rawTag.trim();
900
+ if (!tag)
901
+ throw new Error('Cache tag cannot be empty');
902
+ return invalidateAcrossLayers('invalidateTag', (index) => invalidateLayerTag(index, tag));
903
+ },
904
+ async invalidatePrefix(rawPrefix) {
905
+ const logicalPrefix = rawPrefix.trim();
906
+ if (!logicalPrefix)
907
+ throw new Error('Cache prefix cannot be empty; use clearNamespace instead');
908
+ return invalidateAcrossLayers('invalidatePrefix', (index) => invalidateLayerPrefix(index, logicalPrefix));
909
+ },
596
910
  async clearNamespace() {
597
911
  if (clearInProgress)
598
912
  return clearInProgress;
@@ -601,11 +915,18 @@ export function createTieredCache(options) {
601
915
  keyWriteVersions.clear();
602
916
  const operation = (async () => {
603
917
  const errors = [];
918
+ try {
919
+ await bumpLayerGenerations('clear');
920
+ }
921
+ catch (error) {
922
+ errors.push(error);
923
+ }
604
924
  for (const result of await Promise.allSettled(deletions.values())) {
605
925
  if (result.status === 'rejected')
606
926
  errors.push(result.reason);
607
927
  }
608
928
  await Promise.all(foregroundWrites.values());
929
+ await writeQueues[0].drain();
609
930
  try {
610
931
  await clearLayer(0);
611
932
  }
@@ -614,7 +935,7 @@ export function createTieredCache(options) {
614
935
  errors.push(error);
615
936
  }
616
937
  await Promise.all(layers.slice(1).map(async (layer, index) => {
617
- await writeQueues[index].drain();
938
+ await writeQueues[index + 1].drain();
618
939
  try {
619
940
  await clearLayer(index + 1);
620
941
  }
@@ -655,7 +976,7 @@ export function createTieredCache(options) {
655
976
  ]).size,
656
977
  } : {}),
657
978
  fencedKeys: layerFences[index].keys.size + layerFences[index].failedKeys.size,
658
- ...(index > 0 ? { writeQueue: writeQueues[index - 1].stats() } : {}),
979
+ ...(index > 0 ? { writeQueue: writeQueues[index].stats() } : {}),
659
980
  };
660
981
  });
661
982
  },