@dee-wan/cache 1.0.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +208 -3
- package/dist/chunk-A4WV6RY4.js +311 -0
- package/dist/chunk-A4WV6RY4.js.map +1 -0
- package/dist/chunk-AENVFT2S.js +6 -0
- package/dist/chunk-AENVFT2S.js.map +1 -0
- package/dist/chunk-E6OZZ5UX.js +133 -0
- package/dist/chunk-E6OZZ5UX.js.map +1 -0
- package/dist/chunk-JPK2AF7X.js +18 -0
- package/dist/chunk-JPK2AF7X.js.map +1 -0
- package/dist/chunk-LAXBP5DQ.js +330 -0
- package/dist/chunk-LAXBP5DQ.js.map +1 -0
- package/dist/chunk-LHOK33GI.js +12 -0
- package/dist/chunk-LHOK33GI.js.map +1 -0
- package/dist/chunk-M4UOCIDQ.js +557 -0
- package/dist/chunk-M4UOCIDQ.js.map +1 -0
- package/dist/chunk-NFBMWKY5.js +54 -0
- package/dist/chunk-NFBMWKY5.js.map +1 -0
- package/dist/chunk-O3KHMFHX.js +425 -0
- package/dist/chunk-O3KHMFHX.js.map +1 -0
- package/dist/chunk-WJNCFFF3.js +637 -0
- package/dist/chunk-WJNCFFF3.js.map +1 -0
- package/dist/chunk-WKZ7TKGM.js +82 -0
- package/dist/chunk-WKZ7TKGM.js.map +1 -0
- package/dist/chunk-ZBYAKDNZ.js +45 -0
- package/dist/chunk-ZBYAKDNZ.js.map +1 -0
- package/dist/chunk-ZCMZJKGN.js +78 -0
- package/dist/chunk-ZCMZJKGN.js.map +1 -0
- package/dist/client.d.ts +63 -0
- package/dist/client.js +260 -0
- package/dist/client.js.map +1 -0
- package/dist/cloudflare/index.d.ts +15 -2
- package/dist/cloudflare/index.js +413 -45
- package/dist/cloudflare/index.js.map +1 -1
- package/dist/d1/config.d.ts +41 -0
- package/dist/d1/config.js +102 -0
- package/dist/d1/config.js.map +1 -0
- package/dist/d1/index.d.ts +80 -0
- package/dist/d1/index.js +218 -0
- package/dist/d1/index.js.map +1 -0
- package/dist/d1/response-lease.d.ts +9 -0
- package/dist/d1/response-lease.js +44 -0
- package/dist/d1/response-lease.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -616
- package/dist/index.js.map +1 -1
- package/dist/profile--LBtbQI_.d.ts +30 -0
- package/dist/response-lease-DeeM-ofO.d.ts +19 -0
- package/dist/response-lease.d.ts +7 -0
- package/dist/response-lease.js +14 -0
- package/dist/response-lease.js.map +1 -0
- package/dist/response-warmup-BWQvnT1G.d.ts +38 -0
- package/dist/scoped-read.d.ts +12 -0
- package/dist/scoped-read.js +30 -0
- package/dist/scoped-read.js.map +1 -0
- package/dist/{types-DwtU0KpX.d.ts → types-CtErcz2a.d.ts} +25 -1
- package/docs/cache-contract.md +49 -1
- package/package.json +31 -2
package/dist/cloudflare/index.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
export { createDurableObjectStore } from '../chunk-NFBMWKY5.js';
|
|
2
|
+
export { createResponseWarmup } from '../chunk-A4WV6RY4.js';
|
|
3
|
+
import { boundedD1Store, withinCoordinatorDeadline } from '../chunk-WKZ7TKGM.js';
|
|
4
|
+
import { createRegionalCache } from '../chunk-E6OZZ5UX.js';
|
|
5
|
+
import { GLOBAL_FENCE, MAX_TTL_MS, MAX_VALUE_BYTES, MAX_COORDINATOR_GENERATION_LENGTH, MAX_LEASE_MS, MAX_LOCK_MS } from '../chunk-OQRNVGQE.js';
|
|
6
|
+
import '../chunk-ZCMZJKGN.js';
|
|
2
7
|
import { DurableObject } from 'cloudflare:workers';
|
|
3
8
|
|
|
4
9
|
// src/cloudflare/coordinator-core.ts
|
|
@@ -213,6 +218,13 @@ function createCoordinatorCore(sql, newToken, transact, limits = {}) {
|
|
|
213
218
|
return snapshot !== null && matches(snapshot, epochs(Object.keys(snapshot)));
|
|
214
219
|
}
|
|
215
220
|
return {
|
|
221
|
+
inspectFences(request, now) {
|
|
222
|
+
assertRequest(isFenceList(request.fences) && isGeneration(request.generation), "invalid inspectFences request");
|
|
223
|
+
return transact(() => {
|
|
224
|
+
observeGeneration(request.generation, now);
|
|
225
|
+
return { epochs: epochs(request.fences), pending: pending(request.fences, now) };
|
|
226
|
+
});
|
|
227
|
+
},
|
|
216
228
|
beginWrite(request, now) {
|
|
217
229
|
assertRequest(
|
|
218
230
|
isFenceList(request.fences) && isDuration(request.leaseMs) && isGeneration(request.generation) && isToken(request.token),
|
|
@@ -261,6 +273,7 @@ function createCoordinatorCore(sql, newToken, transact, limits = {}) {
|
|
|
261
273
|
const refreshable = request.refresh === true && now >= Number(entry.soft_expires_at) && !lockLive && !pending(request.fences, now);
|
|
262
274
|
return {
|
|
263
275
|
epochs: Object.fromEntries(request.fences.map((fence) => [fence, current[fence] ?? 0])),
|
|
276
|
+
freshUntil: Math.min(Number(entry.soft_expires_at), Number(entry.expires_at)),
|
|
264
277
|
kind: "hit",
|
|
265
278
|
refreshToken: refreshable ? writeLock(request.key, request.lockMs, now, "refresh") : null,
|
|
266
279
|
value: String(entry.value)
|
|
@@ -331,6 +344,9 @@ var CacheCoordinator = class extends DurableObject {
|
|
|
331
344
|
migrate(ctx.storage.sql, transact);
|
|
332
345
|
this.core = createCoordinatorCore(ctx.storage.sql, () => crypto.randomUUID(), transact);
|
|
333
346
|
}
|
|
347
|
+
inspectFences(request) {
|
|
348
|
+
return this.core.inspectFences(request, Date.now());
|
|
349
|
+
}
|
|
334
350
|
read(request) {
|
|
335
351
|
return this.core.read(request, Date.now());
|
|
336
352
|
}
|
|
@@ -351,56 +367,408 @@ var CacheCoordinator = class extends DurableObject {
|
|
|
351
367
|
}
|
|
352
368
|
};
|
|
353
369
|
|
|
354
|
-
// src/cloudflare/
|
|
355
|
-
var
|
|
356
|
-
var
|
|
357
|
-
|
|
358
|
-
|
|
370
|
+
// src/cloudflare/regional-store.ts
|
|
371
|
+
var states = /* @__PURE__ */ new Map();
|
|
372
|
+
var MAX_NAMESPACES = 4096;
|
|
373
|
+
var MAX_FILLS = 1024;
|
|
374
|
+
var MAX_REFRESHES = 1024;
|
|
375
|
+
var MAX_FAILED_ENDS = 1024;
|
|
376
|
+
var MAX_TOKEN_QUEUES = 1024;
|
|
377
|
+
var MAX_TOKEN_OPERATIONS = 1024;
|
|
378
|
+
var REPAIR_BATCH = 4;
|
|
379
|
+
var REPAIR_BACKOFF_MS = 1e3;
|
|
380
|
+
var fencesFor = (fences) => [...new Set(fences)].sort();
|
|
381
|
+
function stateFor(identity) {
|
|
382
|
+
const existing = states.get(identity);
|
|
383
|
+
if (existing) return existing;
|
|
384
|
+
if (states.size >= MAX_NAMESPACES) return void 0;
|
|
385
|
+
const state = {
|
|
386
|
+
revision: 0,
|
|
387
|
+
floor: -1,
|
|
388
|
+
pending: /* @__PURE__ */ new Set(),
|
|
389
|
+
tokenQueues: /* @__PURE__ */ new Map(),
|
|
390
|
+
invalidations: 0,
|
|
391
|
+
unsafe: false,
|
|
392
|
+
unsafeVersion: 0,
|
|
393
|
+
overflow: false,
|
|
394
|
+
failedEnds: /* @__PURE__ */ new Map(),
|
|
395
|
+
repairing: false,
|
|
396
|
+
nextRepairAt: 0,
|
|
397
|
+
fills: /* @__PURE__ */ new Map(),
|
|
398
|
+
refreshes: /* @__PURE__ */ new Set()
|
|
399
|
+
};
|
|
400
|
+
states.set(identity, state);
|
|
401
|
+
return state;
|
|
402
|
+
}
|
|
403
|
+
function createRegionalStore(store, options) {
|
|
404
|
+
const { namespace, generation, ttlMs } = options;
|
|
405
|
+
if (!namespace || !generation || namespace.length > 8192 || generation.length > 8192 || !Number.isFinite(ttlMs) || ttlMs <= 0 || ttlMs > 5e3 || new TextEncoder().encode(JSON.stringify([namespace, generation])).byteLength > 16384)
|
|
406
|
+
return store;
|
|
407
|
+
const state = stateFor(namespace);
|
|
408
|
+
if (!state) return store;
|
|
409
|
+
const now = options.now ?? Date.now;
|
|
410
|
+
const regional = createRegionalCache(5e3, void 0, {
|
|
411
|
+
cache: options.cache,
|
|
412
|
+
now
|
|
413
|
+
});
|
|
414
|
+
const renewalStore = boundedD1Store(store);
|
|
415
|
+
const inspectFences = store.inspectFences?.bind(store);
|
|
416
|
+
const enabled = options.enabled !== false;
|
|
417
|
+
const blocked = () => state.unsafe || state.overflow || state.invalidations > 0 || state.pending.size > 0 || state.tokenQueues.size > 0;
|
|
418
|
+
const markUnsafe = () => {
|
|
419
|
+
state.unsafe = true;
|
|
420
|
+
state.unsafeVersion++;
|
|
421
|
+
};
|
|
422
|
+
const deferRepair = () => {
|
|
423
|
+
const at = now();
|
|
424
|
+
if (!Number.isSafeInteger(at)) {
|
|
425
|
+
markUnsafe();
|
|
426
|
+
state.nextRepairAt = Infinity;
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
state.nextRepairAt = Math.max(state.nextRepairAt, at + REPAIR_BACKOFF_MS);
|
|
430
|
+
};
|
|
431
|
+
const bump = () => {
|
|
432
|
+
state.revision++;
|
|
433
|
+
const at = now();
|
|
434
|
+
if (!Number.isSafeInteger(at)) markUnsafe();
|
|
435
|
+
else state.floor = Math.max(state.floor, at);
|
|
436
|
+
};
|
|
437
|
+
const serializeToken = (token, action) => {
|
|
438
|
+
let queue = state.tokenQueues.get(token);
|
|
439
|
+
if (!queue && state.tokenQueues.size >= MAX_TOKEN_QUEUES || queue && queue.count >= MAX_TOKEN_OPERATIONS) {
|
|
440
|
+
state.overflow = true;
|
|
441
|
+
bump();
|
|
442
|
+
return Promise.reject(
|
|
443
|
+
new Error("Regional write queue capacity exceeded")
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
if (!queue) {
|
|
447
|
+
queue = { tail: Promise.resolve(), count: 0 };
|
|
448
|
+
state.tokenQueues.set(token, queue);
|
|
449
|
+
}
|
|
450
|
+
const current = queue;
|
|
451
|
+
current.count++;
|
|
452
|
+
const result = current.tail.then(action).finally(() => {
|
|
453
|
+
current.count--;
|
|
454
|
+
if (current.count === 0 && state.tokenQueues.get(token) === current)
|
|
455
|
+
state.tokenQueues.delete(token);
|
|
456
|
+
});
|
|
457
|
+
current.tail = result.then(
|
|
458
|
+
() => void 0,
|
|
459
|
+
() => void 0
|
|
460
|
+
);
|
|
461
|
+
return result;
|
|
462
|
+
};
|
|
463
|
+
const keyFor = (key, fences) => regional.regionalKey(ttlMs, [
|
|
464
|
+
"cache-store-v2",
|
|
465
|
+
namespace,
|
|
466
|
+
generation,
|
|
467
|
+
key,
|
|
468
|
+
fencesFor(fences)
|
|
469
|
+
]);
|
|
470
|
+
const clean = () => {
|
|
471
|
+
for (const [token, fill] of state.fills)
|
|
472
|
+
if (fill.expires <= now()) state.fills.delete(token);
|
|
473
|
+
};
|
|
474
|
+
const sameFences = (left, right) => left.length === right.length && left.every((value, i) => value === right[i]);
|
|
475
|
+
const remember = (token, request, epochs, revision, started) => {
|
|
476
|
+
if (!enabled || !token || request.generation !== generation || !Number.isFinite(request.lockMs) || request.lockMs <= 0 || !sameFences(fencesFor(request.fences), fencesFor(Object.keys(epochs))) || Object.values(epochs).some(
|
|
477
|
+
(epoch) => !Number.isSafeInteger(epoch) || epoch < 0
|
|
478
|
+
) || blocked() || revision !== state.revision)
|
|
479
|
+
return;
|
|
480
|
+
clean();
|
|
481
|
+
if (state.fills.size >= MAX_FILLS && !state.fills.has(token)) return;
|
|
482
|
+
state.fills.set(token, {
|
|
483
|
+
key: request.key,
|
|
484
|
+
generation,
|
|
485
|
+
fences: fencesFor(request.fences),
|
|
486
|
+
epochs: Object.fromEntries(Object.entries(epochs)),
|
|
487
|
+
revision,
|
|
488
|
+
started,
|
|
489
|
+
expires: now() + request.lockMs
|
|
490
|
+
});
|
|
491
|
+
};
|
|
492
|
+
const payload = (value) => {
|
|
493
|
+
if (!value || typeof value !== "object" || !("value" in value) || typeof value.value !== "string" || !("sourceStarted" in value) || typeof value.sourceStarted !== "number" || !Number.isSafeInteger(value.sourceStarted) || value.sourceStarted < 0 || value.sourceStarted > now() || !("epochs" in value) || !value.epochs || typeof value.epochs !== "object" || Array.isArray(value.epochs))
|
|
494
|
+
return null;
|
|
495
|
+
const epochs = {};
|
|
496
|
+
for (const [name, epoch] of Object.entries(value.epochs)) {
|
|
497
|
+
if (!Number.isSafeInteger(epoch) || typeof epoch !== "number" || epoch < 0)
|
|
498
|
+
return null;
|
|
499
|
+
Object.defineProperty(epochs, name, {
|
|
500
|
+
value: epoch,
|
|
501
|
+
enumerable: true,
|
|
502
|
+
configurable: true
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
return { value: value.value, epochs, sourceStarted: value.sourceStarted };
|
|
506
|
+
};
|
|
507
|
+
const promote = async (revision, key, fences, started, lifetime, value) => {
|
|
508
|
+
if (!enabled || blocked() || revision !== state.revision || started <= state.floor || lifetime <= 0)
|
|
509
|
+
return;
|
|
510
|
+
const regionalKey = await keyFor(key, fences);
|
|
511
|
+
if (blocked() || revision !== state.revision || started <= state.floor)
|
|
512
|
+
return;
|
|
513
|
+
await regional.regionalWrite(
|
|
514
|
+
regionalKey,
|
|
515
|
+
Math.min(ttlMs, lifetime),
|
|
516
|
+
started,
|
|
517
|
+
{ ...value, sourceStarted: started }
|
|
518
|
+
);
|
|
519
|
+
};
|
|
520
|
+
const refreshAhead = (regionalKey, request, revision) => {
|
|
521
|
+
if (!options.waitUntil || blocked() || revision !== state.revision || state.refreshes.has(regionalKey) || state.refreshes.size >= MAX_REFRESHES)
|
|
522
|
+
return;
|
|
523
|
+
const copy = { ...request, fences: [...request.fences], refresh: false };
|
|
524
|
+
state.refreshes.add(regionalKey);
|
|
525
|
+
let cancelled = false;
|
|
526
|
+
const task = Promise.resolve().then(async () => {
|
|
527
|
+
if (cancelled || blocked() || revision !== state.revision) return;
|
|
528
|
+
const started = now();
|
|
529
|
+
const result = await renewalStore.read(copy);
|
|
530
|
+
const token = result.kind === "miss" ? result.lockToken : result.refreshToken;
|
|
531
|
+
if (token) {
|
|
532
|
+
try {
|
|
533
|
+
await renewalStore.release({ key: copy.key, lockToken: token });
|
|
534
|
+
} catch {
|
|
535
|
+
}
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (result.kind !== "hit" || typeof result.freshUntil !== "number" || !Number.isSafeInteger(result.freshUntil) || result.freshUntil <= started || !sameFences(
|
|
539
|
+
fencesFor(copy.fences),
|
|
540
|
+
fencesFor(Object.keys(result.epochs))
|
|
541
|
+
) || Object.values(result.epochs).some(
|
|
542
|
+
(epoch) => !Number.isSafeInteger(epoch) || epoch < 0
|
|
543
|
+
))
|
|
544
|
+
return;
|
|
545
|
+
await promote(
|
|
546
|
+
revision,
|
|
547
|
+
copy.key,
|
|
548
|
+
copy.fences,
|
|
549
|
+
started,
|
|
550
|
+
result.freshUntil - started,
|
|
551
|
+
{ value: result.value, epochs: result.epochs }
|
|
552
|
+
);
|
|
553
|
+
}).catch(() => void 0).finally(() => {
|
|
554
|
+
state.refreshes.delete(regionalKey);
|
|
555
|
+
});
|
|
359
556
|
try {
|
|
360
|
-
|
|
361
|
-
} catch
|
|
362
|
-
|
|
363
|
-
|
|
557
|
+
options.waitUntil(task);
|
|
558
|
+
} catch {
|
|
559
|
+
cancelled = true;
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
const repair = () => {
|
|
563
|
+
if (!options.waitUntil || state.repairing || now() < state.nextRepairAt || !state.unsafe && state.failedEnds.size === 0)
|
|
564
|
+
return;
|
|
565
|
+
state.repairing = true;
|
|
566
|
+
const failed = [...state.failedEnds.entries()].filter(([token]) => !state.tokenQueues.has(token)).slice(0, REPAIR_BATCH);
|
|
567
|
+
const unsafeVersion = state.unsafe ? state.unsafeVersion : null;
|
|
568
|
+
let cancelled = false;
|
|
569
|
+
const task = Promise.resolve().then(async () => {
|
|
570
|
+
if (cancelled) return;
|
|
571
|
+
let failedAttempt = false;
|
|
572
|
+
for (const [token, request] of failed) {
|
|
573
|
+
if (state.failedEnds.get(token) !== request) continue;
|
|
574
|
+
try {
|
|
575
|
+
if (state.tokenQueues.has(token)) continue;
|
|
576
|
+
await withinCoordinatorDeadline(
|
|
577
|
+
serializeToken(token, async () => {
|
|
578
|
+
if (state.failedEnds.get(token) !== request) return;
|
|
579
|
+
await store.endWrite({
|
|
580
|
+
token: request.token,
|
|
581
|
+
fences: [...request.fences]
|
|
582
|
+
});
|
|
583
|
+
if (state.failedEnds.get(token) === request) {
|
|
584
|
+
state.failedEnds.delete(token);
|
|
585
|
+
state.pending.delete(token);
|
|
586
|
+
bump();
|
|
587
|
+
}
|
|
588
|
+
})
|
|
589
|
+
);
|
|
590
|
+
} catch {
|
|
591
|
+
failedAttempt = true;
|
|
592
|
+
if (state.failedEnds.get(token) === request) {
|
|
593
|
+
state.failedEnds.delete(token);
|
|
594
|
+
state.failedEnds.set(token, request);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if (unsafeVersion !== null) {
|
|
599
|
+
try {
|
|
600
|
+
await withinCoordinatorDeadline(store.invalidate([GLOBAL_FENCE]));
|
|
601
|
+
if (state.unsafeVersion === unsafeVersion) {
|
|
602
|
+
state.unsafe = false;
|
|
603
|
+
bump();
|
|
604
|
+
}
|
|
605
|
+
} catch {
|
|
606
|
+
failedAttempt = true;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (failedAttempt) deferRepair();
|
|
610
|
+
}).catch(() => {
|
|
611
|
+
deferRepair();
|
|
612
|
+
}).finally(() => {
|
|
613
|
+
state.repairing = false;
|
|
614
|
+
});
|
|
615
|
+
try {
|
|
616
|
+
options.waitUntil(task);
|
|
617
|
+
} catch {
|
|
618
|
+
cancelled = true;
|
|
619
|
+
deferRepair();
|
|
364
620
|
}
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// src/cloudflare/store-options.ts
|
|
369
|
-
var WRITE_PATH_ATTEMPTS = 3;
|
|
370
|
-
var MAX_COORDINATOR_NAME_LENGTH = 256;
|
|
371
|
-
var LOCATION_HINTS = ["wnam", "enam", "sam", "weur", "eeur", "apac", "oc", "afr", "me"];
|
|
372
|
-
var isLocationHint = (value) => LOCATION_HINTS.some((hint) => hint === value);
|
|
373
|
-
function resolveStoreOptions(options) {
|
|
374
|
-
const { attempts = WRITE_PATH_ATTEMPTS, locationHint, name } = options;
|
|
375
|
-
const problems = [
|
|
376
|
-
!(typeof name === "string" && name.length > 0 && name.length <= MAX_COORDINATOR_NAME_LENGTH) && `name must be 1..${MAX_COORDINATOR_NAME_LENGTH} characters and identify one D1 database`,
|
|
377
|
-
!(Number.isInteger(attempts) && attempts >= 1 && attempts <= 10) && "attempts must be an integer in 1..10",
|
|
378
|
-
locationHint !== void 0 && !isLocationHint(locationHint) && `locationHint must be one of ${LOCATION_HINTS.join(", ")}`
|
|
379
|
-
].filter((problem) => typeof problem === "string");
|
|
380
|
-
if (problems.length > 0) throw new TypeError(`cache store: ${problems.join("; ")}`);
|
|
381
|
-
return {
|
|
382
|
-
attempts,
|
|
383
|
-
name,
|
|
384
|
-
sleep: options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
|
|
385
|
-
...locationHint ? { locationHint } : {}
|
|
386
621
|
};
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// src/cloudflare/store.ts
|
|
390
|
-
function createDurableObjectStore(namespace, options) {
|
|
391
|
-
const { attempts, locationHint, name, sleep } = resolveStoreOptions(options);
|
|
392
|
-
const stub = () => namespace.get(namespace.idFromName(name), locationHint ? { locationHint } : void 0);
|
|
393
|
-
const withRetry = (call) => retryTransient(() => call(stub()), attempts, sleep);
|
|
394
622
|
return {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
623
|
+
...inspectFences ? { inspectFences } : {},
|
|
624
|
+
beginWrite(request) {
|
|
625
|
+
const attempted = { ...request, fences: [...request.fences] };
|
|
626
|
+
if (state.failedEnds.has(attempted.token)) state.overflow = true;
|
|
627
|
+
return serializeToken(attempted.token, async () => {
|
|
628
|
+
const existed = state.pending.has(attempted.token);
|
|
629
|
+
if (state.failedEnds.has(attempted.token)) state.overflow = true;
|
|
630
|
+
state.pending.add(attempted.token);
|
|
631
|
+
bump();
|
|
632
|
+
try {
|
|
633
|
+
await store.beginWrite(attempted);
|
|
634
|
+
} catch (error) {
|
|
635
|
+
if (!existed) state.pending.delete(attempted.token);
|
|
636
|
+
bump();
|
|
637
|
+
throw error;
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
},
|
|
641
|
+
endWrite(request) {
|
|
642
|
+
const attempted = { token: request.token, fences: [...request.fences] };
|
|
643
|
+
return serializeToken(attempted.token, async () => {
|
|
644
|
+
const priorFailure = state.failedEnds.get(attempted.token);
|
|
645
|
+
state.pending.add(attempted.token);
|
|
646
|
+
state.invalidations++;
|
|
647
|
+
bump();
|
|
648
|
+
try {
|
|
649
|
+
await store.endWrite({
|
|
650
|
+
token: attempted.token,
|
|
651
|
+
fences: [...attempted.fences]
|
|
652
|
+
});
|
|
653
|
+
if (state.failedEnds.get(attempted.token) === priorFailure && (!priorFailure || sameFences(
|
|
654
|
+
fencesFor(priorFailure.fences),
|
|
655
|
+
fencesFor(attempted.fences)
|
|
656
|
+
))) {
|
|
657
|
+
state.failedEnds.delete(attempted.token);
|
|
658
|
+
state.pending.delete(attempted.token);
|
|
659
|
+
}
|
|
660
|
+
} catch (error) {
|
|
661
|
+
const previous = state.failedEnds.get(attempted.token);
|
|
662
|
+
if (previous && !sameFences(fencesFor(previous.fences), fencesFor(attempted.fences)))
|
|
663
|
+
state.overflow = true;
|
|
664
|
+
else if (previous || state.failedEnds.size < MAX_FAILED_ENDS)
|
|
665
|
+
state.failedEnds.set(attempted.token, attempted);
|
|
666
|
+
else state.overflow = true;
|
|
667
|
+
deferRepair();
|
|
668
|
+
throw error;
|
|
669
|
+
} finally {
|
|
670
|
+
state.invalidations--;
|
|
671
|
+
bump();
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
},
|
|
675
|
+
async invalidate(fences) {
|
|
676
|
+
state.invalidations++;
|
|
677
|
+
bump();
|
|
678
|
+
try {
|
|
679
|
+
await store.invalidate(fences);
|
|
680
|
+
} catch (error) {
|
|
681
|
+
markUnsafe();
|
|
682
|
+
deferRepair();
|
|
683
|
+
throw error;
|
|
684
|
+
} finally {
|
|
685
|
+
state.invalidations--;
|
|
686
|
+
bump();
|
|
687
|
+
}
|
|
688
|
+
},
|
|
689
|
+
async read(request) {
|
|
690
|
+
if (blocked()) repair();
|
|
691
|
+
const started = now();
|
|
692
|
+
const revision = state.revision;
|
|
693
|
+
if (enabled && request.generation === generation && !blocked()) {
|
|
694
|
+
const key = await keyFor(request.key, request.fences);
|
|
695
|
+
const hit = await regional.regionalRead(key, ttlMs);
|
|
696
|
+
const cached = hit && payload(hit.value);
|
|
697
|
+
if (hit && cached && revision === state.revision && !blocked() && cached.sourceStarted > state.floor && hit.expires > now() && hit.expires > cached.sourceStarted && hit.expires <= cached.sourceStarted + ttlMs && sameFences(
|
|
698
|
+
fencesFor(request.fences),
|
|
699
|
+
fencesFor(Object.keys(cached.epochs))
|
|
700
|
+
)) {
|
|
701
|
+
if (key && hit.expires - now() <= ttlMs / 2)
|
|
702
|
+
refreshAhead(key, request, revision);
|
|
703
|
+
return {
|
|
704
|
+
kind: "hit",
|
|
705
|
+
value: cached.value,
|
|
706
|
+
epochs: cached.epochs,
|
|
707
|
+
refreshToken: null,
|
|
708
|
+
freshUntil: hit.expires
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
const result = await store.read(request);
|
|
713
|
+
if (result.kind === "miss")
|
|
714
|
+
remember(
|
|
715
|
+
result.pending ? null : result.lockToken,
|
|
716
|
+
request,
|
|
717
|
+
result.epochs,
|
|
718
|
+
revision,
|
|
719
|
+
started
|
|
720
|
+
);
|
|
721
|
+
else {
|
|
722
|
+
remember(
|
|
723
|
+
result.refreshToken,
|
|
724
|
+
request,
|
|
725
|
+
result.epochs,
|
|
726
|
+
revision,
|
|
727
|
+
started
|
|
728
|
+
);
|
|
729
|
+
if (request.generation === generation && result.refreshToken === null && typeof result.freshUntil === "number" && Number.isSafeInteger(result.freshUntil) && result.freshUntil > started && sameFences(
|
|
730
|
+
fencesFor(request.fences),
|
|
731
|
+
fencesFor(Object.keys(result.epochs))
|
|
732
|
+
)) {
|
|
733
|
+
await promote(
|
|
734
|
+
revision,
|
|
735
|
+
request.key,
|
|
736
|
+
request.fences,
|
|
737
|
+
started,
|
|
738
|
+
result.freshUntil - started,
|
|
739
|
+
{ value: result.value, epochs: result.epochs }
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return result;
|
|
744
|
+
},
|
|
745
|
+
async release(request) {
|
|
746
|
+
state.fills.delete(request.lockToken);
|
|
747
|
+
await store.release(request);
|
|
748
|
+
},
|
|
749
|
+
async write(request) {
|
|
750
|
+
const started = now();
|
|
751
|
+
clean();
|
|
752
|
+
const fill = state.fills.get(request.lockToken);
|
|
753
|
+
state.fills.delete(request.lockToken);
|
|
754
|
+
const result = await store.write(request);
|
|
755
|
+
if (result.ok && fill && fill.key === request.key && fill.generation === generation && fill.expires > started && Object.keys(request.snapshot).length > 0 && Object.entries(request.snapshot).every(
|
|
756
|
+
([fence, epoch]) => Object.hasOwn(fill.epochs, fence) && fill.epochs[fence] === epoch
|
|
757
|
+
)) {
|
|
758
|
+
await promote(
|
|
759
|
+
fill.revision,
|
|
760
|
+
request.key,
|
|
761
|
+
fill.fences,
|
|
762
|
+
Math.min(started, fill.started),
|
|
763
|
+
Math.min(ttlMs, request.ttlMs, request.softTtlMs),
|
|
764
|
+
{ value: request.value, epochs: fill.epochs }
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
return result;
|
|
768
|
+
}
|
|
401
769
|
};
|
|
402
770
|
}
|
|
403
771
|
|
|
404
|
-
export { CacheCoordinator,
|
|
772
|
+
export { CacheCoordinator, createRegionalStore };
|
|
405
773
|
//# sourceMappingURL=index.js.map
|
|
406
774
|
//# sourceMappingURL=index.js.map
|