raffel 1.1.70 → 1.1.72
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/defaults.d.ts +9 -0
- package/dist/cache/defaults.d.ts.map +1 -0
- package/dist/cache/defaults.js +9 -0
- package/dist/cache/defaults.js.map +1 -0
- package/dist/cache/expiration-wheel.d.ts +15 -0
- package/dist/cache/expiration-wheel.d.ts.map +1 -0
- package/dist/cache/expiration-wheel.js +65 -0
- package/dist/cache/expiration-wheel.js.map +1 -0
- package/dist/cache/fs-layer.d.ts +14 -0
- package/dist/cache/fs-layer.d.ts.map +1 -0
- package/dist/cache/fs-layer.js +190 -0
- package/dist/cache/fs-layer.js.map +1 -0
- package/dist/cache/index.d.ts +9 -0
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +4 -0
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/key.d.ts +11 -0
- package/dist/cache/key.d.ts.map +1 -0
- package/dist/cache/key.js +41 -0
- package/dist/cache/key.js.map +1 -0
- package/dist/cache/redis-layer.d.ts +30 -0
- package/dist/cache/redis-layer.d.ts.map +1 -0
- package/dist/cache/redis-layer.js +108 -0
- package/dist/cache/redis-layer.js.map +1 -0
- package/dist/cache/server-runtime.d.ts +74 -0
- package/dist/cache/server-runtime.d.ts.map +1 -0
- package/dist/cache/server-runtime.js +220 -0
- package/dist/cache/server-runtime.js.map +1 -0
- package/dist/cache/tiered.d.ts +86 -0
- package/dist/cache/tiered.d.ts.map +1 -0
- package/dist/cache/tiered.js +681 -0
- package/dist/cache/tiered.js.map +1 -0
- package/dist/cache/write-behind-queue.d.ts +34 -0
- package/dist/cache/write-behind-queue.d.ts.map +1 -0
- package/dist/cache/write-behind-queue.js +115 -0
- package/dist/cache/write-behind-queue.js.map +1 -0
- package/dist/http/app.d.ts.map +1 -1
- package/dist/http/app.js +1 -0
- package/dist/http/app.js.map +1 -1
- package/dist/http/cache.d.ts +22 -0
- package/dist/http/cache.d.ts.map +1 -0
- package/dist/http/cache.js +183 -0
- package/dist/http/cache.js.map +1 -0
- package/dist/http/index.d.ts +2 -0
- package/dist/http/index.d.ts.map +1 -1
- package/dist/http/index.js +1 -0
- package/dist/http/index.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/server/builder/operation-registrar.d.ts +3 -0
- package/dist/server/builder/operation-registrar.d.ts.map +1 -1
- package/dist/server/builder/operation-registrar.js +7 -2
- package/dist/server/builder/operation-registrar.js.map +1 -1
- package/dist/server/builder/programmatic-registration.d.ts +2 -0
- package/dist/server/builder/programmatic-registration.d.ts.map +1 -1
- package/dist/server/builder/programmatic-registration.js +13 -2
- package/dist/server/builder/programmatic-registration.js.map +1 -1
- package/dist/server/builder.d.ts.map +1 -1
- package/dist/server/builder.js +17 -3
- package/dist/server/builder.js.map +1 -1
- package/dist/server/discovery-utils.d.ts +2 -1
- package/dist/server/discovery-utils.d.ts.map +1 -1
- package/dist/server/discovery-utils.js +10 -2
- package/dist/server/discovery-utils.js.map +1 -1
- package/dist/server/fs-routes/types.d.ts +3 -0
- package/dist/server/fs-routes/types.d.ts.map +1 -1
- package/dist/server/handler-builders.d.ts +2 -0
- package/dist/server/handler-builders.d.ts.map +1 -1
- package/dist/server/handler-builders.js +7 -0
- package/dist/server/handler-builders.js.map +1 -1
- package/dist/server/interceptor-utils.d.ts +2 -0
- package/dist/server/interceptor-utils.d.ts.map +1 -1
- package/dist/server/interceptor-utils.js +25 -1
- package/dist/server/interceptor-utils.js.map +1 -1
- package/dist/server/router-module.d.ts +3 -0
- package/dist/server/router-module.d.ts.map +1 -1
- package/dist/server/router-module.js +1 -0
- package/dist/server/router-module.js.map +1 -1
- package/dist/server/types/config-types.d.ts +3 -0
- package/dist/server/types/config-types.d.ts.map +1 -1
- package/dist/server/types/handler-types.d.ts +7 -0
- package/dist/server/types/handler-types.d.ts.map +1 -1
- package/dist/server/types/lifecycle-types.d.ts +2 -0
- package/dist/server/types/lifecycle-types.d.ts.map +1 -1
- package/dist/server/types/protocol-namespace-types.d.ts +3 -0
- package/dist/server/types/protocol-namespace-types.d.ts.map +1 -1
- package/dist/ui/cache/defaults.d.ts +9 -0
- package/dist/ui/cache/defaults.d.ts.map +1 -0
- package/dist/ui/cache/expiration-wheel.d.ts +15 -0
- package/dist/ui/cache/expiration-wheel.d.ts.map +1 -0
- package/dist/ui/cache/fs-layer.d.ts +14 -0
- package/dist/ui/cache/fs-layer.d.ts.map +1 -0
- package/dist/ui/cache/key.d.ts +11 -0
- package/dist/ui/cache/key.d.ts.map +1 -0
- package/dist/ui/cache/server-runtime.d.ts +74 -0
- package/dist/ui/cache/server-runtime.d.ts.map +1 -0
- package/dist/ui/cache/tiered.d.ts +86 -0
- package/dist/ui/cache/tiered.d.ts.map +1 -0
- package/dist/ui/cache/write-behind-queue.d.ts +34 -0
- package/dist/ui/cache/write-behind-queue.d.ts.map +1 -0
- package/dist/ui/server/fs-routes/types.d.ts +3 -0
- package/dist/ui/server/fs-routes/types.d.ts.map +1 -1
- package/dist/ui/validation/index.d.ts +1 -1
- package/dist/ui/validation/index.d.ts.map +1 -1
- package/dist/ui/validation/schema.d.ts +7 -0
- package/dist/ui/validation/schema.d.ts.map +1 -1
- package/dist/validation/index.d.ts +1 -1
- package/dist/validation/index.d.ts.map +1 -1
- package/dist/validation/index.js +1 -1
- package/dist/validation/index.js.map +1 -1
- package/dist/validation/schema.d.ts +7 -0
- package/dist/validation/schema.d.ts.map +1 -1
- package/dist/validation/schema.js +36 -2
- package/dist/validation/schema.js.map +1 -1
- package/package.json +5 -1
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
import { ExpirationWheel } from './expiration-wheel.js';
|
|
2
|
+
import { WriteBehindQueue, } from './write-behind-queue.js';
|
|
3
|
+
import { DEFAULT_CACHE_CIRCUIT_COOLDOWN_MS, DEFAULT_CACHE_CIRCUIT_FAILURE_THRESHOLD, DEFAULT_CACHE_OPERATION_TIMEOUT_MS, DEFAULT_CACHE_READ_TIMEOUT_MS, DEFAULT_L1_MAX_ENTRIES, DEFAULT_L1_MAX_MEMORY_BYTES, } from './defaults.js';
|
|
4
|
+
export function isCacheRecord(value) {
|
|
5
|
+
if (!value || typeof value !== 'object')
|
|
6
|
+
return false;
|
|
7
|
+
const record = value;
|
|
8
|
+
return Number.isFinite(record.createdAt)
|
|
9
|
+
&& Number.isFinite(record.expiresAt)
|
|
10
|
+
&& Number.isFinite(record.version)
|
|
11
|
+
&& (record.staleUntil === undefined || Number.isFinite(record.staleUntil))
|
|
12
|
+
&& (record.sizeBytes === undefined || Number.isFinite(record.sizeBytes))
|
|
13
|
+
&& 'value' in record;
|
|
14
|
+
}
|
|
15
|
+
function layerDuration(configured, layer, fallback) {
|
|
16
|
+
if (typeof configured === 'number')
|
|
17
|
+
return configured;
|
|
18
|
+
return configured?.[layer.id] ?? fallback;
|
|
19
|
+
}
|
|
20
|
+
function isJsonSafe(value, seen = new WeakSet()) {
|
|
21
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
22
|
+
return true;
|
|
23
|
+
if (typeof value === 'number')
|
|
24
|
+
return Number.isFinite(value);
|
|
25
|
+
if (typeof value !== 'object')
|
|
26
|
+
return false;
|
|
27
|
+
if (seen.has(value))
|
|
28
|
+
return false;
|
|
29
|
+
seen.add(value);
|
|
30
|
+
if (Array.isArray(value)) {
|
|
31
|
+
const keys = Object.keys(value);
|
|
32
|
+
if (keys.length !== value.length)
|
|
33
|
+
return false;
|
|
34
|
+
return keys.every((key, index) => key === String(index) && isJsonSafe(value[index], seen));
|
|
35
|
+
}
|
|
36
|
+
const prototype = Object.getPrototypeOf(value);
|
|
37
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
38
|
+
return false;
|
|
39
|
+
const enumerableKeys = Object.keys(value);
|
|
40
|
+
if (Reflect.ownKeys(value).length !== enumerableKeys.length)
|
|
41
|
+
return false;
|
|
42
|
+
return enumerableKeys.every((key) => {
|
|
43
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
44
|
+
return Boolean(descriptor && 'value' in descriptor && isJsonSafe(descriptor.value, seen));
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function logicalSize(value) {
|
|
48
|
+
try {
|
|
49
|
+
if (!isJsonSafe(value))
|
|
50
|
+
return undefined;
|
|
51
|
+
const encoded = JSON.stringify(value);
|
|
52
|
+
return encoded === undefined ? undefined : Buffer.byteLength(encoded);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function isThenable(value) {
|
|
59
|
+
return Boolean(value && typeof value.then === 'function');
|
|
60
|
+
}
|
|
61
|
+
export function createMemoryCacheLayer(options) {
|
|
62
|
+
if (options.ttlMs <= 0)
|
|
63
|
+
throw new Error('Memory cache ttlMs must be greater than zero');
|
|
64
|
+
if (options.maxEntries !== undefined && options.maxEntries <= 0) {
|
|
65
|
+
throw new Error('Memory cache maxEntries must be greater than zero');
|
|
66
|
+
}
|
|
67
|
+
if (options.maxMemoryBytes !== undefined && options.maxMemoryBytes <= 0) {
|
|
68
|
+
throw new Error('Memory cache maxMemoryBytes must be greater than zero');
|
|
69
|
+
}
|
|
70
|
+
if (options.expirationResolutionMs !== undefined && options.expirationResolutionMs <= 0) {
|
|
71
|
+
throw new Error('Memory cache expirationResolutionMs must be greater than zero');
|
|
72
|
+
}
|
|
73
|
+
const entries = new Map();
|
|
74
|
+
const maxEntries = options.maxEntries ?? DEFAULT_L1_MAX_ENTRIES;
|
|
75
|
+
const maxMemoryBytes = options.maxMemoryBytes ?? DEFAULT_L1_MAX_MEMORY_BYTES;
|
|
76
|
+
let currentMemoryBytes = 0;
|
|
77
|
+
let hits = 0;
|
|
78
|
+
let misses = 0;
|
|
79
|
+
const wheel = new ExpirationWheel(options.expirationResolutionMs ?? 1_000, (key, scheduledVersion) => {
|
|
80
|
+
if (entries.get(key)?.version === scheduledVersion)
|
|
81
|
+
remove(key);
|
|
82
|
+
});
|
|
83
|
+
function remove(key) {
|
|
84
|
+
const record = entries.get(key);
|
|
85
|
+
if (record)
|
|
86
|
+
currentMemoryBytes -= record.sizeBytes ?? 0;
|
|
87
|
+
entries.delete(key);
|
|
88
|
+
wheel.cancel(key);
|
|
89
|
+
}
|
|
90
|
+
function evictUntilFits(incomingBytes) {
|
|
91
|
+
if (incomingBytes > maxMemoryBytes)
|
|
92
|
+
return false;
|
|
93
|
+
while (currentMemoryBytes + incomingBytes > maxMemoryBytes ||
|
|
94
|
+
entries.size >= maxEntries) {
|
|
95
|
+
const oldest = entries.keys().next().value;
|
|
96
|
+
if (oldest === undefined)
|
|
97
|
+
break;
|
|
98
|
+
remove(oldest);
|
|
99
|
+
}
|
|
100
|
+
return currentMemoryBytes + incomingBytes <= maxMemoryBytes;
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
id: options.id,
|
|
104
|
+
synchronous: true,
|
|
105
|
+
ttlMs: options.ttlMs,
|
|
106
|
+
get(key) {
|
|
107
|
+
const record = entries.get(key);
|
|
108
|
+
if (!record) {
|
|
109
|
+
misses++;
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
if (Date.now() >= (record.staleUntil ?? record.expiresAt)) {
|
|
113
|
+
remove(key);
|
|
114
|
+
misses++;
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
if (options.eviction !== 'fifo') {
|
|
118
|
+
entries.delete(key);
|
|
119
|
+
entries.set(key, record);
|
|
120
|
+
}
|
|
121
|
+
hits++;
|
|
122
|
+
return record;
|
|
123
|
+
},
|
|
124
|
+
set(key, record, ttlMs, staleMs = 0) {
|
|
125
|
+
const sizeBytes = record.sizeBytes ?? logicalSize(record.value);
|
|
126
|
+
if (sizeBytes === undefined)
|
|
127
|
+
return;
|
|
128
|
+
remove(key);
|
|
129
|
+
if (!evictUntilFits(sizeBytes))
|
|
130
|
+
return;
|
|
131
|
+
const expiresAt = Date.now() + ttlMs;
|
|
132
|
+
const stored = {
|
|
133
|
+
...record,
|
|
134
|
+
sizeBytes,
|
|
135
|
+
expiresAt,
|
|
136
|
+
staleUntil: staleMs > 0 ? expiresAt + staleMs : undefined,
|
|
137
|
+
};
|
|
138
|
+
entries.set(key, stored);
|
|
139
|
+
currentMemoryBytes += sizeBytes;
|
|
140
|
+
wheel.schedule(key, stored.staleUntil ?? stored.expiresAt, stored.version);
|
|
141
|
+
},
|
|
142
|
+
delete(key) {
|
|
143
|
+
remove(key);
|
|
144
|
+
},
|
|
145
|
+
clearNamespace(namespace) {
|
|
146
|
+
for (const key of entries.keys()) {
|
|
147
|
+
if (key.startsWith(`${namespace}:`))
|
|
148
|
+
remove(key);
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
stats() {
|
|
152
|
+
return { totalItems: entries.size, memoryUsageBytes: currentMemoryBytes, hits, misses };
|
|
153
|
+
},
|
|
154
|
+
shutdown() {
|
|
155
|
+
wheel.shutdown();
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
export function createTieredCache(options) {
|
|
160
|
+
if (options.layers.length === 0)
|
|
161
|
+
throw new Error('Tiered cache requires at least one layer');
|
|
162
|
+
if (options.readTimeoutMs !== undefined && options.readTimeoutMs <= 0) {
|
|
163
|
+
throw new Error('Tiered cache readTimeoutMs must be greater than zero');
|
|
164
|
+
}
|
|
165
|
+
if (options.operationTimeoutMs !== undefined && options.operationTimeoutMs <= 0) {
|
|
166
|
+
throw new Error('Tiered cache operationTimeoutMs must be greater than zero');
|
|
167
|
+
}
|
|
168
|
+
for (const layer of options.layers) {
|
|
169
|
+
if (layer.readTimeoutMs !== undefined && layer.readTimeoutMs <= 0) {
|
|
170
|
+
throw new Error(`Cache layer "${layer.id}" readTimeoutMs must be greater than zero`);
|
|
171
|
+
}
|
|
172
|
+
if (layer.operationTimeoutMs !== undefined && layer.operationTimeoutMs <= 0) {
|
|
173
|
+
throw new Error(`Cache layer "${layer.id}" operationTimeoutMs must be greater than zero`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const prefix = `${options.namespace}:`;
|
|
177
|
+
const layers = [...options.layers];
|
|
178
|
+
const writeQueues = layers.slice(1).map((layer) => new WriteBehindQueue({
|
|
179
|
+
...options.writeBehind,
|
|
180
|
+
...layer.writeBehind,
|
|
181
|
+
}));
|
|
182
|
+
let version = 0;
|
|
183
|
+
let namespaceEpoch = 0;
|
|
184
|
+
let clearInProgress;
|
|
185
|
+
const keyEpochs = new Map();
|
|
186
|
+
const keyWriteVersions = new Map();
|
|
187
|
+
const keyActivity = new Map();
|
|
188
|
+
const deletions = new Map();
|
|
189
|
+
const foregroundWrites = new Map();
|
|
190
|
+
const readHealth = new Map();
|
|
191
|
+
const layerFences = layers.map(() => ({
|
|
192
|
+
namespace: 0,
|
|
193
|
+
keys: new Map(),
|
|
194
|
+
failedNamespace: false,
|
|
195
|
+
failedKeys: new Set(),
|
|
196
|
+
}));
|
|
197
|
+
function keyHasFence(key) {
|
|
198
|
+
return layerFences.some((fence) => fence.keys.has(key) || fence.failedKeys.has(key));
|
|
199
|
+
}
|
|
200
|
+
function cleanupKeyState(key) {
|
|
201
|
+
if (keyActivity.has(key) || deletions.has(key) || foregroundWrites.has(key) ||
|
|
202
|
+
keyHasFence(key))
|
|
203
|
+
return;
|
|
204
|
+
keyEpochs.delete(key);
|
|
205
|
+
keyWriteVersions.delete(key);
|
|
206
|
+
}
|
|
207
|
+
function retainKey(key) {
|
|
208
|
+
keyActivity.set(key, (keyActivity.get(key) ?? 0) + 1);
|
|
209
|
+
let active = true;
|
|
210
|
+
return () => {
|
|
211
|
+
if (!active)
|
|
212
|
+
return;
|
|
213
|
+
active = false;
|
|
214
|
+
const count = (keyActivity.get(key) ?? 1) - 1;
|
|
215
|
+
if (count === 0)
|
|
216
|
+
keyActivity.delete(key);
|
|
217
|
+
else
|
|
218
|
+
keyActivity.set(key, count);
|
|
219
|
+
cleanupKeyState(key);
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function addLayerFence(index, key) {
|
|
223
|
+
const fence = layerFences[index];
|
|
224
|
+
if (key === undefined)
|
|
225
|
+
fence.namespace++;
|
|
226
|
+
else
|
|
227
|
+
fence.keys.set(key, (fence.keys.get(key) ?? 0) + 1);
|
|
228
|
+
let active = true;
|
|
229
|
+
return () => {
|
|
230
|
+
if (!active)
|
|
231
|
+
return;
|
|
232
|
+
active = false;
|
|
233
|
+
if (key === undefined) {
|
|
234
|
+
fence.namespace--;
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const count = (fence.keys.get(key) ?? 1) - 1;
|
|
238
|
+
if (count === 0)
|
|
239
|
+
fence.keys.delete(key);
|
|
240
|
+
else
|
|
241
|
+
fence.keys.set(key, count);
|
|
242
|
+
cleanupKeyState(key);
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function isLayerFenced(index, key) {
|
|
246
|
+
const fence = layerFences[index];
|
|
247
|
+
return fence.namespace > 0 || fence.failedNamespace ||
|
|
248
|
+
fence.keys.has(key) || fence.failedKeys.has(key);
|
|
249
|
+
}
|
|
250
|
+
function markLayerFailure(index, key) {
|
|
251
|
+
const fence = layerFences[index];
|
|
252
|
+
if (key === undefined)
|
|
253
|
+
fence.failedNamespace = true;
|
|
254
|
+
else {
|
|
255
|
+
fence.failedKeys.add(key);
|
|
256
|
+
cleanupKeyState(key);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function clearLayerFailure(index, key) {
|
|
260
|
+
const fence = layerFences[index];
|
|
261
|
+
if (key === undefined)
|
|
262
|
+
fence.failedNamespace = false;
|
|
263
|
+
else {
|
|
264
|
+
fence.failedKeys.delete(key);
|
|
265
|
+
cleanupKeyState(key);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
async function withDeadline(candidate, timeoutMs, message, onTimeout, onLateSettlement) {
|
|
269
|
+
if (!isThenable(candidate))
|
|
270
|
+
return candidate;
|
|
271
|
+
let timedOut = false;
|
|
272
|
+
let timer;
|
|
273
|
+
return new Promise((resolve, reject) => {
|
|
274
|
+
timer = setTimeout(() => {
|
|
275
|
+
timedOut = true;
|
|
276
|
+
onTimeout?.();
|
|
277
|
+
reject(new Error(message));
|
|
278
|
+
}, timeoutMs);
|
|
279
|
+
timer.unref();
|
|
280
|
+
Promise.resolve(candidate).then((value) => {
|
|
281
|
+
if (!timedOut) {
|
|
282
|
+
if (timer)
|
|
283
|
+
clearTimeout(timer);
|
|
284
|
+
resolve(value);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
void Promise.resolve(onLateSettlement?.({ ok: true, value })).catch(() => undefined);
|
|
288
|
+
}, (error) => {
|
|
289
|
+
if (!timedOut) {
|
|
290
|
+
if (timer)
|
|
291
|
+
clearTimeout(timer);
|
|
292
|
+
reject(error);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
void Promise.resolve(onLateSettlement?.({ ok: false, error })).catch(() => undefined);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
function trackForegroundWrite(key, task) {
|
|
300
|
+
const releaseKey = retainKey(key);
|
|
301
|
+
const previous = foregroundWrites.get(key);
|
|
302
|
+
const execution = Promise.resolve(previous).then(task);
|
|
303
|
+
foregroundWrites.set(key, execution);
|
|
304
|
+
return execution.finally(() => {
|
|
305
|
+
if (foregroundWrites.get(key) === execution)
|
|
306
|
+
foregroundWrites.delete(key);
|
|
307
|
+
releaseKey();
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
async function writeLayer(index, key, record, ttlMs, staleMs, capturedNamespaceEpoch, capturedKeyEpoch, capturedWriteVersion) {
|
|
311
|
+
if (isLayerFenced(index, key))
|
|
312
|
+
return;
|
|
313
|
+
const layer = layers[index];
|
|
314
|
+
let releaseFence;
|
|
315
|
+
const cleanupLateWrite = async (result) => {
|
|
316
|
+
if (!releaseFence)
|
|
317
|
+
return;
|
|
318
|
+
const stillCurrent = result.ok &&
|
|
319
|
+
namespaceEpoch === capturedNamespaceEpoch &&
|
|
320
|
+
(keyEpochs.get(key) ?? 0) === capturedKeyEpoch &&
|
|
321
|
+
keyWriteVersions.get(key) === capturedWriteVersion;
|
|
322
|
+
if (stillCurrent) {
|
|
323
|
+
releaseFence();
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (!result.ok)
|
|
327
|
+
options.onLayerError?.(layer.id, 'set', result.error);
|
|
328
|
+
try {
|
|
329
|
+
await layer.delete(key);
|
|
330
|
+
releaseFence();
|
|
331
|
+
clearLayerFailure(index, key);
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
options.onLayerError?.(layer.id, 'delete', error);
|
|
335
|
+
releaseFence();
|
|
336
|
+
markLayerFailure(index, key);
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
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
|
+
}
|
|
341
|
+
async function deleteLayer(index, key) {
|
|
342
|
+
const layer = layers[index];
|
|
343
|
+
let releaseFence;
|
|
344
|
+
try {
|
|
345
|
+
await withDeadline(layer.delete(key), layer.operationTimeoutMs ?? options.operationTimeoutMs ?? DEFAULT_CACHE_OPERATION_TIMEOUT_MS, `Cache layer "${layer.id}" delete timed out`, () => { releaseFence = addLayerFence(index, key); }, (result) => {
|
|
346
|
+
releaseFence?.();
|
|
347
|
+
if (result.ok)
|
|
348
|
+
clearLayerFailure(index, key);
|
|
349
|
+
else {
|
|
350
|
+
markLayerFailure(index, key);
|
|
351
|
+
options.onLayerError?.(layer.id, 'delete', result.error);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
clearLayerFailure(index, key);
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
if (!releaseFence)
|
|
358
|
+
markLayerFailure(index, key);
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
async function clearLayer(index) {
|
|
363
|
+
const layer = layers[index];
|
|
364
|
+
let releaseFence;
|
|
365
|
+
try {
|
|
366
|
+
await withDeadline(layer.clearNamespace(options.namespace), layer.operationTimeoutMs ?? options.operationTimeoutMs ?? DEFAULT_CACHE_OPERATION_TIMEOUT_MS, `Cache layer "${layer.id}" clear timed out`, () => { releaseFence = addLayerFence(index); }, (result) => {
|
|
367
|
+
releaseFence?.();
|
|
368
|
+
if (result.ok)
|
|
369
|
+
clearLayerFailure(index);
|
|
370
|
+
else {
|
|
371
|
+
markLayerFailure(index);
|
|
372
|
+
options.onLayerError?.(layer.id, 'clear', result.error);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
clearLayerFailure(index);
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
if (!releaseFence)
|
|
379
|
+
markLayerFailure(index);
|
|
380
|
+
throw error;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function guardedRead(layer, key) {
|
|
384
|
+
const health = readHealth.get(layer) ?? { failures: 0, openUntil: 0 };
|
|
385
|
+
readHealth.set(layer, health);
|
|
386
|
+
if (health.openUntil > Date.now()) {
|
|
387
|
+
throw new Error(`Cache layer "${layer.id}" circuit is open`);
|
|
388
|
+
}
|
|
389
|
+
const markSuccess = (result) => {
|
|
390
|
+
health.failures = 0;
|
|
391
|
+
health.openUntil = 0;
|
|
392
|
+
return result;
|
|
393
|
+
};
|
|
394
|
+
const markFailure = (error) => {
|
|
395
|
+
health.failures++;
|
|
396
|
+
const threshold = layer.circuitBreaker?.failureThreshold ??
|
|
397
|
+
DEFAULT_CACHE_CIRCUIT_FAILURE_THRESHOLD;
|
|
398
|
+
if (health.failures >= threshold) {
|
|
399
|
+
health.openUntil = Date.now() + (layer.circuitBreaker?.cooldownMs ?? DEFAULT_CACHE_CIRCUIT_COOLDOWN_MS);
|
|
400
|
+
}
|
|
401
|
+
throw error;
|
|
402
|
+
};
|
|
403
|
+
try {
|
|
404
|
+
const candidate = layer.get(key);
|
|
405
|
+
if (!isThenable(candidate))
|
|
406
|
+
return markSuccess(candidate);
|
|
407
|
+
return withDeadline(candidate, layer.readTimeoutMs ?? options.readTimeoutMs ?? DEFAULT_CACHE_READ_TIMEOUT_MS, `Cache layer "${layer.id}" read timed out`)
|
|
408
|
+
.then(markSuccess, markFailure);
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
return markFailure(error);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
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;
|
|
439
|
+
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
|
+
}
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
options.onLayerError?.(layer.id, 'get', error);
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
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);
|
|
465
|
+
}
|
|
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;
|
|
472
|
+
}
|
|
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
|
+
}));
|
|
486
|
+
}
|
|
487
|
+
if (!readIsCurrent())
|
|
488
|
+
return undefined;
|
|
489
|
+
return { value: record.value, layer: layer.id, stale: false };
|
|
490
|
+
}
|
|
491
|
+
return readIsCurrent() ? staleHit : undefined;
|
|
492
|
+
}
|
|
493
|
+
finally {
|
|
494
|
+
releaseKey?.();
|
|
495
|
+
}
|
|
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,
|
|
518
|
+
};
|
|
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
|
+
});
|
|
530
|
+
}
|
|
531
|
+
for (let index = 1; index < layers.length; index++) {
|
|
532
|
+
const layer = layers[index];
|
|
533
|
+
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);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
finally {
|
|
549
|
+
releaseKey();
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
async delete(key) {
|
|
553
|
+
if (clearInProgress)
|
|
554
|
+
await clearInProgress.catch(() => undefined);
|
|
555
|
+
const namespacedKey = prefix + key;
|
|
556
|
+
const existing = deletions.get(namespacedKey);
|
|
557
|
+
if (existing)
|
|
558
|
+
return existing;
|
|
559
|
+
const releaseKey = retainKey(namespacedKey);
|
|
560
|
+
keyEpochs.set(namespacedKey, (keyEpochs.get(namespacedKey) ?? 0) + 1);
|
|
561
|
+
keyWriteVersions.delete(namespacedKey);
|
|
562
|
+
const operation = (async () => {
|
|
563
|
+
const errors = [];
|
|
564
|
+
await foregroundWrites.get(namespacedKey);
|
|
565
|
+
try {
|
|
566
|
+
await deleteLayer(0, namespacedKey);
|
|
567
|
+
}
|
|
568
|
+
catch (error) {
|
|
569
|
+
options.onLayerError?.(layers[0].id, 'delete', error);
|
|
570
|
+
errors.push(error);
|
|
571
|
+
}
|
|
572
|
+
await Promise.all(layers.slice(1).map((layer, index) => writeQueues[index].enqueueBarrier(namespacedKey, async () => {
|
|
573
|
+
try {
|
|
574
|
+
await deleteLayer(index + 1, namespacedKey);
|
|
575
|
+
}
|
|
576
|
+
catch (error) {
|
|
577
|
+
options.onLayerError?.(layer.id, 'delete', error);
|
|
578
|
+
errors.push(error);
|
|
579
|
+
}
|
|
580
|
+
})));
|
|
581
|
+
if (errors.length > 0) {
|
|
582
|
+
throw new AggregateError(errors, `Cache key invalidation failed for ${errors.length} layer(s)`);
|
|
583
|
+
}
|
|
584
|
+
})();
|
|
585
|
+
deletions.set(namespacedKey, operation);
|
|
586
|
+
try {
|
|
587
|
+
await operation;
|
|
588
|
+
}
|
|
589
|
+
finally {
|
|
590
|
+
if (deletions.get(namespacedKey) === operation) {
|
|
591
|
+
deletions.delete(namespacedKey);
|
|
592
|
+
}
|
|
593
|
+
releaseKey();
|
|
594
|
+
}
|
|
595
|
+
},
|
|
596
|
+
async clearNamespace() {
|
|
597
|
+
if (clearInProgress)
|
|
598
|
+
return clearInProgress;
|
|
599
|
+
namespaceEpoch++;
|
|
600
|
+
keyEpochs.clear();
|
|
601
|
+
keyWriteVersions.clear();
|
|
602
|
+
const operation = (async () => {
|
|
603
|
+
const errors = [];
|
|
604
|
+
for (const result of await Promise.allSettled(deletions.values())) {
|
|
605
|
+
if (result.status === 'rejected')
|
|
606
|
+
errors.push(result.reason);
|
|
607
|
+
}
|
|
608
|
+
await Promise.all(foregroundWrites.values());
|
|
609
|
+
try {
|
|
610
|
+
await clearLayer(0);
|
|
611
|
+
}
|
|
612
|
+
catch (error) {
|
|
613
|
+
options.onLayerError?.(layers[0].id, 'clear', error);
|
|
614
|
+
errors.push(error);
|
|
615
|
+
}
|
|
616
|
+
await Promise.all(layers.slice(1).map(async (layer, index) => {
|
|
617
|
+
await writeQueues[index].drain();
|
|
618
|
+
try {
|
|
619
|
+
await clearLayer(index + 1);
|
|
620
|
+
}
|
|
621
|
+
catch (error) {
|
|
622
|
+
options.onLayerError?.(layer.id, 'clear', error);
|
|
623
|
+
errors.push(error);
|
|
624
|
+
}
|
|
625
|
+
}));
|
|
626
|
+
if (errors.length > 0) {
|
|
627
|
+
throw new AggregateError(errors, `Cache namespace invalidation failed for ${errors.length} operation(s)`);
|
|
628
|
+
}
|
|
629
|
+
})();
|
|
630
|
+
clearInProgress = operation;
|
|
631
|
+
try {
|
|
632
|
+
await operation;
|
|
633
|
+
}
|
|
634
|
+
finally {
|
|
635
|
+
if (clearInProgress === operation)
|
|
636
|
+
clearInProgress = undefined;
|
|
637
|
+
}
|
|
638
|
+
},
|
|
639
|
+
stats() {
|
|
640
|
+
return layers.map((layer, index) => {
|
|
641
|
+
const health = readHealth.get(layer);
|
|
642
|
+
return {
|
|
643
|
+
id: layer.id,
|
|
644
|
+
totalItems: 0,
|
|
645
|
+
...layer.stats?.(),
|
|
646
|
+
...(health ? {
|
|
647
|
+
readFailures: health.failures,
|
|
648
|
+
circuitOpen: health.openUntil > Date.now(),
|
|
649
|
+
} : {}),
|
|
650
|
+
...(index === 0 ? {
|
|
651
|
+
trackedKeys: new Set([
|
|
652
|
+
...keyActivity.keys(),
|
|
653
|
+
...keyEpochs.keys(),
|
|
654
|
+
...keyWriteVersions.keys(),
|
|
655
|
+
]).size,
|
|
656
|
+
} : {}),
|
|
657
|
+
fencedKeys: layerFences[index].keys.size + layerFences[index].failedKeys.size,
|
|
658
|
+
...(index > 0 ? { writeQueue: writeQueues[index - 1].stats() } : {}),
|
|
659
|
+
};
|
|
660
|
+
});
|
|
661
|
+
},
|
|
662
|
+
async shutdown() {
|
|
663
|
+
await Promise.all(writeQueues.map((queue) => queue.flush()));
|
|
664
|
+
await Promise.all(layers.map(async (layer) => {
|
|
665
|
+
try {
|
|
666
|
+
const candidate = layer.shutdown?.();
|
|
667
|
+
if (candidate !== undefined) {
|
|
668
|
+
await withDeadline(candidate, layer.operationTimeoutMs ?? options.operationTimeoutMs ??
|
|
669
|
+
DEFAULT_CACHE_OPERATION_TIMEOUT_MS, `Cache layer "${layer.id}" shutdown timed out`);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
catch (error) {
|
|
673
|
+
options.onLayerError?.(layer.id, 'shutdown', error);
|
|
674
|
+
}
|
|
675
|
+
}));
|
|
676
|
+
},
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
export { createFileSystemCacheLayer } from './fs-layer.js';
|
|
680
|
+
export { DEFAULT_CACHE_CIRCUIT_COOLDOWN_MS, DEFAULT_CACHE_CIRCUIT_FAILURE_THRESHOLD, DEFAULT_CACHE_OPERATION_TIMEOUT_MS, DEFAULT_CACHE_READ_TIMEOUT_MS, DEFAULT_L1_MAX_ENTRIES, DEFAULT_L1_MAX_MEMORY_BYTES, DEFAULT_L2_MAX_FILES, DEFAULT_L2_MAX_SIZE_BYTES, } from './defaults.js';
|
|
681
|
+
//# sourceMappingURL=tiered.js.map
|