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.
- package/dist/cache/fs-layer.d.ts.map +1 -1
- package/dist/cache/fs-layer.js +25 -2
- package/dist/cache/fs-layer.js.map +1 -1
- package/dist/cache/index.d.ts +5 -5
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +2 -2
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/key.d.ts +28 -7
- package/dist/cache/key.d.ts.map +1 -1
- package/dist/cache/key.js +191 -18
- package/dist/cache/key.js.map +1 -1
- package/dist/cache/redis-layer.d.ts +6 -0
- package/dist/cache/redis-layer.d.ts.map +1 -1
- package/dist/cache/redis-layer.js +150 -33
- package/dist/cache/redis-layer.js.map +1 -1
- package/dist/cache/server-runtime.d.ts +22 -9
- package/dist/cache/server-runtime.d.ts.map +1 -1
- package/dist/cache/server-runtime.js +190 -22
- package/dist/cache/server-runtime.js.map +1 -1
- package/dist/cache/tiered.d.ts +43 -2
- package/dist/cache/tiered.d.ts.map +1 -1
- package/dist/cache/tiered.js +457 -136
- package/dist/cache/tiered.js.map +1 -1
- package/dist/http/cache.d.ts +10 -1
- package/dist/http/cache.d.ts.map +1 -1
- package/dist/http/cache.js +105 -37
- package/dist/http/cache.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware/auth/oauth2-client-credentials.d.ts.map +1 -1
- package/dist/middleware/auth/oauth2-client-credentials.js +3 -0
- package/dist/middleware/auth/oauth2-client-credentials.js.map +1 -1
- package/dist/middleware/auth/oauth2.d.ts.map +1 -1
- package/dist/middleware/auth/oauth2.js +8 -0
- package/dist/middleware/auth/oauth2.js.map +1 -1
- package/dist/middleware/auth.d.ts +2 -0
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +62 -0
- package/dist/middleware/auth.js.map +1 -1
- package/dist/server/builder.d.ts.map +1 -1
- package/dist/server/builder.js +1 -0
- package/dist/server/builder.js.map +1 -1
- package/dist/server/fs-routes/middleware-processor.d.ts.map +1 -1
- package/dist/server/fs-routes/middleware-processor.js +15 -0
- package/dist/server/fs-routes/middleware-processor.js.map +1 -1
- package/dist/types/context.d.ts +2 -0
- package/dist/types/context.d.ts.map +1 -1
- package/dist/types/context.js +3 -0
- package/dist/types/context.js.map +1 -1
- package/dist/ui/cache/fs-layer.d.ts.map +1 -1
- package/dist/ui/cache/key.d.ts +28 -7
- package/dist/ui/cache/key.d.ts.map +1 -1
- package/dist/ui/cache/server-runtime.d.ts +22 -9
- package/dist/ui/cache/server-runtime.d.ts.map +1 -1
- package/dist/ui/cache/tiered.d.ts +43 -2
- package/dist/ui/cache/tiered.d.ts.map +1 -1
- package/dist/ui/server/fs-routes/middleware-processor.d.ts.map +1 -1
- package/dist/ui/types/context.d.ts +2 -0
- package/dist/ui/types/context.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cache/tiered.js
CHANGED
|
@@ -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 -=
|
|
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
|
|
126
|
-
if (
|
|
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(
|
|
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 =
|
|
239
|
+
const prefix = cacheNamespacePrefix(options.namespace);
|
|
177
240
|
const layers = [...options.layers];
|
|
178
|
-
const writeQueues = layers.
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
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, '
|
|
451
|
-
continue;
|
|
579
|
+
options.onLayerError?.(layer.id, 'delete', error);
|
|
452
580
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
|
|
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
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
-
|
|
488
|
-
|
|
489
|
-
|
|
659
|
+
};
|
|
660
|
+
try {
|
|
661
|
+
await writeQueues[index].enqueueBarrier(namespacedKey, write);
|
|
490
662
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
663
|
+
finally {
|
|
664
|
+
releaseQueuedKey();
|
|
665
|
+
}
|
|
666
|
+
if (!allAccepted)
|
|
667
|
+
return false;
|
|
495
668
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
-
|
|
520
|
-
|
|
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
|
-
|
|
532
|
-
const layer = layers[index];
|
|
694
|
+
else {
|
|
533
695
|
const releaseQueuedKey = retainKey(namespacedKey);
|
|
534
|
-
writeQueues[index
|
|
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
|
-
|
|
549
|
-
|
|
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
|
|
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
|
|
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
|
|
979
|
+
...(index > 0 ? { writeQueue: writeQueues[index].stats() } : {}),
|
|
659
980
|
};
|
|
660
981
|
});
|
|
661
982
|
},
|