@astilba/cache 0.0.0-stage → 0.1.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.
@@ -0,0 +1,3528 @@
1
+ import { createHash, createHmac } from "node:crypto";
2
+ //#region src/errors.ts
3
+ /**
4
+ * Thrown by every public entry point during the correctness-kernel phase. A RED
5
+ * invariant that fails with `NotImplementedError` is red for the right reason
6
+ * (the kernel is unimplemented), as opposed to a toolchain/config failure.
7
+ */
8
+ var NotImplementedError = class extends Error {
9
+ constructor(symbol) {
10
+ super(`${symbol}: not implemented in the correctness-kernel phase`);
11
+ this.name = "NotImplementedError";
12
+ }
13
+ };
14
+ /**
15
+ * The single kernel-phase stub body. Every public function/method delegates here
16
+ * so the switch to real logic is a body replacement with zero signature churn.
17
+ * Callers pass their arguments through so parameters are genuinely referenced;
18
+ * inside an `async` method the thrown error becomes a rejected promise.
19
+ */
20
+ function notImplemented(symbol, ..._args) {
21
+ throw new NotImplementedError(symbol);
22
+ }
23
+ /**
24
+ * Distinguishes cache-originated timeouts (retriable — evidence about origin
25
+ * health) from caller aborts (not evidence of origin health).
26
+ */
27
+ var CacheTimeoutError = class extends Error {
28
+ source;
29
+ constructor(source, message) {
30
+ super(message ?? `cache operation timed out (source: ${source})`);
31
+ this.name = "CacheTimeoutError";
32
+ this.source = source;
33
+ }
34
+ };
35
+ /**
36
+ * Thrown by a `strong` read when the registry is unreachable. Opt out with
37
+ * `onUnavailable: 'eventual'`.
38
+ */
39
+ var RegistryUnavailableError = class extends Error {
40
+ /**
41
+ * `options.cause` carries the error this one displaced, where there was one. The serve-time
42
+ * re-validation of a graced candidate runs INSIDE a failed fill: if the registry is unreachable
43
+ * there, this error is what the caller sees instead of the factory failure that started it, so
44
+ * the factory failure rides along rather than being dropped. Nothing is lost and callers get one
45
+ * uniform type. Widened additively — `new RegistryUnavailableError(msg)` is unchanged.
46
+ */
47
+ constructor(message, options) {
48
+ super(message ?? "registry unreachable", options);
49
+ this.name = "RegistryUnavailableError";
50
+ }
51
+ };
52
+ /**
53
+ * Thrown by a read whose tag knowledge is UNKNOWN under `unknownPolicy: 'error'`.
54
+ *
55
+ * WHAT MAKES THIS DISTINCT FROM A MISS, AND WHY THE DISTINCTION IS THE POINT. Every other
56
+ * fail-closed resolution refills from origin, which is hard-safe exactly when the origin is truth.
57
+ * `'error'` is the policy for the case where it is NOT: a namespace under a takedown or a legal
58
+ * embargo. There, an origin that has not caught up yet can lose a race with the purge and hand back
59
+ * exactly the content that was supposed to vanish. So this policy declines to guess: it neither
60
+ * serves the unknown value nor replaces it
61
+ * with a fresh one, and the caller — who knows what the content is — decides. A policy that resolved
62
+ * to the same refill as `'miss'` would be a switch that reads as protection and does nothing, which
63
+ * is worse than no switch at all.
64
+ *
65
+ * Carries the key and the tags whose knowledge was unknown, so a caller can tell an embargoed
66
+ * dependency from a cold one. EXACTLY those: the tags this read could not establish, and no others.
67
+ * A payload of everything the entry declared cannot answer the question the field exists for — the
68
+ * caller would have to guess which of them was the problem, which is the guess this policy refuses
69
+ * to make on their behalf. Reserved (`__`-prefixed) tags never appear here: they are the kernel's
70
+ * own bookkeeping, they are unknown for exactly the same reason the user's are, and naming them
71
+ * tells the caller nothing they can act on.
72
+ */
73
+ var UnknownTagError = class extends Error {
74
+ key;
75
+ tags;
76
+ /**
77
+ * `armedBy` names what selected this resolution — an explicit `unknownPolicy: 'error'`, or the
78
+ * `takedownSensitive` override that outranks it. The message used to state the former
79
+ * unconditionally, which was wrong precisely on the takedown path: the one place where the
80
+ * operator most needs to know which switch fired is the one where the message named the other.
81
+ */
82
+ constructor(key, tags, armedBy = "unknownPolicy") {
83
+ const cause = armedBy === "takedownSensitive" ? "takedownSensitive is set" : "unknownPolicy is 'error'";
84
+ super(`read for key ${JSON.stringify(key)} has unknown tag knowledge and ${cause} (§5a, E9)`);
85
+ this.name = "UnknownTagError";
86
+ this.key = key;
87
+ this.tags = tags;
88
+ }
89
+ };
90
+ /** A typed HTTP error carrying the offending status and response. */
91
+ var HttpError = class extends Error {
92
+ status;
93
+ response;
94
+ constructor(response) {
95
+ super(`HTTP ${response.status}`);
96
+ this.name = "HttpError";
97
+ this.status = response.status;
98
+ this.response = response;
99
+ }
100
+ };
101
+ /**
102
+ * Thrown at the `getOrSet`/`getOrSetEntry` write boundary when a caller-supplied
103
+ * tag is malformed — in particular a user tag beginning with the reserved `__`
104
+ * prefix. A real runtime error, never `NotImplementedError`: tag
105
+ * validation is not deferred kernel logic.
106
+ */
107
+ var InvalidTagError = class extends Error {
108
+ tag;
109
+ constructor(tag, reason) {
110
+ super(`invalid tag ${JSON.stringify(tag)}: ${reason}`);
111
+ this.name = "InvalidTagError";
112
+ this.tag = tag;
113
+ }
114
+ };
115
+ /**
116
+ * Thrown by `duration()` when a computed duration is malformed — a non-positive or
117
+ * non-finite `value`, or (critical case) a `value × unitFactor` product that
118
+ * is not a finite, positive, safe integer once multiplied out. A real
119
+ * runtime error, never `NotImplementedError`: duration validation is not deferred kernel
120
+ * logic. Mirrors `InvalidTagError` — the builder rejects a bad input, it never coerces one.
121
+ */
122
+ var InvalidDurationError = class extends Error {
123
+ value;
124
+ unit;
125
+ constructor(value, unit, reason) {
126
+ super(`invalid duration ${JSON.stringify(String(value) + unit)}: ${reason}`);
127
+ this.name = "InvalidDurationError";
128
+ this.value = value;
129
+ this.unit = unit;
130
+ }
131
+ };
132
+ /**
133
+ * Thrown by the plain `getOrSet` form when a fill is fenced past its write-back
134
+ * retry attempt budget: a hard delete keeps landing mid-fill/mid-
135
+ * refresh so the accepted result never postdates every purge visible at
136
+ * write-back, and the fill fails within its attempt budget rather than ever
137
+ * storing or serving a fenced value. A real runtime outcome, never
138
+ * `NotImplementedError` — the entry form (`getOrSetEntry`) surfaces the same
139
+ * outcome as `{ value: undefined }` instead of throwing.
140
+ */
141
+ var FencedError = class extends Error {
142
+ key;
143
+ constructor(key, message) {
144
+ super(message ?? `fill for key ${JSON.stringify(key)} fenced past its write-back attempt budget (§5.0/MI6)`);
145
+ this.name = "FencedError";
146
+ this.key = key;
147
+ }
148
+ };
149
+ /**
150
+ * Thrown when a factory declares a dependency after it has settled: a
151
+ * `ctx.dependsOn`/`ctx.setTags` call reaching the context AFTER the factory promise
152
+ * resolved or rejected. The fill's tag set closes at settle, so a later declaration is
153
+ * outside the value's lifetime and cannot join it. A real runtime error, never
154
+ * `NotImplementedError`. Carries the offending key.
155
+ */
156
+ var FactorySettledError = class extends Error {
157
+ key;
158
+ constructor(key) {
159
+ super(`factory for key ${JSON.stringify(key)} declared a dependency after it settled (§2.1, E24)`);
160
+ this.name = "FactorySettledError";
161
+ this.key = key;
162
+ }
163
+ };
164
+ /**
165
+ * The `EntryFactoryCtx.skip()` sentinel (M10, invariant 24) — a private
166
+ * throw/catch signal, analogous to how a classified factory error rides
167
+ * `fail()`. `skip()` is typed `never` so it never falsely resolves `undefined`
168
+ * as a caller's `T`; `fillAndStore` catches this exact class and turns it into
169
+ * a `{ skipped: true, value: undefined }` outcome — nothing is stored, nothing
170
+ * is served. Never part of the public surface (not exported from `index.ts`).
171
+ */
172
+ var SkipSignalError = class extends Error {
173
+ constructor() {
174
+ super("ctx.skip() called — the fill is skipped (nothing stored, nothing served)");
175
+ this.name = "SkipSignalError";
176
+ }
177
+ };
178
+ //#endregion
179
+ //#region src/registry/mirror-keys.ts
180
+ /** The default `regId` — single-registry deployments stay zero-config. */
181
+ const DEFAULT_REG_ID = "default";
182
+ /** The `regId` charset + length bound (`[a-z0-9._-]`, ≤ 64). */
183
+ const REG_ID_PATTERN = /^[a-z0-9._-]{1,64}$/u;
184
+ /**
185
+ * Does `regId` satisfy the charset/length (`[a-z0-9._-]`, 1–64 chars)? The pure predicate
186
+ * behind `assertRegId` — the SINGLE source of the grammar, so the machine's throwing gate and the
187
+ * adapter's name-derivation refusal (`resolveRegId`) test the same rule and can never drift.
188
+ */
189
+ function isRegId(regId) {
190
+ return REG_ID_PATTERN.test(regId);
191
+ }
192
+ /**
193
+ * Validate a `regId` against the charset/length (`[a-z0-9._-]`, 1–64 chars) — the gate
194
+ * that makes the scoped keyspace injective without escaping (the delimiter `:` cannot occur
195
+ * inside a `regId`). Called at `configure` time (the machine's single seeding path), so a
196
+ * malformed id fails LOUD before it can ever mint an ambiguous key. Throws on violation.
197
+ */
198
+ function assertRegId(regId) {
199
+ if (!isRegId(regId)) throw new Error(`invalid regId ${JSON.stringify(regId)} — must match [a-z0-9._-], length 1-64 (E19)`);
200
+ }
201
+ /**
202
+ * Build the registry-scoped mirror keys for `regId`. `regId` is expected to be
203
+ * already validated (it enters the machine only via `configure`, which calls `assertRegId`);
204
+ * this is a pure formatter on the hot flush/resync path, so it does not re-validate. Both the
205
+ * writer (execute.ts) and the reader (invalidation.ts) address keys ONLY through here.
206
+ */
207
+ function mirrorKeys(regId) {
208
+ const prefix = `__reg:${regId}:`;
209
+ return {
210
+ deltaKey: (seq) => `${prefix}delta:${seq}`,
211
+ snapKey: (seq) => `${prefix}snap:${seq}`,
212
+ stateKey: `${prefix}state`
213
+ };
214
+ }
215
+ //#endregion
216
+ //#region src/tags.ts
217
+ /** The two reserved tag namespaces: the per-key digest and the
218
+ * per-namespace watermark. A user tag beginning with this prefix is rejected
219
+ * at write — only the kernel itself may stamp `__k:<h>` / `__ns:<name>`. */
220
+ const RESERVED_TAG_PREFIX = "__";
221
+ /** Whether `tag` falls in the reserved `__`-prefixed namespace. */
222
+ function isReservedTag(tag) {
223
+ return tag.startsWith(RESERVED_TAG_PREFIX);
224
+ }
225
+ /**
226
+ * Validate one caller-supplied tag at the write boundary. Throws a
227
+ * real `InvalidTagError` — never `NotImplementedError` — the instant a
228
+ * `__`-prefixed tag reaches `getOrSet`/`getOrSetEntry`/a purge selector, so a
229
+ * forged reserved tag can never be smuggled in as a "user" tag.
230
+ *
231
+ * Division of labor: the CHARSET/length/non-empty authority is the builders — `t`
232
+ * validates-and-rejects, `compound` escapes — so a `Tag` they produce is already
233
+ * well-formed. This write-time check is deliberately the NARROW `__`-prefix backstop,
234
+ * not a re-validation: it catches only the one class the type system cannot — a
235
+ * `__`-prefixed string force-branded to `Tag` (a raw `as Tag` cast, a harness `mkTag`,
236
+ * any hand-forged brand) that never passed through a builder. It intentionally does NOT
237
+ * re-check charset — narrow, not weak.
238
+ */
239
+ function assertUserTag(tag) {
240
+ if (isReservedTag(tag)) throw new InvalidTagError(tag, `user tags may not begin with the reserved "${RESERVED_TAG_PREFIX}" prefix (§2.3, MI13)`);
241
+ }
242
+ /** The full tag charset: the design's `[a-z0-9:_\-.|]` extended with
243
+ * `%` (`compound()`'s escape byte). One or more of these, nothing else. `-` is placed
244
+ * last so it is a literal, not a range. */
245
+ const TAG_CHARSET = /^[a-z0-9:_.|%-]+$/u;
246
+ /** Maximum tag size — a UTF-8 BYTE budget, not code units. */
247
+ const MAX_TAG_BYTES = 256;
248
+ /** UTF-8 byte length of `s` — the tag budget is bytes, not string length. */
249
+ function byteLength(s) {
250
+ return new TextEncoder().encode(s).length;
251
+ }
252
+ /**
253
+ * Template helper for a single tag, e.g. ``t`book:${id}` ``. The tag TRUST
254
+ * BOUNDARY: the interpolations (`values`) are UNTRUSTED user data, so `t` assembles the
255
+ * template, then VALIDATES-and-REJECTS the final string against the full grammar
256
+ * (non-empty, charset, ≤256 UTF-8 bytes, not `__`-prefixed) — throwing `InvalidTagError`
257
+ * on any violation. It never escapes-by-guess: escaping is `compound()`'s separate
258
+ * contract, and silently rewriting an interpolation is exactly how tag injection slips
259
+ * through. On success returns the branded `Tag`.
260
+ */
261
+ function t(strings, ...values) {
262
+ let assembled = "";
263
+ for (const [i, literal] of strings.entries()) {
264
+ assembled += literal;
265
+ if (i < values.length) assembled += String(values[i]);
266
+ }
267
+ if (assembled.length === 0) throw new InvalidTagError(assembled, "a tag must be non-empty (§2.3)");
268
+ assertUserTag(assembled);
269
+ if (!TAG_CHARSET.test(assembled)) throw new InvalidTagError(assembled, "a tag character is outside the [a-z0-9:_-.|%] grammar (§2.3, E10)");
270
+ if (byteLength(assembled) > MAX_TAG_BYTES) throw new InvalidTagError(assembled, `a tag may not exceed ${MAX_TAG_BYTES} UTF-8 bytes (§2.3)`);
271
+ return assembled;
272
+ }
273
+ /**
274
+ * Escapes one `compound()` argument: `%` first (`%25`), then `:`
275
+ * (`%3a`) and `|` (`%7c`), lowercase hex. Escaping `%` first guarantees the
276
+ * mapping is injective — an escape-lookalike in the input (a literal `%3a`)
277
+ * is itself escaped to `%253a`, so it can never be produced by escaping a raw
278
+ * `:` (which yields `%3a`, with no leading `%25`).
279
+ */
280
+ function escapePart(part) {
281
+ return String(part).replaceAll("%", "%25").replaceAll(":", "%3a").replaceAll("|", "%7c");
282
+ }
283
+ /**
284
+ * Injective compound tag over an argument vector, e.g. `compound('book', id,
285
+ * 'locale', locale)`. Escaping; empty vector distinct from
286
+ * `['']`.
287
+ *
288
+ * Every part is escaped so an escaped part can never itself
289
+ * contain an unescaped `|`; joining escaped parts on `|` is therefore an
290
+ * injective join. An explicit leading arity count (itself `|`-terminated)
291
+ * makes the count unambiguous — it is always a decimal-digit run followed by
292
+ * the first `|` — which in turn makes vectors of differing length (including
293
+ * the empty vector vs. `['']`) distinct regardless of what the escaped parts
294
+ * contain.
295
+ */
296
+ function compound(...parts) {
297
+ return `${parts.length}|${parts.map(escapePart).join("|")}`;
298
+ }
299
+ /**
300
+ * A globally-scoped tag — its own scope-prefix escape hatch. Deferred to v0.4:
301
+ * its escape-FORM is a wire-compatibility decision reserved for the scope-qualification
302
+ * design, and ships with that consumer rather than standalone here.
303
+ */
304
+ function globalTag(name) {
305
+ return notImplemented("globalTag (deferred to v0.4)", name);
306
+ }
307
+ //#endregion
308
+ //#region src/telemetry.ts
309
+ /**
310
+ * The telemetry event-name CATALOG — the single, kernel-hosted registry of every
311
+ * event `type` this library emits, the same shape as the exported error classes. Each member is an
312
+ * event NAME; the value equals the key so a producer can reference a name as `TELEMETRY_EVENTS.<name>`
313
+ * (a literal type, refactor-safe) while `TelemetryEventName` (below) is the union of them all.
314
+ *
315
+ * The names are grouped by their emitting LAYER (kernel / L1 driver / react-router adapter /
316
+ * cloudflare adapter) — the annotation the charter requires. The machine-checked layer map (which
317
+ * fails to compile if a member is unannotated) lives in the catalog's exhaustiveness tooth in the
318
+ * test lane.
319
+ *
320
+ * GROWTH: adding a name is ADDITIVE and consumer-SAFE (a sink RECEIVES events, so a producer that
321
+ * emits a wider vocabulary hands a consumer a value from a set it already types against — narrowing,
322
+ * not widening, at the receiver) — so a new name is a MINOR-version change. Removing or renaming one
323
+ * is breaking.
324
+ */
325
+ const TELEMETRY_EVENTS = {
326
+ writeback_throttled: "writeback_throttled",
327
+ l2_write_error: "l2_write_error",
328
+ singleflight_option_mismatch: "singleflight_option_mismatch",
329
+ store_read_suppressed: "store_read_suppressed",
330
+ neg_suppressed: "neg_suppressed",
331
+ private_evicted: "private_evicted",
332
+ poll_tick_failed: "poll_tick_failed",
333
+ l3_ineligible: "l3_ineligible",
334
+ regid_divergence: "regid_divergence",
335
+ bus_dial_failed: "bus_dial_failed",
336
+ strong_degraded: "strong_degraded",
337
+ registry_degraded: "registry_degraded",
338
+ state_stale: "state_stale"
339
+ };
340
+ /** Report a swallowed sink failure to the injected hook, itself guarded. The hook is app code at the
341
+ * very end of the swallow chain, so its own failure has no meta-channel to report to — swallowed, so
342
+ * `safeEmit` cannot throw or float through it. */
343
+ function reportSinkError(hook, error) {
344
+ if (hook === void 0) return;
345
+ const called = hook;
346
+ try {
347
+ Promise.resolve(called(error)).catch(() => {});
348
+ } catch {}
349
+ }
350
+ /**
351
+ * Emit ONE event through an app-supplied sink so that NOTHING it does can escape (
352
+ * the swallow-wrap extracted from the poll driver and made shared). A sink is arbitrary app code;
353
+ * both of its failure modes are neutralized here:
354
+ *
355
+ * • a SYNCHRONOUS throw → the `try`/`catch` (without it the caller's own path would throw);
356
+ * • a REJECTING thenable → the returned value is ADOPTED (`Promise.resolve(...).catch(...)`) so a
357
+ * rejecting `async` sink can never float out as an unhandled rejection.
358
+ *
359
+ * On either failure the injected `onSinkError` hook is called exactly once for that emit. `safeEmit`
360
+ * NEVER throws and returns nothing the caller must await (adopt-don't-await: it must not inherit
361
+ * app-code latency on a background or response path).
362
+ *
363
+ * The TYPE is the structural tooth: `event` is a `TelemetryEvent`, whose `type` is the CATALOG
364
+ * union — so an off-catalog event name is a COMPILE error at the call site, never a grep finding.
365
+ */
366
+ function safeEmit(sink, event, onSinkError) {
367
+ if (sink === void 0) return;
368
+ const called = sink;
369
+ try {
370
+ Promise.resolve(called(event)).catch((error) => {
371
+ reportSinkError(onSinkError, error);
372
+ });
373
+ } catch (error) {
374
+ reportSinkError(onSinkError, error);
375
+ }
376
+ }
377
+ //#endregion
378
+ //#region src/invalidation.ts
379
+ /** The one reason class a `strong_degraded` emit carries today. */
380
+ const STRONG_DEGRADED_REGISTRY_UNREACHABLE = "registry_unreachable";
381
+ /** The channel's starting state, and the one a reader returns to saying rather than assuming: it has
382
+ * never heard a `hello`. Named once so the three sites that compare against it cannot drift. */
383
+ const NEVER_ESTABLISHED = "never-established";
384
+ /** Build the `transient-miss` retry signal for `seq` (the single runtime literal source). */
385
+ function transientMiss(seq) {
386
+ return {
387
+ kind: "transient-miss",
388
+ seq
389
+ };
390
+ }
391
+ /** Narrow an `unknown` to an index-readable object, or `undefined`. The
392
+ * single narrowing assertion in this module: every field read off the result is
393
+ * individually type-checked below before use, so the record view is sound. */
394
+ function parseValueObject(value) {
395
+ if (typeof value !== "object" || value === null) return;
396
+ return value;
397
+ }
398
+ /** Parse a stored JSON string to an object, or `undefined` on a malformed body. */
399
+ function parseJsonObject(raw) {
400
+ let parsed;
401
+ try {
402
+ parsed = JSON.parse(raw);
403
+ } catch {
404
+ return;
405
+ }
406
+ return parseValueObject(parsed);
407
+ }
408
+ /** Read the `state` pointer shape from stored bytes. Malformed — including a
409
+ * non-string or absent `regId` echo — ⇒ undefined (fail closed). */
410
+ function readPointer(raw) {
411
+ const o = parseJsonObject(raw);
412
+ if (!o || typeof o.epoch !== "number" || typeof o.lastBatch !== "number" || typeof o.regId !== "string") return;
413
+ const snapBatch = typeof o.snapBatch === "number" ? o.snapBatch : 0;
414
+ const flushedAt = typeof o.flushedAt === "number" ? o.flushedAt : 0;
415
+ return {
416
+ epoch: o.epoch,
417
+ flushedAt,
418
+ lastBatch: o.lastBatch,
419
+ regId: o.regId,
420
+ snapBatch
421
+ };
422
+ }
423
+ /**
424
+ * Read one watermark change from a parsed value. A CORRUPT change ⇒ `undefined`
425
+ * (fail closed), DISTINGUISHED from a legitimately-absent optional axis:
426
+ * • a PRESENT-but-non-number `softEpoch`/`hardEpoch` (the field is on the object but is not a
427
+ * `number`) is corruption — dropping it and applying the rest would silently delete a purge
428
+ * from history (a serve-dead vector under non-canonical KV writes);
429
+ * • an AXIS-EMPTY change (a `tag` but NEITHER a numeric `softEpoch` NOR a numeric `hardEpoch`)
430
+ * violates the ≥1-axis rule.
431
+ * An ABSENT optional axis stays legal — a change may legitimately touch a single axis. A corrupt
432
+ * change holes its whole batch via `readFrame` (never a partial-apply); the caller fails closed.
433
+ */
434
+ function readChange(value) {
435
+ const rec = parseValueObject(value);
436
+ if (!rec) return;
437
+ const { tag } = rec;
438
+ if (typeof tag !== "string") return;
439
+ const soft = rec.softEpoch;
440
+ const hard = rec.hardEpoch;
441
+ if ("softEpoch" in rec && typeof soft !== "number" || "hardEpoch" in rec && typeof hard !== "number") return;
442
+ if (typeof soft !== "number" && typeof hard !== "number") return;
443
+ return {
444
+ tag,
445
+ ...typeof soft === "number" ? { softEpoch: soft } : {},
446
+ ...typeof hard === "number" ? { hardEpoch: hard } : {}
447
+ };
448
+ }
449
+ /**
450
+ * Read a `__delta` batch frame from stored bytes. Malformed ⇒ undefined. Exported so the
451
+ * doBus wire codec (`src/adapters/cloudflare/wire.ts`) validates a wire-carried frame through the
452
+ * SAME frame parser the KV-resync path uses — one parser for the durable mirror AND the live channel,
453
+ * so a corrupt wire change fails closed exactly like a corrupt `__delta` byte (never a partial
454
+ * apply). Additive export only: no reader branch changes, no behavior change (the corrupt-change
455
+ * fail-closed semantics are pinned by `reader-corrupt-change-fail-closed`).
456
+ */
457
+ function readFrame(raw) {
458
+ const o = parseJsonObject(raw);
459
+ if (!o || typeof o.fromEpoch !== "number" || typeof o.toEpoch !== "number" || !Array.isArray(o.changes)) return;
460
+ const changes = [];
461
+ for (const entry of o.changes) {
462
+ const change = readChange(entry);
463
+ if (!change) return;
464
+ changes.push(change);
465
+ }
466
+ return {
467
+ changes,
468
+ fromEpoch: o.fromEpoch,
469
+ toEpoch: o.toEpoch
470
+ };
471
+ }
472
+ /** Merge a frame's watermark changes into a knowledge map (max-on-write). */
473
+ function applyChanges(known, frame) {
474
+ for (const change of frame.changes) {
475
+ const w = known.get(change.tag) ?? {
476
+ hardEpoch: 0,
477
+ softEpoch: 0
478
+ };
479
+ if (change.softEpoch !== void 0) w.softEpoch = Math.max(w.softEpoch, change.softEpoch);
480
+ if (change.hardEpoch !== void 0) w.hardEpoch = Math.max(w.hardEpoch, change.hardEpoch);
481
+ known.set(change.tag, w);
482
+ }
483
+ }
484
+ /** The validity table over an entry's `bornEpoch` and per-tag knowledge. */
485
+ function classify(bornEpoch, knowledge) {
486
+ if (knowledge.some((k) => !k.known)) return "unknown";
487
+ if (knowledge.some((k) => k.known && k.hardEpoch > bornEpoch)) return "dead";
488
+ if (knowledge.some((k) => k.known && k.softEpoch > bornEpoch)) return "stale";
489
+ return "fresh";
490
+ }
491
+ /** The highest `throughEpoch` a live check verified (0 if none is `known`). */
492
+ function throughEpochOf(knowledge) {
493
+ let max = 0;
494
+ for (const k of knowledge) if (k.known && k.throughEpoch > max) max = k.throughEpoch;
495
+ return max;
496
+ }
497
+ /** Default liveness heartbeat interval in ms — the cadence at which a live writer refreshes the
498
+ * pointer's freshness field. The reader mirrors the coordinator's `REGISTRY_HEARTBEAT_MS`; the
499
+ * 2b-2-b shell threads the real value at construction. Suspicion raises after `3 ×` this much
500
+ * OBSERVED SILENCE — an elapsed-time derivative, sampling-rate-independent (not a tick count). */
501
+ const DEFAULT_HEARTBEAT_INTERVAL_MS = 3e4;
502
+ const SUSPICION_SILENCE_MULTIPLE = 3;
503
+ /**
504
+ * The per-isolate invalidation state. Tracks tri-state tag knowledge
505
+ * fed by the bus, the highest contiguously-applied epoch, and a `suspect` flag
506
+ * raised on any gap/reconnect that a resync must clear before warm trust resumes.
507
+ *Tags absent from the map are `unknown` and fail closed (invariant 11).
508
+ */
509
+ var Invalidation = class {
510
+ #registry;
511
+ #stateStore;
512
+ /** The bus, HELD as an address rather than a subscription. Construction wires; it does not
513
+ * dial. The first read that needs the channel subscribes (`#ensureSubscribed`), inside the request
514
+ * context that asked — which is the only context where a dial is permitted to happen at all. */
515
+ #bus;
516
+ /** Telemetry for the reader's own raw facts — currently `bus_dial_failed`. Optional: a cache
517
+ * built with no sink emits nothing, and the reader never depends on the emit for correctness. */
518
+ #emit;
519
+ /** This isolate's mirror-key scope, read from `registry.regId` at construction. It
520
+ * both scopes the keys this reader resyncs off (`mirrorKeys(#regId)`) AND is the value the
521
+ * pointer's echoed `regId` must equal — a mismatch reads a foreign keyspace and fails closed. */
522
+ #regId;
523
+ #subscription = null;
524
+ /** `close()` was called: no further subscribe may happen (a lazy dial must not resurrect a closed
525
+ * reader — the one way deferral could outlive its owner). */
526
+ #closed = false;
527
+ /** The live channel's state — see `ChannelState`. Starts at the honest answer: this reader
528
+ * has never heard a `hello`, and until it does it says so rather than resting on silence. */
529
+ #channel = NEVER_ESTABLISHED;
530
+ /** tag → applied watermark; absence ⇒ `unknown` (tri-state). */
531
+ #known = /* @__PURE__ */ new Map();
532
+ /** Highest contiguous epoch applied from bus frames / resync. Drives gap
533
+ * detection — must stay strictly contiguous. */
534
+ #appliedEpoch = 0;
535
+ /** Highest epoch this isolate has VERIFIED from any authority — the contiguous bus
536
+ * `#appliedEpoch` OR a live check's `throughEpoch`. This is the writer's
537
+ * applied registry epoch for bornEpoch minting: a fill after a verified
538
+ * live check must postdate what that check observed, so a legitimate refill on a
539
+ * behind isolate is not fenced by its own recorded watermark. */
540
+ #knownThroughEpoch = 0;
541
+ /** Raised on a bus gap or reset; forces resync before warm trust. While raised,
542
+ * every tag is treated as unknown at eventual reads — a possibly-gapped map is not the
543
+ * merely-degraded warm map Q4 lets an isolate keep serving on. */
544
+ #suspect = false;
545
+ /** The max head DECLARED over the currently-open live channel during THIS suspect episode
546
+ * — from a LOSS-revealing event only: `hello.head`, `gap.head`, or a NON-CONTIGUOUS
547
+ * frame's `toEpoch`. A contiguous frame never declares (it only reaches toward a named head).
548
+ * `null` until a head is declared this episode; a `reset` discards it (new episode), a `gap`
549
+ * RAISES it (≥ the frame it lost) — so a hidden purge behind a hole is accounted before
550
+ * convergence, never skipped (serve-dead — the shape). Convergence anchors
551
+ * to THIS, never to pointer freshness. */
552
+ #headThisEpisode = null;
553
+ /** The `flushedAt` of the pointer at the last observation that ADVANCED it. `null` until
554
+ * the first observation seeds the baseline. Compared DERIVATIVELY — "did it advance?" via
555
+ * EQUALITY/`>` — never as an absolute-freshness anchor ( : `flushedAt` may not clear
556
+ * suspicion nor date the map). */
557
+ #lastFlushedAt = null;
558
+ /** The `nowMs` of the previous observation — a MONOTONICITY guard so a stale/reordered tick
559
+ * (`nowMs ≤` this) is a true no-op, NOT an age anchor (it is never compared to `flushedAt`).
560
+ * `null` until the first observation. */
561
+ #lastObservationMs = null;
562
+ /** The `nowMs` at which the current unbroken SILENCE began — the last observation that seeded
563
+ * the baseline or saw `flushedAt` advance. `nowMs − this` is the observed silence; reaching
564
+ * `#suspicionAfterMs` RAISES suspicion (derivative, raise-only, never clears). `null` until the
565
+ * first observation. */
566
+ #silenceStartMs = null;
567
+ /** Raise suspicion once OBSERVED SILENCE reaches this — `3 × heartbeatIntervalMs`, fixed at
568
+ * construction. Elapsed silence is sampling-rate-INDEPENDENT: fast ticks cannot witness this much
569
+ * silence in less than this much time, so a false raise is structurally impossible. */
570
+ #suspicionAfterMs;
571
+ constructor(registry, stateStore, bus, heartbeatIntervalMs = DEFAULT_HEARTBEAT_INTERVAL_MS, emit) {
572
+ this.#registry = registry;
573
+ this.#emit = emit;
574
+ this.#suspicionAfterMs = SUSPICION_SILENCE_MULTIPLE * heartbeatIntervalMs;
575
+ this.#stateStore = stateStore;
576
+ this.#regId = registry.regId;
577
+ this.#bus = bus;
578
+ }
579
+ /**
580
+ * Subscribe to the invalidation bus if this reader has not already — THE LAZY DIAL,
581
+ * and the deferral the reachability rule requires of construction. Idempotent and synchronous, so any entry point
582
+ * may call it unconditionally; the dial itself is the transport's business and may complete long
583
+ * after this returns.
584
+ *
585
+ * CALL IT FROM EVERY PATH THAT IS "USE" — the read path (`validate`), the tick path (`resync` /
586
+ * `resyncViaSnapshot`), and the purge verbs, which are registry RPCs and therefore already inside
587
+ * a request context. NEVER from `explain`: a witness that dialed would be testimony rather than
588
+ * introspection, which is why `explainVerdict` is the one reader entry point that does not
589
+ * appear on that list.
590
+ *
591
+ * NOTHING IS MISSED BY SUBSCRIBING LATE. A purge published before this reader was listening is not
592
+ * lost: every establishment handshakes `reset` → `hello{head}`, which suspends warm trust and
593
+ * anchors a resync against the durable `__state`/`__delta` mirror. The live channel is an
594
+ * optimization over that mirror, never the sole path — so the cost of subscribing at first use is
595
+ * one cold resync, and the benefit is an isolate that can be built at module scope at all.
596
+ *
597
+ * The subscriber consumes a BusEvent union: a `reset` (transport (re)establishment) or a `gap`
598
+ * (dropped frame) marks the map suspect and forces a resync before warm trust resumes; a `frame`
599
+ * takes the contiguity path; a `lost` records the channel's own state.
600
+ */
601
+ ensureSubscribed() {
602
+ if (this.#subscription !== null || this.#closed) return;
603
+ this.#subscription = this.#bus.subscribe((event) => {
604
+ this.#applyEvent(event);
605
+ });
606
+ }
607
+ get appliedEpoch() {
608
+ return this.#appliedEpoch;
609
+ }
610
+ /** Whether this isolate currently DISTRUSTS its warm map — read-only. A read
611
+ * arriving while suspect fails closed (the gate); the shell reads it to know whether
612
+ * the ladder still has work. Raised by a bus gap/reset OR the derivative heartbeat miss;
613
+ * cleared ONLY by `#tryConverge` reaching a live-channel head (never by pointer freshness). */
614
+ get suspect() {
615
+ return this.#suspect;
616
+ }
617
+ /** The live channel's state — read-only exposition of state this reader already tracks for
618
+ * its own use, never new bookkeeping added for the witness. */
619
+ get channel() {
620
+ return this.#channel;
621
+ }
622
+ close() {
623
+ this.#closed = true;
624
+ this.#subscription?.close();
625
+ this.#subscription = null;
626
+ }
627
+ /** Advance the contiguous applied epoch (and the verified high-water) to `toEpoch`. */
628
+ #advanceApplied(toEpoch) {
629
+ this.#appliedEpoch = toEpoch;
630
+ this.#knownThroughEpoch = Math.max(this.#knownThroughEpoch, toEpoch);
631
+ }
632
+ /**
633
+ * Dispatch one bus event. Convergence anchors to a head DECLARED this episode,
634
+ *from exactly the LOSS-REVEALING events: a `hello` (the channel head), a `gap`
635
+ * (its `head`, ≥ the frame it lost), or a NON-CONTIGUOUS frame (its `toEpoch`, the hole
636
+ * it proves). A CONTIGUOUS frame does NOT declare — it only reaches toward a head already named.
637
+ * A `reset` discards the anchor (a new episode; the following `hello` re-declares). The kernel,
638
+ * not the bus, owns contiguity validation.
639
+ */
640
+ #applyEvent(event) {
641
+ switch (event.kind) {
642
+ case "frame":
643
+ this.#applyFrame(event.frame);
644
+ return;
645
+ case "hello":
646
+ this.#channel = "established";
647
+ this.#headThisEpisode = Math.max(this.#headThisEpisode ?? 0, event.head);
648
+ this.#tryConverge();
649
+ return;
650
+ case "lost": {
651
+ const healthyClose = this.#channel === "established";
652
+ this.#channel = this.#channel === NEVER_ESTABLISHED ? NEVER_ESTABLISHED : "lost";
653
+ if (!healthyClose) this.#emit?.({ type: TELEMETRY_EVENTS.bus_dial_failed });
654
+ return;
655
+ }
656
+ case "gap":
657
+ this.#suspect = true;
658
+ this.#headThisEpisode = Math.max(this.#headThisEpisode ?? 0, event.head);
659
+ return;
660
+ case "reset":
661
+ this.#suspect = true;
662
+ this.#headThisEpisode = null;
663
+ return;
664
+ default: return event;
665
+ }
666
+ }
667
+ /** Apply a bus frame if contiguous; otherwise raise suspicion. */
668
+ #applyFrame(frame) {
669
+ if (frame.fromEpoch !== this.#appliedEpoch) {
670
+ this.#suspect = true;
671
+ this.#headThisEpisode = Math.max(this.#headThisEpisode ?? 0, frame.toEpoch);
672
+ return;
673
+ }
674
+ applyChanges(this.#known, frame);
675
+ this.#advanceApplied(frame.toEpoch);
676
+ this.#tryConverge();
677
+ }
678
+ /**
679
+ * Clear suspicion iff a head was DECLARED over the live channel this episode (hello/frame)
680
+ * AND the applied epoch has reached it. `#appliedEpoch` advances only by contiguous
681
+ * application, so `applied >= head` already encodes hole-free reach (β) — no separate holes
682
+ * check. Pointer freshness never arbitrates clearing: a heartbeat refreshes the pointer's
683
+ * freshness field without minting epochs, so freshness motion is not evidence the reader has
684
+ * caught up; only a live-channel head declaration anchors convergence. Bus-down /
685
+ * poll-only declares no head ⇒ sticky suspect (posture, priced).
686
+ */
687
+ #tryConverge() {
688
+ if (this.#suspect && this.#headThisEpisode !== null && this.#appliedEpoch >= this.#headThisEpisode) this.#suspect = false;
689
+ }
690
+ /**
691
+ * Cold/resync path — the bounded single DELTA pass. Read the `__state`
692
+ * pointer from L2 and replay the `(applied, lastBatch]` delta batches — durably write-once
693
+ * in L2 even when the bus frame was dropped — applying the contiguous prefix (β), which
694
+ * advances `#appliedEpoch`. A missing pointer or a hole leaves affected knowledge `unknown`
695
+ * (fail closed). Resync does NOT clear suspicion off pointer freshness: a heartbeat refreshes the
696
+ * freshness field without minting epochs, so freshness motion is not evidence of catch-up.
697
+ *It only APPLIES the durable prefix; clearing is `#tryConverge`'s job, and fires iff
698
+ * a head was DECLARED over the live channel this episode (hello/frame) AND this replay carried
699
+ * the applied epoch up to it.
700
+ *
701
+ * SURFACES A STATUS (2b-2-a): the walk's hole is no longer discarded — it is returned so the
702
+ * SHELL (2b-2-b) can schedule the retry ladder off a pure decision. `resync` NEVER sleeps; the
703
+ * escalation across attempts is driven by the caller's `policy.deltasExhausted`, not by time.
704
+ *
705
+ * THE RETRY LADDER NEVER BLOCKS A READ: `validate()`'s awaited resync stays THIS bounded fast
706
+ * pass; a read arriving during `transient-miss` classifies `unknown` and takes the posture
707
+ * NOW — the ladder heals the future, not the present. Fail-closed means what it says. (So
708
+ * `validate` calls `resync()` with the default policy and ignores the status; it does not wait
709
+ * on retries, and a `transient-miss`/`needs-snapshot`/`terminal-gap` read fails closed at once
710
+ * via the gate.)
711
+ */
712
+ async resync(policy) {
713
+ this.ensureSubscribed();
714
+ const deltasExhausted = policy?.deltasExhausted ?? false;
715
+ const mirror = await this.#readMirrorPointer();
716
+ if (!mirror) {
717
+ this.#tryConverge();
718
+ return transientMiss(0);
719
+ }
720
+ const hole = await this.#replayBatches(mirror.store, mirror.keys, 1, mirror.pointer.lastBatch);
721
+ this.#tryConverge();
722
+ if (hole === null) return "converged";
723
+ return deltasExhausted ? "needs-snapshot" : transientMiss(hole);
724
+ }
725
+ /**
726
+ * The SNAPSHOT bridge — the escalation the shell drives after `resync` returns
727
+ * `needs-snapshot`. Load ONLY the pointer-blessed snapshot `__snap:<pointer.snapBatch>` (a
728
+ * from-0 `BusFrame`, `fromEpoch:0`, parsed by the SAME `readFrame`), apply it as an ABSOLUTE
729
+ * base (`#appliedEpoch` ← `snap.toEpoch` when ahead — bridging a hole in the now-GC-eligible
730
+ * region below `snapBatch`), then replay the tail `(snapBatch, lastBatch]`. Never fetches an
731
+ * UNBLESSED snap nor a delta past `lastBatch` (the over-fetch discipline the contract pins).
732
+ *
733
+ * A lagging `snapBatch` is FIRST-CLASS NORMAL (1c): `snapBatch` may trail `lastBatch`
734
+ * arbitrarily — the arithmetic on `(snapBatch, lastBatch]` is correct, no warning, no anomaly.
735
+ *
736
+ * BLESSED-BUT-MISSING SNAP: if `__snap:<snapBatch>` is itself absent, give it ONE brief retry
737
+ * (`transient-miss` — it may be lagging); once the caller's snapshot-budget is spent
738
+ * (`snapshotExhausted`), rule permanently — a FULL delta replay from the floor if the chain
739
+ * `(0, lastBatch]` is intact (⇒ `converged`), else `terminal-gap`. No spin, no serve-through.
740
+ */
741
+ async resyncViaSnapshot(policy) {
742
+ this.ensureSubscribed();
743
+ const snapshotExhausted = policy?.snapshotExhausted ?? false;
744
+ const mirror = await this.#readMirrorPointer();
745
+ if (!mirror) {
746
+ this.#tryConverge();
747
+ return transientMiss(0);
748
+ }
749
+ const { store, keys, pointer } = mirror;
750
+ const snapRaw = await store.get(keys.snapKey(pointer.snapBatch), "snap");
751
+ const snap = snapRaw ? readFrame(snapRaw.value) : void 0;
752
+ if (!snap) {
753
+ if (!snapshotExhausted) {
754
+ this.#tryConverge();
755
+ return transientMiss(pointer.snapBatch);
756
+ }
757
+ const holeNoSnap = await this.#replayBatches(store, keys, 1, pointer.lastBatch);
758
+ this.#tryConverge();
759
+ return holeNoSnap === null ? "converged" : "terminal-gap";
760
+ }
761
+ this.#applySnapshotBase(snap);
762
+ const tailHole = await this.#replayBatches(store, keys, pointer.snapBatch + 1, pointer.lastBatch);
763
+ this.#tryConverge();
764
+ if (tailHole === null) return "converged";
765
+ return transientMiss(tailHole);
766
+ }
767
+ /**
768
+ * Read the `__state` pointer through the registry-scoped keys, returning the scoped
769
+ * `store`/`keys` alongside it, or `undefined` when the pointer is absent/malformed OR echoes a
770
+ * foreign `regId` (a scheme/prefix divergence or another registry's pointer — fail closed).
771
+ */
772
+ async #readMirrorPointer() {
773
+ const store = this.#stateStore;
774
+ const keys = mirrorKeys(this.#regId);
775
+ const raw = await store.get(keys.stateKey, "pointer");
776
+ const pointer = raw ? readPointer(raw.value) : void 0;
777
+ if (!pointer || pointer.regId !== this.#regId) return;
778
+ return {
779
+ keys,
780
+ pointer,
781
+ store
782
+ };
783
+ }
784
+ /**
785
+ * Replay batches `fromSeq..lastBatch` (XRANGE discipline), applying each whose
786
+ * `fromEpoch` is contiguous with the applied epoch. Returns the seq of the FIRST hole
787
+ * (a missing/garbled/non-contiguous forward batch — fail closed), or `null` when the walk
788
+ * reached `lastBatch` hole-free. `fromSeq` is `1` for a from-floor `(0, lastBatch]` replay and
789
+ * `snapBatch + 1` for a snapshot-bridged tail `(snapBatch, lastBatch]` — so GC'd batches below
790
+ * a snapshot floor are never miscounted as holes. Reads ONLY `deltaKey(fromSeq..lastBatch)` —
791
+ * never a seq past the pointer — through the SAME `keys` the pointer was read with (
792
+ * over-fetch discipline).
793
+ */
794
+ async #replayBatches(store, keys, fromSeq, lastBatch) {
795
+ let firstHole = null;
796
+ for (let seq = fromSeq; seq <= lastBatch; seq++) {
797
+ const batchRaw = await store.get(keys.deltaKey(seq), "delta");
798
+ const frame = batchRaw ? readFrame(batchRaw.value) : void 0;
799
+ const applied = frame ? this.#applyIfContiguous(frame) : false;
800
+ if (!(frame && applied) && firstHole === null) firstHole = seq;
801
+ }
802
+ return firstHole;
803
+ }
804
+ /** Apply a batch frame iff it advances the applied epoch contiguously. */
805
+ #applyIfContiguous(frame) {
806
+ if (frame.toEpoch <= this.#appliedEpoch) return true;
807
+ if (frame.fromEpoch !== this.#appliedEpoch) return false;
808
+ applyChanges(this.#known, frame);
809
+ this.#advanceApplied(frame.toEpoch);
810
+ return true;
811
+ }
812
+ /**
813
+ * Apply a from-0 snapshot frame as an ABSOLUTE base: merge its watermarks (max-on-write,
814
+ * never regressing a higher already-known value) and JUMP the applied epoch to `snap.toEpoch`
815
+ * when the snapshot is AHEAD — a complete `(0, snap.toEpoch]` checkpoint carries its own
816
+ * contiguity from the floor, so no `fromEpoch === applied` gate applies. A snapshot BEHIND the
817
+ * applied epoch (a lagging `snapBatch` — first-class normal) adds nothing and never regresses.
818
+ */
819
+ #applySnapshotBase(snap) {
820
+ if (snap.toEpoch <= this.#appliedEpoch) return;
821
+ applyChanges(this.#known, snap);
822
+ this.#advanceApplied(snap.toEpoch);
823
+ }
824
+ /**
825
+ * derivative suspicion RAISING — a PURE state update fed one pointer
826
+ * observation. A heartbeat refreshes the pointer's freshness field without minting epochs, so
827
+ * freshness MOTION is not evidence the reader has caught up; only a live-channel head declaration
828
+ * anchors convergence. This monitor uses freshness the OTHER way — to RAISE: it tracks OBSERVED
829
+ * SILENCE (how long `flushedAt` has failed to advance) and raises `#suspect` once that silence
830
+ * reaches `#suspicionAfterMs`. DERIVATIVE-ONLY, RAISE-ONLY: `flushedAt` never clears suspicion nor
831
+ * anchors absolute freshness — only `>`/equality ACROSS observations, only to RAISE.
832
+ * `nowMs` is compared ONLY to our own prior watch-times (`#lastObservationMs`, `#silenceStartMs`),
833
+ * NEVER to `flushedAt`, so no absolute-age anchor sneaks in. Measuring ELAPSED silence (not
834
+ * counting non-advancing ticks) makes the raise SAMPLING-RATE-INDEPENDENT: fast ticks cannot
835
+ * witness `#suspicionAfterMs` of silence in less time, and a slow-but-live writer resets the
836
+ * window. The shell feeds it observations (2b-2-b); here explicit test ticks.
837
+ */
838
+ notePointerObservation(pointer, nowMs) {
839
+ if (this.#lastObservationMs !== null && nowMs <= this.#lastObservationMs) return;
840
+ this.#lastObservationMs = nowMs;
841
+ if (this.#lastFlushedAt === null || this.#silenceStartMs === null) {
842
+ this.#lastFlushedAt = pointer.flushedAt;
843
+ this.#silenceStartMs = nowMs;
844
+ return;
845
+ }
846
+ if (pointer.flushedAt > this.#lastFlushedAt) {
847
+ this.#lastFlushedAt = pointer.flushedAt;
848
+ this.#silenceStartMs = nowMs;
849
+ return;
850
+ }
851
+ if (nowMs - this.#silenceStartMs >= this.#suspicionAfterMs) this.#suspect = true;
852
+ }
853
+ /**
854
+ * The poll DECISION — a PURE, deterministic predicate: has at least `thresholdMs`
855
+ * elapsed since the last poll? The base cadence is maxSyncLag (60s) and the ±20% jitter is the
856
+ * SHELL's rng job (2b-2-b) — both enter as the caller-computed `thresholdMs`, so no ambient
857
+ * time/rng lives here. The poll DRIVER (the timer that calls this) is 2b-2-b.
858
+ */
859
+ pollDue(nowMs, lastPollMs, thresholdMs) {
860
+ return nowMs - lastPollMs >= thresholdMs;
861
+ }
862
+ /**
863
+ * The `bornEpoch` to stamp into a fresh envelope at fill start, read
864
+ * SYNCHRONOUSLY from LOCAL VERIFIED knowledge — the highest epoch this isolate has
865
+ * observed from any authority (contiguous bus applies OR a recorded live check),
866
+ * never a fresh live read. So the default read path keeps the zero-round-trip cost
867
+ * model, while a fill that follows a verified live check postdates what that check
868
+ * observed — a legitimate refill on a behind isolate is not fenced by its own
869
+ * recorded watermark. A lagging high-water can only UNDER-state bornEpoch ⇒
870
+ * over-invalidation ⇒ the safe direction. The caller snapshots
871
+ * this EARLY as the fence-critical floor (a concurrent bus purge landing after the
872
+ * snapshot cannot lift it, so INV-9/12 hold) and only raises the fill's bornEpoch by
873
+ * the epoch its OWN reads verified (via `validate`'s `checkedThrough`), never by a
874
+ * concurrent frame — see `runGetOrSet`.
875
+ */
876
+ localBornEpoch() {
877
+ return this.#knownThroughEpoch;
878
+ }
879
+ /**
880
+ * The verdict `explain` reports — a PURE read of CURRENT local knowledge: classify
881
+ * the envelope against what this isolate knows NOW, with NO live check, NO resync, and NO
882
+ * recording. It is exposition, not testimony: it adds no tracked state and mutates nothing (the
883
+ * introspection-not-testimony line). A suspect map fails closed to `unknown` (the posture
884
+ * an eventual read would take), and `unknown` stays `unknown`. The soft/hard watermarks are the
885
+ * evidence the bornEpoch was weighed against — the purge epochs behind a `stale`/`dead` verdict
886
+ * (`0` when nothing is known / fail-closed).
887
+ */
888
+ explainVerdict(envelope) {
889
+ const knowledge = this.#localKnowledge(envelope.tags);
890
+ let softEpoch = 0;
891
+ let hardEpoch = 0;
892
+ for (const k of knowledge) if (k.known) {
893
+ softEpoch = Math.max(softEpoch, k.softEpoch);
894
+ hardEpoch = Math.max(hardEpoch, k.hardEpoch);
895
+ }
896
+ const validity = this.#suspect ? "unknown" : classify(envelope.bornEpoch, knowledge);
897
+ return {
898
+ hardEpoch,
899
+ softEpoch,
900
+ validity
901
+ };
902
+ }
903
+ /**
904
+ * Validate an envelope against this isolate's knowledge (table). `strong`
905
+ * (or an unknown tag under `registry-check`) consults a live registry read; a
906
+ * plain `eventual` read uses only local bus-fed knowledge and fails closed on any
907
+ * unknown/suspect tag (invariant 11). Never serves a hard-purged value.
908
+ *
909
+ * REACHABILITY IS SPLIT BY CHANNEL. This method is the REGISTRY-RPC half: a failure here is
910
+ * a strong read's `RegistryUnavailableError` or, on the eventual path, a `'miss'`.
911
+ * The BUS half is not represented here at all — a bus that is down or never came up is never
912
+ * grounds for a throw, because the eventual path's whole design is to be correct without it.
913
+ */
914
+ async validate(envelope, consistency, resolution, onUnavailable) {
915
+ const { tags, bornEpoch } = envelope;
916
+ this.ensureSubscribed();
917
+ if (consistency === "strong") try {
918
+ return await this.#strongLiveCheck(tags, bornEpoch);
919
+ } catch (error) {
920
+ if (onUnavailable !== "eventual") throw new RegistryUnavailableError(`strong read could not reach the registry — ${String(error)}`);
921
+ this.#emit?.({
922
+ reason: STRONG_DEGRADED_REGISTRY_UNREACHABLE,
923
+ type: TELEMETRY_EVENTS.strong_degraded
924
+ });
925
+ }
926
+ if (this.#suspect) await this.resync();
927
+ const localVerdict = this.#suspect ? "unknown" : classify(bornEpoch, this.#localKnowledge(tags));
928
+ if (localVerdict !== "unknown") return {
929
+ checkedThrough: 0,
930
+ validity: localVerdict
931
+ };
932
+ if (typeof resolution !== "string") throw new UnknownTagError(envelope.key, this.#unknownUserTags(tags), resolution.armedBy);
933
+ if (resolution !== "registry-check") return {
934
+ checkedThrough: 0,
935
+ validity: "unknown"
936
+ };
937
+ let checked;
938
+ try {
939
+ checked = await this.#registry.check(tags);
940
+ } catch {
941
+ return {
942
+ checkedThrough: 0,
943
+ validity: "unknown"
944
+ };
945
+ }
946
+ this.#recordChecked(tags, checked);
947
+ return {
948
+ checkedThrough: throughEpochOf(checked),
949
+ validity: classify(bornEpoch, checked)
950
+ };
951
+ }
952
+ /**
953
+ * The strong-read live check: ONE live, un-memoized registry read over
954
+ * `tags`, memoized into local knowledge (`#recordChecked`), classified against
955
+ * `bornEpoch` via the table. The single engine of both the strong READ arm of
956
+ * `validate` and the pre-fetch strong-miss check (`liveCheckForFill`) —
957
+ * local knowledge is never consulted here; the primary read is the authority.
958
+ */
959
+ async #strongLiveCheck(tags, bornEpoch) {
960
+ const knowledge = await this.#registry.check(tags);
961
+ this.#recordChecked(tags, knowledge);
962
+ return {
963
+ checkedThrough: throughEpochOf(knowledge),
964
+ validity: classify(bornEpoch, knowledge)
965
+ };
966
+ }
967
+ /**
968
+ * Pre-fetch strong-miss live check: the SAME live un-memoized
969
+ * registry read `#strongLiveCheck` runs, executed BEFORE the factory on a strong fill so
970
+ * the fill's bornEpoch is minted from now-current knowledge — an understatement, never an
971
+ * overstatement, of recency (the safe direction). Records the reading into `#known` (
972
+ * memoization), so a later eventual read of the tag fails closed with no second round-trip.
973
+ * Returns the verified high-water; the caller raises the fill's bornEpoch to it.
974
+ */
975
+ async liveCheckForFill(tags, onUnavailable) {
976
+ try {
977
+ const { checkedThrough } = await this.#strongLiveCheck(tags, 0);
978
+ return checkedThrough;
979
+ } catch (error) {
980
+ if (onUnavailable !== "eventual") throw new RegistryUnavailableError(`strong fill could not reach the registry to mint its bornEpoch — ${String(error)}`);
981
+ this.#emit?.({
982
+ reason: STRONG_DEGRADED_REGISTRY_UNREACHABLE,
983
+ type: TELEMETRY_EVENTS.strong_degraded
984
+ });
985
+ return 0;
986
+ }
987
+ }
988
+ /** Merge a verified live-check result into local knowledge. Max-on-write, so
989
+ * a concurrently-applied newer bus frame is never regressed. The check's
990
+ * `throughEpoch` advances the verified high-water (bornEpoch source) so a subsequent
991
+ * refill postdates what this check observed. */
992
+ #recordChecked(tags, knowledge) {
993
+ for (const [i, tag] of tags.entries()) {
994
+ const k = knowledge[i];
995
+ if (k === void 0 || !k.known) continue;
996
+ const w = this.#known.get(tag) ?? {
997
+ hardEpoch: 0,
998
+ softEpoch: 0
999
+ };
1000
+ w.softEpoch = Math.max(w.softEpoch, k.softEpoch);
1001
+ w.hardEpoch = Math.max(w.hardEpoch, k.hardEpoch);
1002
+ this.#known.set(tag, w);
1003
+ this.#knownThroughEpoch = Math.max(this.#knownThroughEpoch, k.throughEpoch);
1004
+ }
1005
+ }
1006
+ /**
1007
+ * The write-back fence, evaluated against LOCAL bus-applied knowledge: any tag
1008
+ * `hardEpoch > bornEpoch` in the isolate's contiguously-applied state ⇒ the candidate
1009
+ * is fenced (a hard purge landed DURING the fill and was delivered here). Returns
1010
+ * `true` when the write-back may proceed. A soft purge lands already-stale but still
1011
+ * stores; only DEAD fences.
1012
+ *
1013
+ * DESIGN (per the INV-12 erratum): the write-back guard is realized as a LOCAL
1014
+ * fence here plus read-time fail-closed resolution — not a live registry read on the
1015
+ * write path. The local fence catches a purge that landed AND delivered locally DURING the
1016
+ * fill (tag KNOWN-dead locally ⇒ INV-9/12); it lets a cold/dropped-frame isolate STORE a value
1017
+ * whose tag is locally unknown (origin is truth after a purge — the legitimate
1018
+ * refill case). A born-dead value that slips past this fence is stored but INERT: its own
1019
+ * SUBSEQUENT eventual reads fail closed on the unknown tag and live-check — that read-time
1020
+ * check is where the GOAL (never serve a value born against a durably-accepted purge) is
1021
+ * enforced for THAT path. A strong fill's OWN serve is anchored earlier, at INITIATION: the
1022
+ * pre-factory check minted its `bornEpoch` at the global epoch, so it already dominates
1023
+ * every purge accepted before initiation — no read-time check re-verifies the fill
1024
+ * that produced it.
1025
+ */
1026
+ fenceWriteBack(tags, bornEpoch) {
1027
+ return classify(bornEpoch, this.#localKnowledge(tags)) !== "dead";
1028
+ }
1029
+ /** Local tri-state knowledge for these tags (absent ⇒ unknown). */
1030
+ /**
1031
+ * The USER tags this read could not establish — what `UnknownTagError` carries.
1032
+ *
1033
+ * Under suspicion EVERY tag is unknown (the gate treats a map with a hole as knowing
1034
+ * nothing), so the whole user set is the honest answer there. Otherwise it is exactly the tags the
1035
+ * map has no watermark for. Reserved tags are dropped either way: `__k:`/`__ns:` are the kernel's
1036
+ * own bookkeeping, they are unknown for the same reason the user's tags are, and a caller cannot
1037
+ * do anything with them — the same reason they never reach an L3 header.
1038
+ */
1039
+ #unknownUserTags(tags) {
1040
+ const knowledge = this.#localKnowledge(tags);
1041
+ return tags.filter((tag, index) => !isReservedTag(tag) && (this.#suspect || knowledge[index]?.known !== true));
1042
+ }
1043
+ #localKnowledge(tags) {
1044
+ return tags.map((tag) => {
1045
+ const w = this.#known.get(tag);
1046
+ if (!w) return { known: false };
1047
+ return {
1048
+ hardEpoch: w.hardEpoch,
1049
+ known: true,
1050
+ softEpoch: w.softEpoch,
1051
+ throughEpoch: this.#appliedEpoch
1052
+ };
1053
+ });
1054
+ }
1055
+ };
1056
+ //#endregion
1057
+ //#region src/drivers.ts
1058
+ /**
1059
+ * Structural type-guard for `StoreWriteError`. Purely shape-based — it NEVER
1060
+ * uses `instanceof`, so a harness fake and a real driver are recognized by the SAME
1061
+ * check without a shared class. Total and safe on arbitrary `unknown`: rejects
1062
+ * non-objects/null, requires `code` to be exactly one of the three literals and
1063
+ * `retryable` to be a boolean.
1064
+ */
1065
+ function isStoreWriteError(e) {
1066
+ if (typeof e !== "object" || e === null) return false;
1067
+ const code = "code" in e ? e.code : void 0;
1068
+ const retryable = "retryable" in e ? e.retryable : void 0;
1069
+ return (code === "throttled" || code === "too_large" || code === "unavailable") && typeof retryable === "boolean";
1070
+ }
1071
+ /**
1072
+ * Structural type-guard for `StoreReadError` — shape-based, never `instanceof`,
1073
+ * exactly as `isStoreWriteError` is. Total and safe on arbitrary `unknown`. A driver error
1074
+ * that carries a read-suppressible `code` satisfies this whatever class raised it, which is
1075
+ * the point: the own `unavailable` shape is recognized without a shared identity.
1076
+ */
1077
+ function isStoreReadError(e) {
1078
+ if (typeof e !== "object" || e === null) return false;
1079
+ const code = "code" in e ? e.code : void 0;
1080
+ const retryable = "retryable" in e ? e.retryable : void 0;
1081
+ return (code === "throttled" || code === "unavailable") && typeof retryable === "boolean";
1082
+ }
1083
+ /**
1084
+ * Rejection raised by `memory().set` when a single value's UTF-8 byte length exceeds the
1085
+ * store's total `maxBytes` budget — a value larger than the whole budget can never fit, so
1086
+ * it is REJECTED, never truncated. Satisfies the structural `StoreWriteError`
1087
+ * contract (`too_large`, non-retryable) WITHOUT sharing a class with the kernel: consumers
1088
+ * discriminate via `isStoreWriteError` (structural), never `instanceof`. The kernel-side
1089
+ * twin of the harness `OversizeValueError`; internal, so it needs no export.
1090
+ */
1091
+ var MemoryOversizeError = class extends Error {
1092
+ code = "too_large";
1093
+ retryable = false;
1094
+ constructor(key, sizeBytes, maxBytes) {
1095
+ super(`value too large: ${key} (${sizeBytes} > ${maxBytes} bytes)`);
1096
+ this.name = "MemoryOversizeError";
1097
+ }
1098
+ };
1099
+ /**
1100
+ * Rejection raised by `memory().set` when a write carries `expirationTtl` but the store was built
1101
+ * with NO injected clock — the residency cannot be honored without a `now` source, so the write
1102
+ * FAILS LOUD rather than silently drop the TTL. Unlike `MemoryOversizeError` this is a
1103
+ * MISCONFIGURATION, not a `StoreWriteError`: it must NOT satisfy the structural write-failure
1104
+ * contract the kernel absorbs — a residency the store can never honor is a programmer error,
1105
+ * surfaced typed so a caller can `instanceof`-catch it, never quietly ignored. Internal, mirroring
1106
+ * `MemoryOversizeError` (neither is re-exported from `index.ts`).
1107
+ */
1108
+ var MemoryConfigError = class extends Error {
1109
+ constructor(message) {
1110
+ super(message);
1111
+ this.name = "MemoryConfigError";
1112
+ }
1113
+ };
1114
+ /** is this a PRIVATE (`usr:`-scoped) L1 entry? Read off the `scope`
1115
+ * metadata LITERAL the kernel writes with every L1 set (`pub` / `usr:<h>` / `ten:<h>`) — the
1116
+ * `usr:` PREFIX only, never the hash. A `usr:` entry is per-principal, so its eviction is disclosed;
1117
+ * detection touches nothing identifying, and a non-canonical store (no `scope` metadata) never
1118
+ * matches. */
1119
+ function isPrivateEntry(entry) {
1120
+ const scope = entry.metadata?.scope;
1121
+ return typeof scope === "string" && scope.startsWith("usr:");
1122
+ }
1123
+ /**
1124
+ * In-process L1 store — isolate-local, bounded by `maxEntries` / `maxBytes` with LRU
1125
+ * eviction on either bound (an insertion-ordered `Map`, re-inserted on touch, so recency —
1126
+ * not mere insertion order — picks the victim). Each `memory()` owns a PER-INSTANCE byte
1127
+ * accounter (Σ value UTF-8 byte lengths).
1128
+ *
1129
+ * an optional `telemetry` sink receives a `private_evicted` event when a
1130
+ * PRIVATE (`usr:`-scoped) entry is evicted from the LRU under memory pressure — reason class +
1131
+ * counts + bytes ONLY, so an operator sees L1 pressure on private data without the event ever
1132
+ * naming WHAT was evicted (payload discipline). The `usr:`-priority-protected LRU (evict
1133
+ * private entries LAST) is a spec MAY and stays UNBUILT: this discloses whatever the current LRU
1134
+ * policy evicts, it does not change the victim. The cross-instance shared/joint reserve (warn
1135
+ * on Σ`maxBytes` overflow across isolate L1s) is a separate advisory extra, still DEFERRED.
1136
+ */
1137
+ function memory(options) {
1138
+ const clock = options?.clock;
1139
+ const maxEntries = options?.maxEntries;
1140
+ const maxBytes = options?.maxBytes;
1141
+ const telemetry = options?.telemetry;
1142
+ const onSinkError = options?.onSinkError;
1143
+ const encoder = new TextEncoder();
1144
+ const entries = /* @__PURE__ */ new Map();
1145
+ let totalBytes = 0;
1146
+ const drop = (key, entry) => {
1147
+ entries.delete(key);
1148
+ totalBytes -= entry.bytes;
1149
+ };
1150
+ const overBounds = () => maxEntries !== void 0 && entries.size > maxEntries || maxBytes !== void 0 && totalBytes > maxBytes;
1151
+ return {
1152
+ delete: async (key) => {
1153
+ const entry = entries.get(key);
1154
+ if (entry !== void 0) drop(key, entry);
1155
+ },
1156
+ get: async (key) => {
1157
+ const entry = entries.get(key);
1158
+ const expired = entry !== void 0 && clock !== void 0 && entry.expireAtMs !== void 0 && clock.now() >= entry.expireAtMs;
1159
+ if (entry === void 0 || expired) {
1160
+ if (entry !== void 0) drop(key, entry);
1161
+ return;
1162
+ }
1163
+ entries.delete(key);
1164
+ entries.set(key, entry);
1165
+ return entry.metadata === void 0 ? { value: entry.value } : {
1166
+ metadata: entry.metadata,
1167
+ value: entry.value
1168
+ };
1169
+ },
1170
+ set: async (key, value, writeOptions) => {
1171
+ const bytes = encoder.encode(value).length;
1172
+ if (maxBytes !== void 0 && bytes > maxBytes) throw new MemoryOversizeError(key, bytes, maxBytes);
1173
+ let expireAtMs;
1174
+ if (writeOptions?.expirationTtl !== void 0) {
1175
+ if (clock === void 0) throw new MemoryConfigError("memory(): expirationTtl requires an injected clock — construct memory({ clock, … }) to honor residency (§8)");
1176
+ expireAtMs = clock.now() + writeOptions.expirationTtl * 1e3;
1177
+ }
1178
+ const existing = entries.get(key);
1179
+ if (existing !== void 0) drop(key, existing);
1180
+ entries.set(key, {
1181
+ bytes,
1182
+ value,
1183
+ ...writeOptions?.metadata === void 0 ? {} : { metadata: writeOptions.metadata },
1184
+ ...expireAtMs === void 0 ? {} : { expireAtMs }
1185
+ });
1186
+ totalBytes += bytes;
1187
+ while (overBounds()) {
1188
+ const lruKey = entries.keys().next().value;
1189
+ if (lruKey === void 0) break;
1190
+ const lru = entries.get(lruKey);
1191
+ if (lru !== void 0) {
1192
+ drop(lruKey, lru);
1193
+ if (telemetry !== void 0 && isPrivateEntry(lru)) safeEmit(telemetry, {
1194
+ bytes: lru.bytes,
1195
+ count: 1,
1196
+ type: TELEMETRY_EVENTS.private_evicted
1197
+ }, onSinkError);
1198
+ }
1199
+ }
1200
+ }
1201
+ };
1202
+ }
1203
+ //#endregion
1204
+ //#region src/duration.ts
1205
+ /**
1206
+ * Milliseconds per unit — the ONE `Duration`→ms grammar source. Shared with
1207
+ * `cache.ts`'s coarse `durationMs` parser (which imports this const) so the overflow
1208
+ * check here and the retention/heartbeat parse there agree on the factor table byte-for-byte.
1209
+ * `m` = minutes.
1210
+ */
1211
+ const DURATION_UNIT_MS = {
1212
+ d: 864e5,
1213
+ h: 36e5,
1214
+ m: 6e4,
1215
+ ms: 1,
1216
+ s: 1e3
1217
+ };
1218
+ /**
1219
+ * Build a `Duration` from computed values — the trust point for
1220
+ * programmatically-derived durations (as opposed to a `'5m'` string literal the type
1221
+ * system already constrains). Rejects a non-positive or non-finite `value`, and — and,
1222
+ * the case a naive impl passes — a `value × unitFactor` product that is not a finite,
1223
+ * positive, SAFE integer once multiplied out. So `duration(Number.MAX_SAFE_INTEGER, 'd')`
1224
+ * is REJECTED even though the input is a safe integer: the overflow lives in the product.
1225
+ * Throws `InvalidDurationError` on any violation; never coerces.
1226
+ */
1227
+ function duration(value, unit) {
1228
+ if (!Number.isFinite(value) || value <= 0) throw new InvalidDurationError(value, unit, "value must be a finite number greater than 0 (§2.1)");
1229
+ const ms = value * DURATION_UNIT_MS[unit];
1230
+ if (!Number.isSafeInteger(ms) || ms <= 0) throw new InvalidDurationError(value, unit, "computed milliseconds must be a finite, positive, safe integer (F31)");
1231
+ return `${value}${unit}`;
1232
+ }
1233
+ //#endregion
1234
+ //#region src/http.ts
1235
+ /** The default retriable HTTP status set (Cloudflare 52x joins it). */
1236
+ const RETRIABLE_STATUSES = /* @__PURE__ */ new Set([
1237
+ 408,
1238
+ 425,
1239
+ 429,
1240
+ 500,
1241
+ 502,
1242
+ 503,
1243
+ 504,
1244
+ 520,
1245
+ 521,
1246
+ 522,
1247
+ 523,
1248
+ 524,
1249
+ 525,
1250
+ 526,
1251
+ 527,
1252
+ 530
1253
+ ]);
1254
+ /** Wrap a non-ok `Response` into a typed `HttpError`. */
1255
+ function httpError(response) {
1256
+ return new HttpError(response);
1257
+ }
1258
+ /**
1259
+ * The default `staleIfError` predicate — retriable: the default
1260
+ * retriable set, network errors, and cache-originated timeouts. `404/410/403` are
1261
+ * facts, never masked (never retriable) — a caller-abort `CacheTimeoutError` with
1262
+ * `source:'caller'` is likewise not evidence of origin health.
1263
+ */
1264
+ function isRetriableHttp(err) {
1265
+ if (err instanceof HttpError) return RETRIABLE_STATUSES.has(err.status);
1266
+ if (err instanceof CacheTimeoutError) return err.source === "cache";
1267
+ return err instanceof TypeError;
1268
+ }
1269
+ //#endregion
1270
+ //#region src/replication-poller.ts
1271
+ const DEFAULT_POLL_BASE_MS = 6e4;
1272
+ const DEFAULT_FIRST_LADDER_DELAY_MS = 1e3;
1273
+ const DEFAULT_LATER_LADDER_DELAY_MS = 5e3;
1274
+ const DEFAULT_JITTER_FRACTION = .2;
1275
+ /** Poll-failure backoff ceiling (knob): a warm baseline poll that does NOT converge doubles
1276
+ * the cadence (×2) up to this cap — 300s — and the doubled interval is jittered like any other, so a
1277
+ * fleet whose L2 is failing degrades to one attempt per ~5 minutes per isolate instead of hammering
1278
+ * a sick store every 60s. The cap is on the BASE interval; the ±jitter rides on top (a jittered
1279
+ * 300s is 240–360s), because de-synchronizing the fleet matters most exactly when the store is sick. */
1280
+ const MAX_POLL_BACKOFF_MS = 3e5;
1281
+ /** Backoff multiplier per consecutive failed baseline poll (×2). */
1282
+ const POLL_BACKOFF_FACTOR = 2;
1283
+ /** The rung at which the delta budget is spent and the pass may escalate to the snapshot bridge. */
1284
+ const ESCALATE_AT_ATTEMPT = 3;
1285
+ /** The rung at which the snapshot budget is ALSO spent — the reader makes the permanent ruling
1286
+ * (full replay if the chain is intact ⇒ converged, else terminal-gap). Below it, a lagging snap
1287
+ * gets one brief retry. */
1288
+ const SNAPSHOT_FINAL_ATTEMPT = 4;
1289
+ /** Parse the `__state` pointer bytes the reader resyncs off into the wire shape the
1290
+ * monitor consumes. Malformed / absent ⇒ `undefined` (the observation is simply skipped — a shell
1291
+ * read that cannot see the pointer never fabricates liveness). Numbers-only fields; missing
1292
+ * optionals default the same way the kernel reader does. */
1293
+ function readStatePointer(raw) {
1294
+ let parsed;
1295
+ try {
1296
+ parsed = JSON.parse(raw);
1297
+ } catch {
1298
+ return;
1299
+ }
1300
+ if (typeof parsed !== "object" || parsed === null) return;
1301
+ const o = parsed;
1302
+ if (typeof o.epoch !== "number" || typeof o.lastBatch !== "number" || typeof o.regId !== "string") return;
1303
+ return {
1304
+ epoch: o.epoch,
1305
+ flushedAt: typeof o.flushedAt === "number" ? o.flushedAt : 0,
1306
+ lastBatch: o.lastBatch,
1307
+ regId: o.regId,
1308
+ snapBatch: typeof o.snapBatch === "number" ? o.snapBatch : 0
1309
+ };
1310
+ }
1311
+ /**
1312
+ * The tick-driven replication poll driver. Each `onPollTick(nowMs)`:
1313
+ * 1. reads the `__state` pointer and feeds the derivative liveness monitor
1314
+ * (`notePointerObservation`) — raise-only, every tick, sampling-rate-independent;
1315
+ * 2. while the reader is SUSPECT, runs the retry ladder when an attempt is DUE:
1316
+ * attempt 1 → `resync()` (delta); +~1s → attempt 2 → `resync()` (delta); +~5s →
1317
+ * attempt 3 → `resync({deltasExhausted})` and, on `needs-snapshot`, `resyncViaSnapshot()`
1318
+ * (snapshot-permitted). `converged` clears the reader (and backs the ladder off to the
1319
+ * baseline cadence when suspicion persists awaiting a live-channel head — never a spin);
1320
+ * `terminal-gap` STOPS the ladder permanently (fail-closed, no spin);
1321
+ * 3. while WARM, runs the baseline liveness poll on the maxSyncLag cadence (60s ±20%), gated by
1322
+ * the kernel's pure `pollDue`, and BACKS OFF on failure: a non-converged poll doubles the
1323
+ * interval (×2, cap 300s, still jittered); a converged one resets it to the base.
1324
+ *
1325
+ * Pure + tick-driven: no wall clock, no ambient timers, no sleeps — `nowMs` and the injected rng
1326
+ * are the only sources of time/randomness.
1327
+ *
1328
+ * RE-ENTRANCY-SAFE BY CONTRACT. `onPollTick` is NOT required to be awaited before the
1329
+ * next one is fired, and under the request-piggyback driver it routinely is not — a burst of
1330
+ * requests hands one isolate several ticks while a step's store I/O is still in flight. Two
1331
+ * properties make that safe HERE, at the root, rather than in each driver: (i) every due-decision
1332
+ * and the state writes it authorizes happen in ONE synchronous block — no `await` separates a
1333
+ * due-check from its commit, so no tick ever acts on a schedule another tick has already consumed;
1334
+ * (ii) at most ONE ladder step is in flight, and a tick arriving mid-step declines rather than
1335
+ * queues. The `__state` observation is exempt from both — the +25 vigil runs on EVERY tick.
1336
+ */
1337
+ var ReplicationPoller = class {
1338
+ #reader;
1339
+ #store;
1340
+ #keys;
1341
+ #rng;
1342
+ #pollBaseMs;
1343
+ #firstLadderDelayMs;
1344
+ #laterLadderDelayMs;
1345
+ #jitterFraction;
1346
+ /** Active hole-chasing ladder, or `null` when warm (no hole to chase). */
1347
+ #ladder = null;
1348
+ /** A terminal-gap verdict indexed to the pointer that produced it: the LADDER stays stopped
1349
+ * (fail-closed, no spin) while this chain cannot complete. Permanence attaches to the CHAIN, not
1350
+ * the vigil — the baseline observation never stops (below). Cleared on a fresh suspect episode OR
1351
+ * when a durable `snapBatch` advance past `#terminalSnapBatch` bridges the hole. */
1352
+ #terminal = false;
1353
+ /** The pointer `snapBatch` the terminal verdict was made against — expired deltas never return, so
1354
+ * ONLY an observed `snapBatch` beyond this can re-arm one snapshot-permitted attempt. `null` when
1355
+ * not terminal. */
1356
+ #terminalSnapBatch = null;
1357
+ /** Suspect state at the previous tick — the false→true EDGE opens a fresh episode. */
1358
+ #prevSuspect = false;
1359
+ /** SINGLE-FLIGHT: a ladder step is between its due-decision and its awaits. Set in the
1360
+ * SAME synchronous block as the due-decision that authorized it and cleared in that block's
1361
+ * `finally`, so at most one step ever runs. Ticks are request-piggybacked (an isolate gets no
1362
+ * free-running timer), which makes RE-ENTRY ROUTINE rather than exotic — a burst of requests can
1363
+ * hand this poller several ticks while one step's store I/O is still in flight. A tick that finds
1364
+ * a step running DECLINES; it never queues, because queueing merely defers the burst it exists to
1365
+ * prevent. Nothing is lost: the rung it would have run is already scheduled (below). */
1366
+ #stepInFlight = false;
1367
+ /** Last baseline poll time (`null` until the first warm tick seeds the cursor). */
1368
+ #lastPollMs = null;
1369
+ /** Jittered baseline threshold, recomputed each poll (the current interval ±20%). */
1370
+ #pollThresholdMs;
1371
+ /** The CURRENT baseline interval the threshold is drawn from: `#pollBaseMs` while polls converge,
1372
+ * doubled per consecutive failure up to `MAX_POLL_BACKOFF_MS`. Reset to the base by the
1373
+ * first poll that converges again — the backoff is a property of the current failure episode, never
1374
+ * a permanent cadence change. */
1375
+ #pollIntervalMs;
1376
+ constructor(reader, store, keys, rng, options = {}) {
1377
+ this.#reader = reader;
1378
+ this.#store = store;
1379
+ this.#keys = keys;
1380
+ this.#rng = rng;
1381
+ this.#pollBaseMs = options.pollBaseMs ?? DEFAULT_POLL_BASE_MS;
1382
+ this.#firstLadderDelayMs = options.firstLadderDelayMs ?? DEFAULT_FIRST_LADDER_DELAY_MS;
1383
+ this.#laterLadderDelayMs = options.laterLadderDelayMs ?? DEFAULT_LATER_LADDER_DELAY_MS;
1384
+ this.#jitterFraction = options.jitterFraction ?? DEFAULT_JITTER_FRACTION;
1385
+ this.#pollIntervalMs = this.#pollBaseMs;
1386
+ this.#pollThresholdMs = this.#jitter(this.#pollIntervalMs);
1387
+ }
1388
+ /**
1389
+ * Whether the reader's replication has reached a permanent terminal gap — the
1390
+ * hole-chasing ladder is stopped, fail-closed, until a durable `snapBatch` advance re-arms it.
1391
+ * Read-only exposition of the already-tracked `#terminal` verdict for `explain`; it
1392
+ * adds no bookkeeping — the field is written only by the tick machinery, never by this getter.
1393
+ */
1394
+ get terminal() {
1395
+ return this.#terminal;
1396
+ }
1397
+ /**
1398
+ * Advance the state machine one tick. Always observes the pointer for liveness; then
1399
+ * drives the ladder (suspect) or the baseline poll (warm). Out-of-band — never on a read path.
1400
+ */
1401
+ async onPollTick(nowMs) {
1402
+ const raw = await this.#store.get(this.#keys.stateKey);
1403
+ const pointer = raw === void 0 ? void 0 : readStatePointer(raw.value);
1404
+ if (pointer !== void 0) this.#reader.notePointerObservation(pointer, nowMs);
1405
+ if (this.#stepInFlight) return;
1406
+ const { suspect } = this.#reader;
1407
+ if (suspect && !this.#prevSuspect) {
1408
+ this.#terminal = false;
1409
+ this.#terminalSnapBatch = null;
1410
+ this.#ladder = null;
1411
+ }
1412
+ this.#prevSuspect = suspect;
1413
+ if (this.#terminal) {
1414
+ if (pointer !== void 0 && this.#terminalSnapBatch !== null && pointer.snapBatch > this.#terminalSnapBatch) {
1415
+ this.#terminal = false;
1416
+ this.#terminalSnapBatch = null;
1417
+ if (await this.#reader.resyncViaSnapshot({ snapshotExhausted: true }) === "terminal-gap") {
1418
+ this.#terminal = true;
1419
+ this.#terminalSnapBatch = pointer.snapBatch;
1420
+ }
1421
+ }
1422
+ return;
1423
+ }
1424
+ if (suspect) {
1425
+ const ladder = this.#ladder ?? {
1426
+ attempt: 0,
1427
+ nextDueMs: nowMs
1428
+ };
1429
+ this.#ladder = ladder;
1430
+ if (nowMs < ladder.nextDueMs) return;
1431
+ ladder.attempt += 1;
1432
+ ladder.nextDueMs = nowMs + this.#jitter(this.#ladderDelay(ladder.attempt));
1433
+ this.#stepInFlight = true;
1434
+ try {
1435
+ await this.#runLadderStep(nowMs, ladder, pointer?.snapBatch ?? 0);
1436
+ } finally {
1437
+ this.#stepInFlight = false;
1438
+ }
1439
+ return;
1440
+ }
1441
+ this.#ladder = null;
1442
+ if (this.#lastPollMs === null) {
1443
+ this.#lastPollMs = nowMs;
1444
+ this.#pollThresholdMs = this.#jitter(this.#pollIntervalMs);
1445
+ return;
1446
+ }
1447
+ if (this.#reader.pollDue(nowMs, this.#lastPollMs, this.#pollThresholdMs)) {
1448
+ this.#lastPollMs = nowMs;
1449
+ const status = await this.#reader.resync();
1450
+ this.#pollIntervalMs = status === "converged" ? this.#pollBaseMs : this.#backedOff(this.#pollIntervalMs);
1451
+ this.#pollThresholdMs = this.#jitter(this.#pollIntervalMs);
1452
+ }
1453
+ }
1454
+ /** One backoff step: ×2, capped at 300s (never below the configured base — a base ABOVE the cap is
1455
+ * already slower than any backoff could make it, so the cap floors to it rather than SPEEDING the
1456
+ * poll up on failure). */
1457
+ #backedOff(intervalMs) {
1458
+ const cap = Math.max(MAX_POLL_BACKOFF_MS, this.#pollBaseMs);
1459
+ return Math.min(intervalMs * POLL_BACKOFF_FACTOR, cap);
1460
+ }
1461
+ /**
1462
+ * Run one ladder rung. Rungs 1–2 retry the delta pass; rung 3 escalates
1463
+ * (`deltasExhausted`) and bridges via the snapshot on `needs-snapshot`; rung ≥4 spends the
1464
+ * snapshot budget so the reader makes the permanent ruling. Never sleeps.
1465
+ *
1466
+ * the caller has ALREADY claimed the rung (`ladder.attempt`) and committed its provisional
1467
+ * next-due, synchronously, before this is entered — so this method only ever OVERWRITES that
1468
+ * schedule when the outcome calls for a different one. It re-schedules NOTHING on the retry path
1469
+ * (the provisional IS the retry), and it writes nothing that a re-entrant tick could race, because
1470
+ * the in-flight flag the caller set means no other tick is deciding anything while this runs.
1471
+ */
1472
+ async #runLadderStep(nowMs, ladder, observedSnapBatch) {
1473
+ const { attempt } = ladder;
1474
+ let status = await this.#reader.resync({ deltasExhausted: attempt >= ESCALATE_AT_ATTEMPT });
1475
+ if (status === "needs-snapshot") status = await this.#reader.resyncViaSnapshot({ snapshotExhausted: attempt >= SNAPSHOT_FINAL_ATTEMPT });
1476
+ if (status === "converged") {
1477
+ ladder.attempt = 0;
1478
+ ladder.nextDueMs = nowMs + this.#jitter(this.#pollBaseMs);
1479
+ return;
1480
+ }
1481
+ if (status === "terminal-gap") {
1482
+ this.#terminal = true;
1483
+ this.#terminalSnapBatch = observedSnapBatch;
1484
+ this.#ladder = null;
1485
+ }
1486
+ }
1487
+ /** The base ladder delay for the rung just run: attempt 1 → ~1s, attempts ≥2 → ~5s. */
1488
+ #ladderDelay(attempt) {
1489
+ return attempt <= 1 ? this.#firstLadderDelayMs : this.#laterLadderDelayMs;
1490
+ }
1491
+ /** Jitter a base delay symmetrically by ±`jitterFraction`, drawn from the injected rng (no
1492
+ * ambient randomness). rng.next() ∈ [0,1) ⇒ factor ∈ [1−f, 1+f). */
1493
+ #jitter(baseMs) {
1494
+ const f = this.#jitterFraction;
1495
+ return baseMs * (1 - f + 2 * f * this.#rng.next());
1496
+ }
1497
+ };
1498
+ /** The library's default safe-mode codec. Deterministic JSON round-trip. */
1499
+ const safeCodec = {
1500
+ decode(value) {
1501
+ return JSON.parse(value);
1502
+ },
1503
+ encode(value) {
1504
+ return JSON.stringify(value);
1505
+ },
1506
+ id: "safe/1"
1507
+ };
1508
+ //#endregion
1509
+ //#region src/cache.ts
1510
+ /** The `<scope>` literal of the one shared, principal-free storage class. It is
1511
+ * the ONLY value that keeps a response shared-cacheable under. */
1512
+ const PUBLIC_SCOPE = "pub";
1513
+ /** The scope evidence a served HIT carries: its stored literal, or the fail-closed
1514
+ * `unreadable` class when the shared surface returned none. */
1515
+ function hitScopeEvidence(hit) {
1516
+ if (hit.scopeUnreadable || hit.rawScope === void 0) return { kind: "unreadable" };
1517
+ return {
1518
+ kind: "readable",
1519
+ literal: hit.rawScope
1520
+ };
1521
+ }
1522
+ /** The scope evidence a FILL carries: the literal this kernel just resolved for it.
1523
+ *Never `unreadable` — a computed scope cannot fail to be read. */
1524
+ function fillScopeEvidence(literal) {
1525
+ return {
1526
+ kind: "readable",
1527
+ literal
1528
+ };
1529
+ }
1530
+ /** Record one consumed dependency on the ambient collector, if there is one. */
1531
+ function recordDependency(ambient, dependency) {
1532
+ ambient?.()?.record(dependency);
1533
+ }
1534
+ /** Record a SERVED stored entry as a consumed dependency: the tags it was stored
1535
+ * with, plus the scope evidence read off the surface it came from — `unreadable` when that
1536
+ * surface returned no scope at all, which poisons the response rather than guessing. A
1537
+ * `undefined` hit is a serve that never happened, and records nothing. */
1538
+ function recordServedHit(ambient, hit) {
1539
+ if (hit === void 0) return;
1540
+ recordDependency(ambient, {
1541
+ scope: hitScopeEvidence(hit),
1542
+ tags: hit.envelope.tags
1543
+ });
1544
+ }
1545
+ /** The verdict for a registry-absent instance: with no watermark authority every
1546
+ * stored entry reads FRESH, exactly as the codec-only read path classifies it — there is nothing
1547
+ * that could purge it, so nothing is `stale`/`dead`/`unknown`. */
1548
+ const FRESH_VERDICT = {
1549
+ hardEpoch: 0,
1550
+ softEpoch: 0,
1551
+ validity: "fresh"
1552
+ };
1553
+ /**
1554
+ * The request-scoped per-dependency attribution `explain` surfaces — the bound
1555
+ * render collector's recorded dependencies, projected to the public shape. `undefined` when no
1556
+ * collector is bound (outside an adapter request frame): the attribution lives and dies with the
1557
+ * request, so its ABSENCE is itself the honest report (a witness reports what it knows; unbound ⇒
1558
+ * there is nothing to attribute).
1559
+ */
1560
+ function explainAttribution(ambient) {
1561
+ const state = ambient?.();
1562
+ if (state === void 0) return;
1563
+ return state.records.map((dependency) => ({
1564
+ scope: dependency.scope,
1565
+ tags: dependency.tags,
1566
+ ...dependency.l3 === void 0 ? {} : { l3: dependency.l3 }
1567
+ }));
1568
+ }
1569
+ /** Per-instance namespace-version counter. `clear()` bumps it so
1570
+ * every canonical key minted AFTER the bump is unreachable from a pre-clear key —
1571
+ * a second, independent defense alongside the `__ns` hard watermark (INV-23). Held
1572
+ * as a small mutable cell (not a plain closure variable) so it can be threaded
1573
+ * through the free functions below exactly like `Invalidation`. */
1574
+ var NsVersion = class {
1575
+ #version = 0;
1576
+ get current() {
1577
+ return this.#version;
1578
+ }
1579
+ bump() {
1580
+ this.#version += 1;
1581
+ }
1582
+ };
1583
+ /** Canonical key form `<ns-version>:<ns>:<scope>:<user-key>`. */
1584
+ function canonicalKey(nsVersion, namespace, scope, userKey) {
1585
+ return `${nsVersion}:${namespace}:${scope}:${userKey}`;
1586
+ }
1587
+ /**
1588
+ * The adapter-visible identity fields a default-scope request can carry, in
1589
+ * precedence order ( principal-derived scoping; adapter-visible
1590
+ * identity / request-identity-header values). `userId` is the per-user
1591
+ * principal the RR/Workers adapter surfaces; a request that carries only a
1592
+ * `tenant` still bears adapter-visible identity, so it too derives a principal.
1593
+ * A request with none of these carries no derivable principal.
1594
+ */
1595
+ const PRINCIPAL_FIELDS = ["userId", "tenant"];
1596
+ /**
1597
+ * Derive the adapter-visible principal from a request. Returns
1598
+ * the first present identity field's value keyed by its field name, or `undefined`
1599
+ * when the request carries no recognized identity (no context / null principal ⇒
1600
+ * shared `pub` class). Adapter-visible identity is a primitive
1601
+ * (a userId/tenant string, a header value) — a non-primitive field value is not a
1602
+ * clean principal and is skipped. A read here goes through the caller-supplied
1603
+ * request object directly (NOT the dev-mode guarded Proxy) — principal derivation
1604
+ * is the kernel's own inspection, not a public-factory closure, so it must never
1605
+ * itself trip the guarded-accessor demotion (INV-4 test 2).
1606
+ */
1607
+ function derivePrincipal(request) {
1608
+ if (request === void 0) return;
1609
+ for (const field of PRINCIPAL_FIELDS) {
1610
+ const value = request[field];
1611
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return `${field}:${value.toString()}`;
1612
+ }
1613
+ }
1614
+ /**
1615
+ * Hash a derived principal into the `usr:<hash>` scope suffix. A
1616
+ * synchronous SHA-256 truncated to 128 bits (32 hex) — hashing is not randomness,
1617
+ *and this keeps the fill path synchronous. The result is
1618
+ * non-empty and checkable, so the abolished hashless `usr:` literal can
1619
+ * never recur: every `usr:` scope carries a principal hash.
1620
+ */
1621
+ function hashPrincipal(principal) {
1622
+ return createHash("sha256").update(principal).digest("hex").slice(0, 32);
1623
+ }
1624
+ /**
1625
+ * The `<scope>` literal prefix of the per-principal storage class — the ONE
1626
+ * class `resolveScope` confines to the isolate-local L1. Shared by both directions so they
1627
+ * cannot drift: the MINT below builds a literal with it, and `storedScopeIsL1Only` reads a
1628
+ * STORED literal back through it. A serve derives confinement from the identity the entry was
1629
+ * stored under, so the two sides must agree on one prefix by construction.
1630
+ */
1631
+ const PRINCIPAL_SCOPE_PREFIX = "usr:";
1632
+ function resolveScope(input) {
1633
+ const { scope } = input;
1634
+ if (scope === "public") return {
1635
+ l1Only: false,
1636
+ literal: "pub"
1637
+ };
1638
+ if (scope !== void 0 && typeof scope === "object") return {
1639
+ l1Only: false,
1640
+ literal: `ten:${hashPrincipal(scope.tenant)}`
1641
+ };
1642
+ const principal = derivePrincipal(input.request);
1643
+ if (principal !== void 0) return {
1644
+ l1Only: true,
1645
+ literal: `${PRINCIPAL_SCOPE_PREFIX}${hashPrincipal(principal)}`
1646
+ };
1647
+ return {
1648
+ l1Only: false,
1649
+ literal: "pub"
1650
+ };
1651
+ }
1652
+ /**
1653
+ * Whether a STORED `<scope>` literal names the L1-confined class — the read-back twin
1654
+ * of `resolveScope`'s `l1Only`, over a literal rather than the call inputs a serve no longer has.
1655
+ * `pub`/`ten:` are shared-tier classes; only `usr:<hash>` is confined.
1656
+ *
1657
+ * NOTE the asymmetry this CANNOT see, by construction: a `pub` fill DEMOTED at fill time by the
1658
+ * dev-mode guarded-accessor Proxy stores the `pub` literal, so its confinement leaves no trace in
1659
+ * the stored identity. Such an entry reads back as `pub` here, and a serve reports its durability
1660
+ * ABSENT — unknowable, which is the honest answer, rather than a `true` the literal cannot
1661
+ * support.
1662
+ */
1663
+ function storedScopeIsL1Only(literal) {
1664
+ return literal.startsWith(PRINCIPAL_SCOPE_PREFIX);
1665
+ }
1666
+ /** The `<scope>` literal alone — for the contexts (canonical key, join
1667
+ * identity, key-selector purge) that need only the string, not the tier policy. */
1668
+ function scopeLiteral(input = {}) {
1669
+ return resolveScope(input).literal;
1670
+ }
1671
+ /** Normalize the config's `telemetry` (a bare sink function, a config object, or
1672
+ * absent) to `{ sink, hosted?, salt?, onSinkError? }`. A bare function carries no
1673
+ * hosted/salt/onSinkError; the config-object form passes them through unchanged. */
1674
+ function telemetryOf(config) {
1675
+ const { telemetry } = config;
1676
+ if (telemetry === void 0) return;
1677
+ if (typeof telemetry === "function") return { sink: telemetry };
1678
+ return telemetry;
1679
+ }
1680
+ /**
1681
+ * Pseudonymize one string field for a HOSTED sink: a per-project
1682
+ * HMAC-salted hash, so a short or low-entropy identifier (a key or a tag) is not
1683
+ * offline-invertible and never travels raw. The salt is the per-project secret
1684
+ * (`hostedTelemetry(salt)`), so holding key+tag fixed and changing ONLY the salt
1685
+ * changes the ref — and the ref tracks its own input, so a key ref moves iff the
1686
+ * key moves and a tag ref moves iff the tag moves (INV-15). HMAC (not a bare hash)
1687
+ * so the salt genuinely keys the digest per project.
1688
+ */
1689
+ function saltedRef(salt, value) {
1690
+ return createHmac("sha256", salt).update(value).digest("hex").slice(0, 32);
1691
+ }
1692
+ /** Redact one event field for a hosted sink: HMAC-salt every string (per-field,
1693
+ * including inside a string array — key ref, tag refs), leave non-string scalars
1694
+ * intact. `type` (the event NAME, not sensitive) is redacted by the caller's field
1695
+ * filter, never here. A field-total salted pseudonymization — not
1696
+ * anonymization. */
1697
+ function redactField(salt, value) {
1698
+ if (typeof value === "string") return saltedRef(salt, value);
1699
+ if (Array.isArray(value)) return value.map((element) => redactField(salt, element));
1700
+ return value;
1701
+ }
1702
+ /**
1703
+ * Emit a telemetry event. A plain sink receives the event verbatim
1704
+ * (raw key/tag/detail — redaction is hosted-only by design, INV-15). A HOSTED sink
1705
+ * receives a field-total salted pseudonymization: every string field EXCEPT `type`
1706
+ * (the event name) is replaced by a per-project HMAC-salted hash of its own value,
1707
+ * so no raw key/tag/salt/detail ever leaves the process. A hosted config
1708
+ * missing a salt cannot pseudonymize, so it is suppressed rather than leaking raw
1709
+ * fields. The final delivery — plain or redacted — goes through `safeEmit`, so an
1710
+ * app sink that throws or rejects is swallowed (its `onSinkError` hook notified) and
1711
+ * can never surface on the kernel's own path.
1712
+ */
1713
+ function emitTelemetry(config, event) {
1714
+ const telemetry = telemetryOf(config);
1715
+ if (telemetry === void 0) return;
1716
+ if (telemetry.hosted !== true) {
1717
+ safeEmit(telemetry.sink, event, telemetry.onSinkError);
1718
+ return;
1719
+ }
1720
+ const { salt } = telemetry;
1721
+ if (salt === void 0) return;
1722
+ const redacted = { type: event.type };
1723
+ for (const [field, value] of Object.entries(event)) {
1724
+ if (field === "type") continue;
1725
+ redacted[field] = redactField(salt, value);
1726
+ }
1727
+ safeEmit(telemetry.sink, redacted, telemetry.onSinkError);
1728
+ }
1729
+ /** The reserved per-key tag: `__k:<h>`, `<h>` a ≥128-bit (32 hex char) digest
1730
+ * of the canonical key (scoped, ns-versioned). SHA-256 truncated to 128 bits — a
1731
+ * synchronous digest (`node:crypto`, available on both Node and Bun) so the fill
1732
+ * path stays fully synchronous; hashing is not randomness. `expire
1733
+ * ({key})` recomputes this same digest to target the identical tag. */
1734
+ function keyTag(canonical) {
1735
+ return `__k:${createHash("sha256").update(canonical).digest("hex").slice(0, 32)}`;
1736
+ }
1737
+ /** The reserved per-namespace tag: `__ns:<name>`. Every envelope
1738
+ * carries it so `clear()`'s hard purge rides the same epoch/bus/conservative
1739
+ * machinery as any other purge — this is what lets a lagging isolate's read
1740
+ * fail closed instead of serving pre-clear data (INV-23). */
1741
+ function nsTag(namespace) {
1742
+ return `__ns:${namespace}`;
1743
+ }
1744
+ /** Validate the committed user-tag SET at the write boundary. Two ceilings, both
1745
+ * per typed `InvalidTagError`, never `NotImplementedError`:
1746
+ * - the `__`-prefix backstop, per tag — a forged reserved tag never reaches storage;
1747
+ * - the count ceiling, over the whole set — at most `MAX_USER_TAGS` user tags.
1748
+ * The caller passes whatever set is being committed — the declared tags at `getOrSet`'s
1749
+ * boundary, or the settle-union of declared + `setTags` + `dependsOn` — so the ceiling
1750
+ * is route-independent by construction: one function, one set, one boundary. The reserved
1751
+ * `__k`/`__ns` tags are stamped separately and never counted here. The set is expected already
1752
+ * deduped (the ceiling counts distinct user tags, not raw call arity). */
1753
+ function validateUserTags(tags) {
1754
+ if (tags === void 0) return;
1755
+ if (tags.length > 126) throw new InvalidTagError(tags[126] ?? "", `an entry may not carry more than 126 user tags (§2.3, MI13); got ${tags.length}`);
1756
+ for (const tag of tags) assertUserTag(tag);
1757
+ }
1758
+ /** `tags` are sorted, deduped, and include `__k`/`__ns`. Dedupes FIRST, then validates the
1759
+ * distinct set — so the count ceiling measures distinct user tags, not raw call
1760
+ * arity — and returns just the caller-supplied set, sorted and deduped. The reserved
1761
+ * `__k`/`__ns` tags are appended separately by `envelopeTags` below, since they depend on the
1762
+ * canonical key / namespace, not just the call options. */
1763
+ function sortedUserTags(tags) {
1764
+ if (tags === void 0) return [];
1765
+ const distinct = [...new Set(tags)].toSorted();
1766
+ validateUserTags(distinct);
1767
+ return distinct;
1768
+ }
1769
+ /** The full stored-envelope tag set: sorted user tags plus the reserved
1770
+ * `__k:<h>` (per-key digest of the canonical key) and `__ns:<name>` (per-namespace
1771
+ * watermark) tags. This is what rides the dual-epoch invalidation core, so
1772
+ * `expire({key})` / `clear()` invalidate through the same machinery as any tag
1773
+ * purge. */
1774
+ function envelopeTags(userTags, canonical, namespace) {
1775
+ return [
1776
+ ...userTags,
1777
+ keyTag(canonical),
1778
+ nsTag(namespace)
1779
+ ].toSorted();
1780
+ }
1781
+ function makeTagAccumulator(declared) {
1782
+ const membership = [];
1783
+ let authored;
1784
+ let settled = false;
1785
+ return {
1786
+ isSettled() {
1787
+ return settled;
1788
+ },
1789
+ markSettled() {
1790
+ settled = true;
1791
+ },
1792
+ addMembership(tag) {
1793
+ assertUserTag(tag);
1794
+ membership.push(tag);
1795
+ },
1796
+ author(tags) {
1797
+ for (const tag of tags) assertUserTag(tag);
1798
+ authored = [...tags];
1799
+ },
1800
+ settleUserTags() {
1801
+ return [.../* @__PURE__ */ new Set([...authored ?? declared, ...membership])].toSorted();
1802
+ }
1803
+ };
1804
+ }
1805
+ /** Resolve the codec, falling back to the built-in safe-mode codec when the
1806
+ * config wires none — `world()` wires no codec, so the core needs a default
1807
+ * to store/read values at all. */
1808
+ function codecOf(config) {
1809
+ return config.codec ?? safeCodec;
1810
+ }
1811
+ /**
1812
+ * The consistency this call reads at: per-call, then the instance's declared default, then
1813
+ * the own (`eventual`).
1814
+ *
1815
+ * The middle step is what makes `CacheDefaults.consistency` a setting rather than a decoration:
1816
+ * without it, a caller who declared `defaults: { consistency: 'strong' }` got eventual reads and
1817
+ * nothing said so. The final fallback is unchanged and matches what every shipped default already
1818
+ * declares, so an instance that sets nothing reads exactly as it did before.
1819
+ */
1820
+ function consistencyOf(config, options) {
1821
+ return options.consistency ?? config.defaults?.consistency ?? "eventual";
1822
+ }
1823
+ /**
1824
+ * How fail-closed `unknown` resolves for this instance.
1825
+ *
1826
+ * `takedownSensitive` is an OVERRIDE, and the precedence is the whole point. It outranks an
1827
+ * explicitly-set `unknownPolicy` rather than merely filling in for an absent one — because as an
1828
+ * absent-policy default it was unreachable in practice: any composition that sets `unknownPolicy`
1829
+ * shadows it, and `createWorkersCache`'s own safe posture sets it, so the switch was dead on the
1830
+ * entire hero path. A legal-embargo control that a routine, recommended default silently disables is
1831
+ * worse than no control: it reads as protection and does nothing.
1832
+ *
1833
+ * Reading it as an override is also what the flag MEANS. `unknownPolicy` says how this namespace
1834
+ * would like unknowns resolved; `takedownSensitive` says this namespace is one where guessing is not
1835
+ * allowed. The second is a statement about the CONTENT, so it cannot be outranked by a preference.
1836
+ */
1837
+ function unknownResolution(config) {
1838
+ if (config.takedownSensitive === true || config.defaults?.takedownSensitive === true) return {
1839
+ armedBy: "takedownSensitive",
1840
+ kind: "error"
1841
+ };
1842
+ const policy = config.defaults?.unknownPolicy ?? "registry-check";
1843
+ return policy === "error" ? {
1844
+ armedBy: "unknownPolicy",
1845
+ kind: "error"
1846
+ } : policy;
1847
+ }
1848
+ /** The `staleIfError` predicate for this instance — the configured
1849
+ * override, or the default HTTP classifier. */
1850
+ function staleIfErrorOf(config) {
1851
+ return config.defaults?.staleIfError ?? isRetriableHttp;
1852
+ }
1853
+ /** Whether a thrown error is the `not_found` fact (a 404) — never masked,
1854
+ * never retriable, but eligible for negative caching when `notFoundTtl` is declared. */
1855
+ function isNotFound(err) {
1856
+ return err instanceof HttpError && err.status === 404;
1857
+ }
1858
+ /** A miss with no live check performed (absent / codecId / decode). */
1859
+ const MISS_READ = { checkedThrough: 0 };
1860
+ /** Whether a stored entry's codec identity matches (or is an accepted migration
1861
+ * source) — checked BEFORE decode so a foreign codec never runs. */
1862
+ function codecIdMatches(storedCodecId, codec, config) {
1863
+ if (storedCodecId === codec.id) return true;
1864
+ const acceptCodecs = config.defaults?.acceptCodecs ?? [];
1865
+ return typeof storedCodecId === "string" && acceptCodecs.includes(storedCodecId);
1866
+ }
1867
+ /** Reconstruct the envelope shell from stored bytes + metadata. `bornEpoch`/
1868
+ * `tags` ride in metadata so validation runs without a second decode. */
1869
+ function envelopeFromStore(key, raw, metadata, decoded, codecId) {
1870
+ const bornEpoch = typeof metadata.bornEpoch === "number" ? metadata.bornEpoch : 0;
1871
+ const bornMs = typeof metadata.bornMs === "number" ? metadata.bornMs : 0;
1872
+ const tags = Array.isArray(metadata.tags) ? metadata.tags.filter((t) => typeof t === "string") : [];
1873
+ return {
1874
+ bornEpoch,
1875
+ bornMs,
1876
+ codecId,
1877
+ enc: "json",
1878
+ grace: 0,
1879
+ key,
1880
+ kind: metadata.kind === "neg" ? "neg" : "val",
1881
+ scope: typeof metadata.scope === "string" ? metadata.scope : "pub",
1882
+ size: raw.length,
1883
+ src: "factory",
1884
+ storedAt: bornMs,
1885
+ tags,
1886
+ ttl: 0,
1887
+ v: 3,
1888
+ val: decoded,
1889
+ ...typeof metadata.fence === "number" ? { fence: metadata.fence } : {}
1890
+ };
1891
+ }
1892
+ /**
1893
+ * The value a HIT on this envelope SURFACES.
1894
+ *
1895
+ * A `kind:'neg'` envelope is a remembered 404: it caches the not-found FACT, and the envelope gives it no
1896
+ * value to carry. Its stored bytes are a JSON-representable placeholder (`writeNegEnvelope`) chosen
1897
+ * only because `undefined` is not a storable body — they are never the answer. So a read of a neg
1898
+ * surfaces `undefined`, which is EXACTLY what the fill turn that wrote it surfaced (INV-7): the
1899
+ * point of caching a 404 is that the next reader gets the same answer without paying the origin,
1900
+ * and an answer that changes on the second call is not a cached fact at all. Serving the placeholder
1901
+ * instead handed the caller a `null` no origin ever produced, and reported a durable VALUE hit for
1902
+ * an entry holding only a negative.
1903
+ *
1904
+ * A `val` envelope surfaces its decoded value unchanged. This is the ONE point where the two kinds
1905
+ * diverge on the read path; every other neg rule is already a refusal to serve at all ( a
1906
+ * stale neg is never grace/SWR-served, so the only servable neg is a fresh one).
1907
+ */
1908
+ function servedValue(envelope) {
1909
+ if (envelope.kind === "neg") return;
1910
+ return envelope.val;
1911
+ }
1912
+ /**
1913
+ * Classify a rejected `Store.get` — the read-side twin of `classifyWriteBackFailure`, and
1914
+ * consistent with its reasoning rather than merely its shape:
1915
+ *
1916
+ * - CLASSIFIED (`throttled` / `unavailable`) ⇒ returned to the caller, which absorbs it. The kernel signs
1917
+ * that a suppressed L2 GET error is a plain MISS — never routed to unknown/fail-closed — so on
1918
+ * the serving path the caller refills, and a configured L1 (checked FIRST) may already
1919
+ * have served without this tier being read at all.
1920
+ * - UNCLASSIFIED ⇒ RETHROWN unchanged, for the same reason the write half rethrows one: the kernel
1921
+ * absorbs only failures a driver has named, and a rejection nobody classified is not evidence of
1922
+ * absence. Swallowing it would turn an unknown fault into a confident "not found" — which, on a
1923
+ * read, is the one answer that silently refills over whatever is really there.
1924
+ *
1925
+ * Suppression is the only telemetry-bearing outcome (`store_read_suppressed`, a raw fact at the
1926
+ * instant it occurs). A rethrown error needs no event: it reaches the caller intact.
1927
+ */
1928
+ function classifyStoreReadFailure(config, key, error) {
1929
+ if (!isStoreReadError(error)) throw error;
1930
+ emitTelemetry(config, {
1931
+ code: error.code,
1932
+ key,
1933
+ type: TELEMETRY_EVENTS.store_read_suppressed
1934
+ });
1935
+ return error;
1936
+ }
1937
+ /** The ONE spelling of the "the store did not answer" discriminant, shared by the probe's outcome
1938
+ * and the witness's own arm so the two can never drift apart. */
1939
+ const READ_FAILED_KIND = "read-failed";
1940
+ /** The two probe outcomes that carry no payload — one shared instance each, exactly as `MISS_READ`
1941
+ * is for the read path. */
1942
+ const PROBE_ABSENT = { kind: "absent" };
1943
+ const PROBE_READ_FAILED = { kind: READ_FAILED_KIND };
1944
+ /**
1945
+ * A NON-mutating presence probe for `explain`: read `canonical` from `store` with a
1946
+ * plain `store.get` — never a hydrating read — and reconstruct its envelope plus the RAW stored
1947
+ * scope; `absent` when the entry is missing, codec-mismatched, or undecodable; `read-failed`
1948
+ * when the store's own read was suppressed. The read fails closed exactly as the serve path
1949
+ * does (INV-8/27); it observes, it never writes.
1950
+ */
1951
+ async function probeStoredEntry(store, codec, config, canonical) {
1952
+ let stored;
1953
+ try {
1954
+ stored = await store.get(canonical);
1955
+ } catch (error) {
1956
+ classifyStoreReadFailure(config, canonical, error);
1957
+ return PROBE_READ_FAILED;
1958
+ }
1959
+ if (!stored || !codecIdMatches(stored.metadata?.codecId, codec, config)) return PROBE_ABSENT;
1960
+ let decoded;
1961
+ try {
1962
+ decoded = codec.decode(stored.value);
1963
+ } catch {
1964
+ return PROBE_ABSENT;
1965
+ }
1966
+ const envelope = envelopeFromStore(canonical, stored.value, stored.metadata ?? {}, decoded, codec.id);
1967
+ const storedScope = stored.metadata?.scope;
1968
+ const scope = typeof storedScope === "string" ? {
1969
+ kind: "readable",
1970
+ literal: storedScope
1971
+ } : { kind: "unreadable" };
1972
+ const storedTtl = stored.metadata?.ttl;
1973
+ return {
1974
+ envelope,
1975
+ kind: "found",
1976
+ scope,
1977
+ ttl: typeof storedTtl === "number" ? {
1978
+ kind: "stored",
1979
+ seconds: storedTtl
1980
+ } : { kind: "not-stored" }
1981
+ };
1982
+ }
1983
+ /**
1984
+ * THE READ PATH'S CLASSIFIER for the SERVING path. Given raw stored bytes and metadata, it answers
1985
+ * what a reader on THIS instance would make of them: an envelope, or nothing at all. (The
1986
+ * introspection probe still runs its own copy of this sequence — see `probeStoredEntry`, which
1987
+ * answers a different question and must report the causes of absence rather than collapse them to
1988
+ * one. Folding the two is worth doing and is not this change.)
1989
+ *
1990
+ * Two gates precede the reconstruction and both fail CLOSED, because a reader that cannot identify
1991
+ * or decode an entry is served a miss rather than a guess — the codecId check (INV-27/ : a
1992
+ * mismatch `acceptCodecs` does not admit is a miss) and `decode` itself (INV-8: a body this codec
1993
+ * cannot read is a miss). Only past both is an envelope built.
1994
+ *
1995
+ * Both seats call THIS function, by identity, and that is the point. "Servable" is defined by the
1996
+ * read path, so the write layer's neg-write guard must ask the READER's question rather than a looser one
1997
+ * of its own: an envelope no reader can classify as a val is not a servable good value — it is a
1998
+ * miss wearing bytes, and protecting it protects a ghost. Answering that question with a partial
1999
+ * copy of these steps is exactly how two seats stop meaning one thing while each keeps passing its
2000
+ * own tests.
2001
+ */
2002
+ function readerEnvelope(config, codec, key, stored) {
2003
+ if (!codecIdMatches(stored.metadata?.codecId, codec, config)) return;
2004
+ let decoded;
2005
+ try {
2006
+ decoded = codec.decode(stored.value);
2007
+ } catch {
2008
+ return;
2009
+ }
2010
+ return envelopeFromStore(key, stored.value, stored.metadata ?? {}, decoded, codec.id);
2011
+ }
2012
+ /**
2013
+ * Read one tier: codecId-check + decode + validate against knowledge.
2014
+ * `hit` is a `ReadHit` (fresh or stale-servable) or `undefined` (absent / dead /
2015
+ * decode failure / codecId mismatch / suppressed read failure / unknown-fail-closed ⇒ the
2016
+ * caller refills). `checkedThrough` is the epoch a live check by THIS read verified (0 if
2017
+ * none), which the refill uses so a behind isolate does not fence its own legitimate refetch.
2018
+ */
2019
+ async function readTier(store, codec, config, inval, consistency, key) {
2020
+ let stored;
2021
+ try {
2022
+ stored = await store.get(key);
2023
+ } catch (error) {
2024
+ classifyStoreReadFailure(config, key, error);
2025
+ return MISS_READ;
2026
+ }
2027
+ if (!stored) return MISS_READ;
2028
+ const envelope = readerEnvelope(config, codec, key, stored);
2029
+ if (!envelope) return MISS_READ;
2030
+ const storedScope = stored.metadata?.scope;
2031
+ const rawScope = typeof storedScope === "string" ? storedScope : void 0;
2032
+ const scopeEvidence = {
2033
+ scopeUnreadable: rawScope === void 0,
2034
+ ...rawScope === void 0 ? {} : { rawScope }
2035
+ };
2036
+ if (!inval) return {
2037
+ checkedThrough: 0,
2038
+ hit: {
2039
+ ...scopeEvidence,
2040
+ envelope,
2041
+ stale: false,
2042
+ value: servedValue(envelope)
2043
+ }
2044
+ };
2045
+ const { validity, checkedThrough } = await inval.validate(envelope, consistency, unknownResolution(config), config.defaults?.onUnavailable);
2046
+ if (validity === "fresh") return {
2047
+ checkedThrough,
2048
+ hit: {
2049
+ ...scopeEvidence,
2050
+ envelope,
2051
+ stale: false,
2052
+ value: servedValue(envelope)
2053
+ }
2054
+ };
2055
+ if (validity === "stale" && envelope.kind !== "neg") return {
2056
+ checkedThrough,
2057
+ hit: {
2058
+ ...scopeEvidence,
2059
+ envelope,
2060
+ stale: true,
2061
+ value: servedValue(envelope)
2062
+ }
2063
+ };
2064
+ if (envelope.kind === "neg") return { checkedThrough };
2065
+ return {
2066
+ checkedThrough,
2067
+ staleCandidate: {
2068
+ ...scopeEvidence,
2069
+ envelope,
2070
+ stale: true,
2071
+ value: servedValue(envelope)
2072
+ }
2073
+ };
2074
+ }
2075
+ /**
2076
+ * DEVELOPMENT-TIME TEACHING TOOL — NOT A PRODUCTION SAFETY NET. It is dev-gated by design
2077
+ * (`config.dev === true`), and it stays that way. Two readings to head off, in both directions:
2078
+ *
2079
+ * - DO NOT PROMOTE IT TO PRODUCTION believing it closes a privacy hole. It does not close one:
2080
+ * an application that hand-mixes per-principal content into a public-scoped fill without
2081
+ * touching `ctx.request` at all is untouched by it, so it was never a boundary — it is a
2082
+ * tripwire that catches the honest mistake early, in the one environment where the developer
2083
+ * is watching. Making the demotion live in production would make an entry's STORAGE TIER a
2084
+ * function of what a factory happened to read on one execution: the same key lands durable on
2085
+ * the request whose factory took the cache-hit path and L1-only on the request whose factory
2086
+ * read the principal, so the tier stops being a property of the call and becomes a property of
2087
+ * the run. That is nondeterministic caching, and it is a worse bargain than the mistake it
2088
+ * would be trying to catch.
2089
+ *
2090
+ * - DO NOT DELETE IT believing it is dead code. INV-4 pins it, and the demotion is exactly
2091
+ * what that invariant's enforceable half asserts — a public fill that read request state is
2092
+ * confined to L1 and never reaches the shared tier. Blind it and the sacred suite reds.
2093
+ *
2094
+ * It deliberately does NOT feed the shared-cacheability gate. That gate answers a different
2095
+ * question — whether the scopes the library DECLARED and STORED permit a shared HTTP cache to keep
2096
+ * the response — and it answers it from evidence that survives the request. This guard sees
2097
+ * only what one factory did while it ran. Wiring a runtime observation into that gate would make
2098
+ * the emitted response posture vary run to run for the same route, which is the same nondeterminism
2099
+ * as above, on the wire instead of in the tiers.
2100
+ */
2101
+ function buildRequestGuard(config, options) {
2102
+ const { request } = options;
2103
+ if (!(config.dev === true && options.scope === "public") || request === void 0) return {
2104
+ request,
2105
+ wasTouched: () => false
2106
+ };
2107
+ let touched = false;
2108
+ return {
2109
+ request: new Proxy(request, { get(target, prop, receiver) {
2110
+ touched = true;
2111
+ return Reflect.get(target, prop, receiver);
2112
+ } }),
2113
+ wasTouched: () => touched
2114
+ };
2115
+ }
2116
+ /** Acquire the cross-isolate lock if requested, returning its fence handle. */
2117
+ async function maybeLock(config, options) {
2118
+ if (options.lock !== true) return;
2119
+ const { lock } = config;
2120
+ if (!lock) return;
2121
+ return await lock.acquire(options.key);
2122
+ }
2123
+ /**
2124
+ * Classify a rejected L2 write-back into the kernel's write-failure semantics.
2125
+ * WIRES CLASSIFICATION + TELEMETRY ONLY — no REFRESH_QUEUE / retry machinery is built
2126
+ * (that is deferred). A recognized transient failure is SUPPRESSED: the write did not
2127
+ * become durable, but the operation does not fail the caller and produces no
2128
+ * fail-closed/unknown state — the function returns `false` (non-durable) and emits the
2129
+ * reserved telemetry name. Everything else keeps today's behavior and PROPAGATES:
2130
+ * - `throttled` (retryable) → `writeback_throttled`, suppressed (`durable:false`).
2131
+ * - `unavailable` (retryable) → `l2_write_error`, suppressed (`durable:false`).
2132
+ * - `too_large` (permanent, non-retryable) → rethrown, never silently suppressed.
2133
+ * - unclassified → surfaced as `l2_write_error` telemetry, then rethrown unchanged.
2134
+ */
2135
+ function classifyWriteBackFailure(config, key, error) {
2136
+ const classified = isStoreWriteError(error) ? error : void 0;
2137
+ if (classified?.code === "throttled") {
2138
+ emitTelemetry(config, {
2139
+ key,
2140
+ type: "writeback_throttled"
2141
+ });
2142
+ return false;
2143
+ }
2144
+ if (classified?.code === "unavailable") {
2145
+ emitTelemetry(config, {
2146
+ key,
2147
+ type: "l2_write_error"
2148
+ });
2149
+ return false;
2150
+ }
2151
+ if (classified === void 0) emitTelemetry(config, {
2152
+ key,
2153
+ type: "l2_write_error"
2154
+ });
2155
+ throw error;
2156
+ }
2157
+ /**
2158
+ * Is this stored envelope a `val` that protects? THE suppression-worthiness
2159
+ * test, and it lives in ONE place because two seats have to agree about it:
2160
+ *
2161
+ * - the SERVE seat (`gracedStillServable`) asks it of the candidate THIS read holds, deciding
2162
+ * whether an error-path serve may draw on it;
2163
+ * - the WRITE seat (`negDisplacesProtectedVal`) asks it of the envelope the born-monotone compare
2164
+ * already fetched, deciding whether a `kind:'neg'` candidate may land on it.
2165
+ *
2166
+ * One function, because the two answers may never disagree in the direction that destroys data: a
2167
+ * val the serve path would hand to a caller is exactly a val the write path must refuse to bury.
2168
+ * Written twice they drift, and the drift is silent — each site keeps passing its own tests while
2169
+ * the pair stops meaning one thing.
2170
+ *
2171
+ * `validity` is the verdict, produced by the same machinery on both sides. `unknown` is
2172
+ * deliberately NOT settled here: it is a GAP rather than a window, and the two seats answer a gap
2173
+ * differently on purpose — the serve seat refuses to serve THROUGH one, the write seat refuses to
2174
+ * write through one. Each states that rule, with its own derivation, at its own site.
2175
+ */
2176
+ function e7ProtectsStoredVal(envelope, validity) {
2177
+ return envelope.kind !== "neg" && (validity === "fresh" || validity === "stale");
2178
+ }
2179
+ /** The `UnknownResolution` a RE-VALIDATION of an already-stored envelope asks for: settle
2180
+ * an unknown tag with one live registry read. Named once because the two seats that re-validate the
2181
+ * same envelope — the serve seat's grace check and the write seat's neg-write guard — must ask the SAME
2182
+ * question of it; drifting onto different resolutions would let them reach different verdicts about
2183
+ * one entry and call it agreement. Distinct from `unknownResolution`, which reports the instance's
2184
+ * configured READ-time policy. */
2185
+ const REVALIDATION_RESOLUTION = "registry-check";
2186
+ /**
2187
+ * the kind-aware guard: would landing a `kind:'neg'` candidate here bury a
2188
+ * `val` the letter protects? Asked of the envelope the born-monotone compare ALREADY fetched, which
2189
+ * is why it lives at the write layer — one boundary, so every neg-write source passes it identically
2190
+ * and the outcome cannot vary by the route that reached it.
2191
+ *
2192
+ * The question is ONTOLOGICAL: what does the STORE hold? That is a different question from the one
2193
+ * the filling caller's own read can answer, and the gap between them is the whole defect this closes
2194
+ * — a caller blind to a servable val (a tier that was unreadable for a moment, or a join whose
2195
+ * leader read nothing) would otherwise write straight over it, and every later reader would be
2196
+ * served the negative. Note also which KIND is which: `FilledAttempt.storedKind` describes what THIS
2197
+ * fill produced, while the kind read here is the STORED envelope's — different objects, and only the
2198
+ * second one can answer this.
2199
+ *
2200
+ * The within-grace term is not re-invented: it is the verdict `validate` gives, asked on the
2201
+ * same terms the serve seat asks for it (`gracedStillServable`), and settled by the shared predicate
2202
+ * both seats share.
2203
+ */
2204
+ async function negDisplacesProtectedVal(config, key, existing, inval, consistency) {
2205
+ const stored = readerEnvelope(config, codecOf(config), key, existing);
2206
+ if (!stored) return false;
2207
+ if (!inval) return e7ProtectsStoredVal(stored, "fresh");
2208
+ let validity;
2209
+ try {
2210
+ ({validity} = await inval.validate(stored, consistency, REVALIDATION_RESOLUTION, config.defaults?.onUnavailable));
2211
+ } catch {
2212
+ validity = "unknown";
2213
+ }
2214
+ return e7ProtectsStoredVal(stored, validity === "unknown" ? "stale" : validity);
2215
+ }
2216
+ /**
2217
+ * Born-monotone write-back (invariant 17): never replace a stored
2218
+ * envelope with a strictly-higher `(bornEpoch, fence)`. Read-compare-write against
2219
+ * L2 — the KV CAS-less residual race is documented and bounded to one refresh cycle.
2220
+ *`bornMs` is never an arbiter. `durable` is `true` when the candidate landed (or a
2221
+ * strictly-newer envelope already sits durably), `false` when a classified `throttled`/`unavailable`
2222
+ * failure suppressed it.
2223
+ *
2224
+ * It is ALSO where the kind-aware guard sits, for the reason the guard needs and this function
2225
+ * already has: the stored envelope is in hand here. A `kind:'neg'` candidate may only land on a true
2226
+ * miss, so every neg-write source — a solo caller's own disposition, a blind leader's in-window
2227
+ * recording — meets the same test on the same evidence, and route-independence holds by construction
2228
+ * rather than by matching two call sites. `val`-over-`neg` is untouched: recovery is the direction
2229
+ * The neg-write guard leaves open, and the guard is asked only of a neg candidate.
2230
+ */
2231
+ async function bornMonotoneWrite(config, l2, key, encoded, candidate, inval, consistency) {
2232
+ let existing;
2233
+ try {
2234
+ existing = await l2.get(key);
2235
+ } catch (error) {
2236
+ return {
2237
+ durable: classifyWriteBackFailure(config, key, error),
2238
+ writeSuppressed: false
2239
+ };
2240
+ }
2241
+ if (existing) {
2242
+ if (candidate.kind === "neg" && await negDisplacesProtectedVal(config, key, existing, inval, consistency)) {
2243
+ emitTelemetry(config, {
2244
+ key,
2245
+ type: TELEMETRY_EVENTS.neg_suppressed
2246
+ });
2247
+ return {
2248
+ durable: false,
2249
+ writeSuppressed: true
2250
+ };
2251
+ }
2252
+ const meta = existing.metadata ?? {};
2253
+ const storedBornEpoch = typeof meta.bornEpoch === "number" ? meta.bornEpoch : 0;
2254
+ const storedFence = typeof meta.fence === "number" ? meta.fence : -1;
2255
+ const candidateFence = candidate.fence ?? -1;
2256
+ if (storedBornEpoch > candidate.bornEpoch || storedBornEpoch === candidate.bornEpoch && storedFence > candidateFence) return {
2257
+ durable: true,
2258
+ writeSuppressed: false
2259
+ };
2260
+ }
2261
+ try {
2262
+ await l2.set(key, encoded, { metadata: {
2263
+ bornEpoch: candidate.bornEpoch,
2264
+ bornMs: candidate.bornMs,
2265
+ codecId: candidate.codecId,
2266
+ enc: candidate.enc,
2267
+ kind: candidate.kind,
2268
+ scope: candidate.scope,
2269
+ tags: candidate.tags,
2270
+ v: candidate.v,
2271
+ ...candidate.fence === void 0 ? {} : { fence: candidate.fence }
2272
+ } });
2273
+ return {
2274
+ durable: true,
2275
+ writeSuppressed: false
2276
+ };
2277
+ } catch (error) {
2278
+ return {
2279
+ durable: classifyWriteBackFailure(config, key, error),
2280
+ writeSuppressed: false
2281
+ };
2282
+ }
2283
+ }
2284
+ /**
2285
+ * Re-validate a graced candidate's tags AT SERVE TIME: a hard delete
2286
+ * that landed after the candidate was read (e.g. during the failed refresh) must
2287
+ * fence it here — a graced value is never served once its purge is visible. No
2288
+ * `Invalidation` (registry-absent) ⇒ nothing to invalidate it ⇒ always servable.
2289
+ *
2290
+ * AT `strong` THIS IS A LIVE REGISTRY CHECK LIKE ANY OTHER, so it honors `onUnavailable`
2291
+ * exactly as its sibling sites do. Leaving it unguarded did not avoid a throw — it MANUFACTURED
2292
+ * one: the guard inside `validate` fires for a caller who explicitly opted out, which is the one
2293
+ * caller who asked not to see it.
2294
+ *
2295
+ * `factoryError` is what this re-validation runs on behalf of, and it rides out as the thrown
2296
+ * error's `cause`. That answers the reason the guard was omitted here — the fear that a throw would
2297
+ * REPLACE the caller's factory error — without leaving the site unguarded: nothing is dropped, and
2298
+ * callers still get the one type the API names.
2299
+ *
2300
+ * WHEN THE OPT-OUT IS TAKEN this call drops to what an eventual read would do, and the candidate is
2301
+ * settled against local knowledge alone. A tag whose hardness cannot be established stays UNKNOWN,
2302
+ * and an unknown tag is never graced (ladder sends unknown to 5a and never to grace) —
2303
+ * when hardness is exactly what we failed to learn, a hard purge is one of the things the gap could
2304
+ * be concealing. So an unknown candidate is not servable, and the factory's own error is what the
2305
+ * caller ends up with.
2306
+ */
2307
+ async function gracedStillServable(inval, consistency, envelope, onUnavailable, factoryError) {
2308
+ if (!inval) return e7ProtectsStoredVal(envelope, "fresh");
2309
+ let validity;
2310
+ try {
2311
+ ({validity} = await inval.validate(envelope, consistency, REVALIDATION_RESOLUTION, onUnavailable));
2312
+ } catch (error) {
2313
+ if (error instanceof RegistryUnavailableError) throw new RegistryUnavailableError(error.message, { cause: factoryError });
2314
+ throw error;
2315
+ }
2316
+ return e7ProtectsStoredVal(envelope, validity);
2317
+ }
2318
+ /**
2319
+ * Write a `kind:'neg'` envelope for a `not_found` fact.
2320
+ * `notFoundTtl` is the per-call opt-in; the write shares the same born-monotone
2321
+ * fence as any write-back.
2322
+ *
2323
+ * Returns the write's OUTCOME (durability, and whether the guard refused it) AND
2324
+ * the `bornMs` it stamped — the neg's birth instant, read off the clock exactly once here and
2325
+ * carried out for every later consumer, the same way a value fill's is.
2326
+ *
2327
+ * `inval`/`consistency` ride down for the guard alone: it decides against the STORE, and deciding
2328
+ * against the store means asking the same validity question a reader asks of what it finds there.
2329
+ */
2330
+ async function writeNegEnvelope(config, key, tags, scope, bornEpoch, inval, consistency) {
2331
+ const { l2 } = config;
2332
+ const bornMs = config.clock.now();
2333
+ if (!l2) return {
2334
+ bornMs,
2335
+ durable: true,
2336
+ writeSuppressed: false
2337
+ };
2338
+ const codec = codecOf(config);
2339
+ const encoded = codec.encode(null);
2340
+ return {
2341
+ bornMs,
2342
+ ...await bornMonotoneWrite(config, l2, key, encoded, {
2343
+ bornEpoch,
2344
+ bornMs,
2345
+ enc: "json",
2346
+ grace: 0,
2347
+ key,
2348
+ kind: "neg",
2349
+ scope,
2350
+ size: encoded.length,
2351
+ src: "factory",
2352
+ storedAt: bornMs,
2353
+ tags,
2354
+ ttl: 0,
2355
+ v: 3,
2356
+ val: null,
2357
+ codecId: codec.id
2358
+ }, inval, consistency)
2359
+ };
2360
+ }
2361
+ /** Build the INV-26 neg-suppression serve of a still-servable candidate (a 404 that a servable
2362
+ * `val` displaces). ONE constructor for both call sites — the leader-sighted classification and a
2363
+ * blind leader's per-waiter tail — so `value` is always the served candidate's own value.
2364
+ *
2365
+ * It also EMITS, and the emit belongs here rather than at either caller. The design lists `neg_suppressed`
2366
+ * among a suppression's consequences, and the design attaches those consequences to whichever seat renders
2367
+ * the suppression, one event per suppression. This is the serve seat's renderer; the write layer's
2368
+ * guard emits its own at its own site. Emitting from the shared constructor is what makes one
2369
+ * event per suppression structural instead of a convention two call sites have to remember —
2370
+ * before it, the epistemic suppression that the own rationale describes fired no telemetry at all,
2371
+ * so the commonest case was the silent one. */
2372
+ function negSuppressed(config, key, served) {
2373
+ emitTelemetry(config, {
2374
+ key,
2375
+ type: TELEMETRY_EVENTS.neg_suppressed
2376
+ });
2377
+ return {
2378
+ kind: "neg-suppressed",
2379
+ served,
2380
+ value: served.hit.value
2381
+ };
2382
+ }
2383
+ /**
2384
+ * Write the `not_found` neg and package it as the `filled` attempt every
2385
+ * SERVED-fill path already understands: `value:undefined`, the neg's stored tag set, and the write's
2386
+ * own `durable`/`bornMs`. A neg carries no `bornEpoch` on the outcome — its presence is the marker
2387
+ * of a successful VALUE fill, which this is not — so L1 hydration falls back to the caller's
2388
+ * outer floor exactly as the value path intends.
2389
+ *
2390
+ * ONE writer of the neg for two call sites: the leader that read a candidate of its own that
2391
+ * re-validated DEAD (its own disposition, in `classifyFillError`), and any blind waiter's own tail
2392
+ * (`runOwnNotFoundTail`). Both stamp the fill's tags/scope/bornEpoch, so a neg written on either path
2393
+ * is byte-identical to the one a solo caller writes — and both meet the write-layer guard, which is
2394
+ * why a SUPPRESSED write is one of the outcomes this can carry (`writeSuppressed`): the fill still
2395
+ * produced the not-found FACT and still serves it, but nothing was stored and nothing is evicted.
2396
+ */
2397
+ async function negWriteFilled(config, key, tags, scope, bornEpoch, inval, consistency) {
2398
+ const neg = await writeNegEnvelope(config, key, tags, scope, bornEpoch, inval, consistency);
2399
+ return {
2400
+ bornMs: neg.bornMs,
2401
+ durable: neg.durable,
2402
+ kind: "filled",
2403
+ storedKind: "neg",
2404
+ tags,
2405
+ value: void 0,
2406
+ writeSuppressed: neg.writeSuppressed
2407
+ };
2408
+ }
2409
+ /**
2410
+ * The factory error-path classification, extracted out of
2411
+ * `fillAndStore` to keep its cognitive complexity down:
2412
+ * - `ctx.skip()` (M10/invariant 24): `{kind:'skipped'}` — nothing stored,
2413
+ * nothing served; no write-back of any kind, not even a neg.
2414
+ * - `not_found` (404, a fact — never masked): with `notFoundTtl` declared and THIS fill's own read
2415
+ * holding a still-servable `val`, suppress the neg-write and serve that val — a neg must never
2416
+ * displace a servable value — with no `servedOnError` (a suppressed neg is not an
2417
+ * error-path serve). This fill's own copy re-validated DEAD ⇒ write the neg and serve it (its own
2418
+ * disposition). This fill read NO copy at all (a blind leader) ⇒ ride the 404 out as a shareable
2419
+ * `not-found` FACT so each waiter runs its own tail against its own read ( `NotFoundFact
2420
+ * Attempt`), the mirror of the retriable branch's fenced-vs-fact split just below. Without
2421
+ * `notFoundTtl`, rethrow.
2422
+ * - retriable (`staleIfError`, default `isRetriableHttp`) with a graced `val`
2423
+ * still servable at serve time: serve it, `servedOnError: true`. A
2424
+ * `kind:'neg'` graced candidate is NEVER offered here — it is never grace/SIE-
2425
+ * served — so a neg-only history rethrows.
2426
+ * - otherwise (non-retriable, or no servable graced copy): rethrow — a failed or
2427
+ * timed-out factory never modifies the stored good value.
2428
+ */
2429
+ async function classifyFillError(error, config, key, tags, scope, bornEpoch, inval, consistency, options, graced) {
2430
+ if (error instanceof SkipSignalError) return { kind: "skipped" };
2431
+ if (isNotFound(error)) {
2432
+ if (options.notFoundTtl === void 0) throw error;
2433
+ if (graced) {
2434
+ if (await gracedStillServable(inval, consistency, graced.hit.envelope, config.defaults?.onUnavailable, error)) return negSuppressed(config, key, graced);
2435
+ return await negWriteFilled(config, key, tags, scope, bornEpoch, inval, consistency);
2436
+ }
2437
+ return {
2438
+ error,
2439
+ kind: "not-found",
2440
+ neg: await negWriteFilled(config, key, tags, scope, bornEpoch, inval, consistency),
2441
+ scope
2442
+ };
2443
+ }
2444
+ if (options.grace !== void 0 && staleIfErrorOf(config)(error)) {
2445
+ if (graced) {
2446
+ if (await gracedStillServable(inval, consistency, graced.hit.envelope, config.defaults?.onUnavailable, error)) return {
2447
+ kind: "graced",
2448
+ served: graced,
2449
+ value: graced.hit.value
2450
+ };
2451
+ return {
2452
+ error,
2453
+ kind: "fenced"
2454
+ };
2455
+ }
2456
+ return {
2457
+ error,
2458
+ kind: "retriable-failed"
2459
+ };
2460
+ }
2461
+ throw error;
2462
+ }
2463
+ /**
2464
+ * Fill: run the factory, fence the write-back, then born-monotone
2465
+ * write-through to L2. Returns `{kind:'filled', value}` on success,
2466
+ * `{kind:'fenced'}` when the fill was fenced by a hard purge visible at write-back
2467
+ * (never store or serve a fenced value), `{kind:'skipped'}` on
2468
+ * `ctx.skip()` (M10/invariant 24), or `{kind:'graced', value}` when the
2469
+ * factory threw a retriable error and `graced` — the currently-known
2470
+ * stale-but-servable `val` — is STILL servable once re-validated at serve time.
2471
+ *`bornEpoch` is captured by the caller at FILL START so a
2472
+ * purge that bumps the epoch after the fill began cannot retroactively make the
2473
+ * born value look fresh. Error-path handling is `classifyFillError` above.
2474
+ */
2475
+ async function fillAndStore(config, options, key, inval, consistency, bornEpoch, graced, ambient) {
2476
+ const codec = codecOf(config);
2477
+ const { l2 } = config;
2478
+ if (!l2) return notImplemented("cache.getOrSet (no l2 configured)", config, options);
2479
+ const declaredUserTags = sortedUserTags(options.tags);
2480
+ const declaredTags = envelopeTags(declaredUserTags, key, config.namespace);
2481
+ const accum = makeTagAccumulator(declaredUserTags);
2482
+ const resolvedScope = resolveScope(options);
2483
+ const scope = resolvedScope.literal;
2484
+ const guard = buildRequestGuard(config, options);
2485
+ const lockHandle = await maybeLock(config, options);
2486
+ const ctx = {
2487
+ dependsOn(tag, dependsOptions) {
2488
+ if (dependsOptions?.l3 === false) notImplemented("FactoryCtx.dependsOn({ l3: false }) — deferred to v0.5", config, options);
2489
+ if (accum.isSettled()) throw new FactorySettledError(key);
2490
+ accum.addMembership(tag);
2491
+ recordDependency(ambient, {
2492
+ scope: fillScopeEvidence(scope),
2493
+ tags: [tag],
2494
+ ...dependsOptions?.l3 === void 0 ? {} : { l3: dependsOptions.l3 }
2495
+ });
2496
+ },
2497
+ fail(err) {
2498
+ if (err === void 0) throw new Error("factory failed (non-retriable)");
2499
+ if (err instanceof Error) throw err;
2500
+ throw new Error("factory failed (non-retriable)", { cause: err });
2501
+ },
2502
+ /** Deferred to v0.4 — explicit grace-reuse from within a fill is not part of this phase. */
2503
+ reuseGraced() {
2504
+ return notImplemented("FactoryCtx.reuseGraced (deferred to v0.4)", config, options);
2505
+ },
2506
+ ...guard.request === void 0 ? {} : { request: guard.request },
2507
+ setTags(tags) {
2508
+ if (accum.isSettled()) throw new FactorySettledError(key);
2509
+ accum.author(tags);
2510
+ },
2511
+ /** Deferred to v0.4 — the TTL-override (what it overrides, how it meets grace and
2512
+ * the duration grammar) is a v0.4 design slot, not a silent no-op (spec-surface sweep). */
2513
+ setTtl() {
2514
+ notImplemented("FactoryCtx.setTtl (deferred to v0.4)", config, options);
2515
+ },
2516
+ signal: new AbortController().signal,
2517
+ skip() {
2518
+ throw new SkipSignalError();
2519
+ }
2520
+ };
2521
+ try {
2522
+ let value;
2523
+ try {
2524
+ value = await options.factory(ctx);
2525
+ } catch (error) {
2526
+ accum.markSettled();
2527
+ return await classifyFillError(error, config, key, declaredTags, scope, bornEpoch, inval, consistency, options, graced);
2528
+ }
2529
+ accum.markSettled();
2530
+ const userTags = accum.settleUserTags();
2531
+ validateUserTags(userTags);
2532
+ const tags = envelopeTags(userTags, key, config.namespace);
2533
+ if (inval && tags.length > 0 && !inval.fenceWriteBack(tags, bornEpoch)) return { kind: "fenced" };
2534
+ const bornMs = config.clock.now();
2535
+ const encoded = codec.encode(value);
2536
+ const fence = lockHandle?.fence;
2537
+ const envelope = {
2538
+ bornEpoch,
2539
+ bornMs,
2540
+ enc: "json",
2541
+ grace: 0,
2542
+ key,
2543
+ kind: "val",
2544
+ scope,
2545
+ size: encoded.length,
2546
+ src: "factory",
2547
+ storedAt: bornMs,
2548
+ tags,
2549
+ ttl: 0,
2550
+ v: 3,
2551
+ val: value,
2552
+ codecId: codec.id,
2553
+ ...fence === void 0 ? {} : { fence }
2554
+ };
2555
+ const written = resolvedScope.l1Only || guard.wasTouched() ? {
2556
+ durable: false,
2557
+ writeSuppressed: false
2558
+ } : await bornMonotoneWrite(config, l2, key, encoded, envelope, inval, consistency);
2559
+ return {
2560
+ bornEpoch,
2561
+ bornMs,
2562
+ durable: written.durable,
2563
+ kind: "filled",
2564
+ storedKind: "val",
2565
+ tags,
2566
+ value,
2567
+ writeSuppressed: written.writeSuppressed
2568
+ };
2569
+ } finally {
2570
+ await lockHandle?.release();
2571
+ }
2572
+ }
2573
+ /** Write-back attempt budget: fence-taught retries. A fill fenced by a
2574
+ * hard purge that landed mid-fill re-mints its `bornEpoch` from the now-advanced verified
2575
+ * high-water and refetches, up to this many attempts before failing typed. */
2576
+ const WRITE_BACK_ATTEMPT_BUDGET = 3;
2577
+ /**
2578
+ * `fillAndStore` fronted by the CHECK-THEN-FETCH strong-miss reorder
2579
+ * and the fence-taught retry. On a
2580
+ * `strong`, registry-backed fill the live un-memoized registry check
2581
+ * (`inval.liveCheckForFill`) runs over the fill's tags BEFORE the factory; the fill's
2582
+ * `bornEpoch` is raised to the now-current verified high-water, so the born value
2583
+ * postdates every purge that check observed — hard OR soft. A strong read
2584
+ * therefore never serves a stale-born or hard-dead value, and two strong reads of an
2585
+ * unmutated key agree.
2586
+ *
2587
+ * If the LOCAL write-back fence (`fenceWriteBack`) fences the fill — a hard purge
2588
+ * landed DURING the factory and was delivered/verified into local knowledge — the fill
2589
+ * RE-MINTS `bornEpoch` from the now-advanced verified high-water (`localBornEpoch`,
2590
+ * which a resync or this call's own live check has caught up between attempts, per
2591
+ * `#recordChecked`) and REFETCHES: the honest post-acceptance fetch the purge demanded,
2592
+ * which borns past it and passes the fence ( observations commission later
2593
+ * fetches, they never bless the earlier one). Bounded by the attempt budget; an
2594
+ * attempt whose re-mint cannot advance the floor gives up (the fence is not from an
2595
+ * observable purge). Exhaustion returns `fenced` — `getOrSet` throws `FencedError`,
2596
+ * `getOrSetEntry` misses. `eventual`/registry-absent/tags-empty fills skip
2597
+ * the pre-fetch check but share the retry.
2598
+ */
2599
+ async function fillAndStoreChecked(config, options, key, inval, consistency, bornEpoch, graced, ambient) {
2600
+ const tags = inval ? envelopeTags(sortedUserTags(options.tags), key, config.namespace) : [];
2601
+ let bornEpochForFill = bornEpoch;
2602
+ if (consistency === "strong" && inval && tags.length > 0) {
2603
+ const checkedThrough = await inval.liveCheckForFill(tags, config.defaults?.onUnavailable);
2604
+ bornEpochForFill = Math.max(bornEpochForFill, checkedThrough);
2605
+ }
2606
+ for (let attempt = 0; attempt < WRITE_BACK_ATTEMPT_BUDGET; attempt++) {
2607
+ const result = await fillAndStore(config, options, key, inval, consistency, bornEpochForFill, graced, ambient);
2608
+ if (result.kind !== "fenced" || !inval || tags.length === 0) return result;
2609
+ const reMinted = Math.max(bornEpochForFill, inval.localBornEpoch());
2610
+ if (reMinted === bornEpochForFill) return result;
2611
+ bornEpochForFill = reMinted;
2612
+ }
2613
+ return { kind: "fenced" };
2614
+ }
2615
+ /** A fresh, empty singleflight registry for one isolate. */
2616
+ function newSingleflight() {
2617
+ return {
2618
+ byIdentity: /* @__PURE__ */ new Map(),
2619
+ byKey: /* @__PURE__ */ new Map()
2620
+ };
2621
+ }
2622
+ /**
2623
+ * The singleflight JOIN IDENTITY ( strict option-compatibility, step
2624
+ * the canonical key plus every structurally-relevant option a
2625
+ * waiter would otherwise silently miss. Two concurrent calls coalesce onto ONE
2626
+ * factory invocation iff their join identities are byte-identical strings.
2627
+ *
2628
+ * - `key`: the already-canonicalized (ns-versioned, scoped) key — two calls
2629
+ * for different keys must never coalesce.
2630
+ * - `consistency`: a `strong` request never joins a non-strong (`eventual`)
2631
+ * fill — an eventual waiter MAY join a strong fill (strictly safer), so the
2632
+ * identity is keyed on the REQUESTING call's own consistency, not some
2633
+ * merged value; a `strong` caller's identity differs from an `eventual`
2634
+ * caller's, so they never hash equal and each strong call always runs its
2635
+ * own fill, while two `eventual` calls (or two `strong` calls) still share.
2636
+ * - `apiForm`: `getOrSet` vs `getOrSetEntry` — a plain-`T` waiter must
2637
+ * never join an entry-form fill's ctx (skip-capability differs, M10).
2638
+ * - structural options (GPT note): tags (sorted+deduped, so order never
2639
+ * matters), ttl, grace, scope, notFoundTtl, codec id — a mismatch here means
2640
+ * a waiter could receive an envelope missing its own declared dependency
2641
+ * tags, the exact stringly-typed no-op class this library exists to kill.
2642
+ *
2643
+ * `dev` mismatch handling (throw vs a separate fill + telemetry event) belongs
2644
+ * to a later cluster; this cluster only builds the identity and coalesces
2645
+ * exact matches — a mismatch here simply produces a different map key, so it
2646
+ * already runs its own separate fill (the safe default) without yet emitting
2647
+ * `singleflight_option_mismatch` or dev-throwing.
2648
+ */
2649
+ function joinIdentity(key, consistency, apiForm, codec, options) {
2650
+ const userTags = sortedUserTags(options.tags);
2651
+ return JSON.stringify({
2652
+ apiForm,
2653
+ codecId: codec.id,
2654
+ consistency,
2655
+ grace: options.grace ?? null,
2656
+ key,
2657
+ notFoundTtl: options.notFoundTtl ?? null,
2658
+ scope: scopeLiteral(options),
2659
+ tags: userTags,
2660
+ ttl: options.ttl ?? null
2661
+ });
2662
+ }
2663
+ /** A structural options mismatch between concurrent same-key fills.
2664
+ * The message points at the fix (declare compatible options); dev throws it. */
2665
+ var SingleflightOptionMismatchError = class extends Error {
2666
+ constructor(userKey) {
2667
+ super(`singleflight option mismatch for key ${JSON.stringify(userKey)}: concurrent calls declared incompatible options (tags/ttl/grace/scope/notFoundTtl/codec). Make the options identical, or serialize the calls.`);
2668
+ this.name = "SingleflightOptionMismatchError";
2669
+ }
2670
+ };
2671
+ /**
2672
+ * Detect + handle a singleflight OPTION MISMATCH: another fill for
2673
+ * the SAME canonical key is already in flight under a DIFFERENT join identity.
2674
+ * `dev:true` throws (a caller bug surfaced loudly); prod runs a SEPARATE fill and
2675
+ * emits `singleflight_option_mismatch` — carrying the colliding key + the
2676
+ * declared tags (hosted sinks salt every field, INV-15). A matching-identity
2677
+ * joiner never reaches here (it joined in `singleflight` before this ran).
2678
+ */
2679
+ function handleOptionMismatch(map, key, identity, config, options) {
2680
+ const identities = map.byKey.get(key);
2681
+ if (identities === void 0) return;
2682
+ let incumbent;
2683
+ for (const [inFlight, record] of identities) if (inFlight !== identity) {
2684
+ incumbent = record;
2685
+ break;
2686
+ }
2687
+ if (incumbent === void 0) return;
2688
+ if (config.dev === true) throw new SingleflightOptionMismatchError(options.key);
2689
+ emitTelemetry(config, {
2690
+ key,
2691
+ tags: incumbent.envelopeTags,
2692
+ type: "singleflight_option_mismatch"
2693
+ });
2694
+ }
2695
+ /** Track this identity as in flight for its canonical key (mismatch detection),
2696
+ * remembering its full stored tag set for a later mismatch event's tag ref. */
2697
+ function trackKeyIdentity(map, key, identity, fillEnvelopeTags) {
2698
+ const identities = map.byKey.get(key) ?? /* @__PURE__ */ new Map();
2699
+ const existing = identities.get(identity);
2700
+ if (existing) existing.count += 1;
2701
+ else identities.set(identity, {
2702
+ count: 1,
2703
+ envelopeTags: fillEnvelopeTags
2704
+ });
2705
+ map.byKey.set(key, identities);
2706
+ }
2707
+ /** Untrack this identity once its fill settles (mismatch detection cleanup). */
2708
+ function untrackKeyIdentity(map, key, identity) {
2709
+ const identities = map.byKey.get(key);
2710
+ if (identities === void 0) return;
2711
+ const record = identities.get(identity);
2712
+ if (record !== void 0) {
2713
+ record.count -= 1;
2714
+ if (record.count <= 0) identities.delete(identity);
2715
+ }
2716
+ if (identities.size === 0) map.byKey.delete(key);
2717
+ }
2718
+ /**
2719
+ * Run `fill` under in-isolate singleflight (invariant 5): if a fill with
2720
+ * the SAME join identity is already in flight, join it instead of starting a
2721
+ * second factory invocation — both callers resolve to the one result. Otherwise,
2722
+ * a concurrent fill for the same KEY under a DIFFERENT identity is an OPTION
2723
+ * MISMATCH: dev throws, prod runs a separate fill + emits the event
2724
+ * (`handleOptionMismatch`). Then register this call's fill and run it.
2725
+ *
2726
+ * CRITICAL — coalescing under `Promise.all` / same-tick concurrent callers
2727
+ * (not just `fc.scheduler` interleavings): the in-flight promise is registered
2728
+ * into the map SYNCHRONOUSLY, in the same microtask turn as the identity
2729
+ * lookup, with NO `await` between the `byIdentity.get` check and the
2730
+ * registration. `fill()` itself is only invoked (and starts doing real async
2731
+ * work) after registration. Two callers racing through `Promise.all` each
2732
+ * invoke `singleflight` synchronously up to this point; the first to run
2733
+ * (JS's single-threaded execution order gives the two `Promise.all` callbacks
2734
+ * a well-defined sequential dispatch — there is no `await` before either call
2735
+ * reaches here) wins the identity check and registers, so a same-identity
2736
+ * second call synchronously observes the first's entry and joins it, while a
2737
+ * different-identity second call synchronously observes the mismatch. The entry
2738
+ * is removed in a `finally` once the fill settles (success or failure) — a
2739
+ * subsequent, non-concurrent call always runs its own fresh fill.
2740
+ */
2741
+ async function singleflight(map, key, identity, config, options, fill) {
2742
+ const existing = map.byIdentity.get(identity);
2743
+ if (existing) return await existing;
2744
+ handleOptionMismatch(map, key, identity, config, options);
2745
+ trackKeyIdentity(map, key, identity, envelopeTags(sortedUserTags(options.tags), key, config.namespace));
2746
+ const registration = (async () => {
2747
+ try {
2748
+ return await fill();
2749
+ } finally {
2750
+ map.byIdentity.delete(identity);
2751
+ untrackKeyIdentity(map, key, identity);
2752
+ }
2753
+ })();
2754
+ map.byIdentity.set(identity, registration);
2755
+ return await registration;
2756
+ }
2757
+ /**
2758
+ * Open the mint for one read, snapshotting the `bornEpoch` floor SYNCHRONOUSLY — the writer's
2759
+ * applied registry epoch at fill start. Taking it at OPEN is the fence-critical part:
2760
+ * a purge that lands via a concurrent bus frame after this call cannot lift the floor, so a fill
2761
+ * that BEGAN before a concurrent purge still fences correctly at write-back (INV-9/INV-12).
2762
+ * Registry-absent ⇒ 0.
2763
+ */
2764
+ function openBornMint(inval) {
2765
+ const floor = inval ? inval.localBornEpoch() : 0;
2766
+ return { stamp: (checkedThrough) => Math.max(floor, checkedThrough) };
2767
+ }
2768
+ /**
2769
+ * What a SERVE can prove about the served value's durability. A serve
2770
+ * reports only what ITS OWN evidence supports:
2771
+ *
2772
+ * - found in L2 ⇒ `true`. The read itself observed the value in the shared tier, so the claim is
2773
+ * backed by something this turn actually did.
2774
+ * - the STORED scope resolves L1-only (a `usr:` principal) ⇒ `false`. Derived from the identity
2775
+ * the entry was stored under, not from this turn: such an entry is never published to L2, so
2776
+ * the kernel KNOWS it is not durable.
2777
+ * - otherwise ⇒ ABSENT. A `pub`/`ten:` entry found in L1, or a scope the surface did not return
2778
+ * readably: the L1 envelope carries no write-back record, and the serve algorithm does
2779
+ * not consult L2 on an L1 hit, and adding a read to populate this field would make an L1 hit
2780
+ * cost what the driver contract forbids. The write-through outcome simply cannot be settled here, and
2781
+ * an unsettled question is reported as such rather than guessed in either direction.
2782
+ *
2783
+ * Returned as a spread FRAGMENT so absence is STRUCTURAL — the property simply not present, never
2784
+ * a sentinel and never an explicit `undefined` (`exactOptionalPropertyTypes` keeps the difference
2785
+ * observable to a consumer that compares against `false`).
2786
+ */
2787
+ function serveDurability(tier, hit) {
2788
+ if (tier === "l2") return { durable: true };
2789
+ const evidence = hitScopeEvidence(hit);
2790
+ if (evidence.kind === "readable" && storedScopeIsL1Only(evidence.literal)) return { durable: false };
2791
+ return {};
2792
+ }
2793
+ /**
2794
+ * The error-path grace serve: a stored candidate that re-validated as
2795
+ * still-servable is served `stale:true` after the fill failed. `graced` (a retriable failure)
2796
+ * carries `servedOnError`; `neg-suppressed` (a 404, which is not `staleIfError`) does not.
2797
+ *
2798
+ * Every field here reports the CANDIDATE — the entry actually being served — rather than the fill
2799
+ * that failed: its tier is the tier it was read from, its durability what that tier can prove,
2800
+ *its age its own stamp. The earlier shape named `tier:'l2'` and `durable:true` as
2801
+ * constants, for a value that may have been found in L1 and never written to a shared tier at all.
2802
+ */
2803
+ function gracedServe(attempt, ambient) {
2804
+ const { served } = attempt;
2805
+ recordServedHit(ambient, served.hit);
2806
+ return {
2807
+ ...serveDurability(served.tier, served.hit),
2808
+ bornMs: served.hit.envelope.bornMs,
2809
+ skipped: false,
2810
+ stale: true,
2811
+ tier: served.tier,
2812
+ value: attempt.value,
2813
+ ...attempt.kind === "graced" ? { servedOnError: true } : {}
2814
+ };
2815
+ }
2816
+ /**
2817
+ * One caller's error-path tail over a SHARED retriable failure: the fill that
2818
+ * failed produced no serve of its own (a blind leader), so it handed out the classified error as a
2819
+ * FACT. This runs on behalf of a caller against ITS OWN read — a singleflight joiner that read a
2820
+ * copy of its own, or the blind leader itself. Each state resolves to exactly what a SOLO caller in
2821
+ * that same state resolves to, because a caller's outcome must not depend on which caller happened
2822
+ * to win singleflight:
2823
+ *
2824
+ * - this caller read a `val` candidate that STILL re-validates as servable now ( a
2825
+ * hard delete during the fill must not resurrect it): serve it `{stale:true, servedOnError}`
2826
+ * with THIS candidate's own evidence (tier/durable/age), exactly as a leader's own
2827
+ * graced serve would. `staleCandidate` is already a `ServedCandidate`, so the evidence-riding
2828
+ * machinery is reused unchanged.
2829
+ * - this caller read a candidate that re-validates DEAD: it REFUSES the serve and takes the
2830
+ * classified failure — returned as the FENCED OUTCOME, not thrown, so the wrapper applies the
2831
+ * same per-form mapping the solo path gets for this state (the plain form rethrows the typed
2832
+ * error, the entry form resolves a miss). Throwing here would make an entry-form joiner reject
2833
+ * where an identical solo call resolves a miss.
2834
+ * - this caller read no candidate at all: the classified error is rethrown, exactly as a solo
2835
+ * caller with nothing to fall back on rethrows it.
2836
+ *
2837
+ * A joiner's recourse is thus judged against its OWN read, never the leader's disposition — the
2838
+ * leader's blindness is a fact it shares, not a verdict it imposes.
2839
+ */
2840
+ async function runOwnErrorTail(config, options, inval, consistency, staleCandidate, error, ambient) {
2841
+ if (options.grace !== void 0 && staleCandidate !== void 0) {
2842
+ if (await gracedStillServable(inval, consistency, staleCandidate.hit.envelope, config.defaults?.onUnavailable, error)) return gracedServe({
2843
+ kind: "graced",
2844
+ served: staleCandidate,
2845
+ value: staleCandidate.hit.value
2846
+ }, ambient);
2847
+ return {
2848
+ durable: false,
2849
+ error,
2850
+ skipped: false,
2851
+ stale: false,
2852
+ tier: "miss"
2853
+ };
2854
+ }
2855
+ throw error;
2856
+ }
2857
+ /** The read tiers in order: L1 then L2. Absent tiers are skipped. */
2858
+ function tiersOf(config) {
2859
+ const tiers = [];
2860
+ if (config.l1) tiers.push([config.l1, "l1"]);
2861
+ if (config.l2) tiers.push([config.l2, "l2"]);
2862
+ return tiers;
2863
+ }
2864
+ /** Write the freshly-filled value into L1 (write-through) — best-effort. The whole fill
2865
+ * outcome rides in, because every field written here belongs to that ONE fill: its `bornEpoch` so
2866
+ * both tiers validate identically ( `fallbackBornEpoch` covers the neg-write outcome, which
2867
+ * stamps none), its `bornMs` so both tiers report the same age ( birth is an event, and a value
2868
+ * has ONE: the stamp is minted at fill and carried here, never re-read off the clock, which would
2869
+ * date the L1 copy to the moment the L2 write finished and make one value two ages), and its STORED
2870
+ * tag set (settle-union) so the two tiers are tag-coherent: a later L1 hit replays the
2871
+ * same purge handles L2 carries.
2872
+ *
2873
+ * A `kind:'neg'` fill whose write LANDED stores nothing here and EVICTS whatever L1 still holds for
2874
+ * the key — two halves of one rule, and neither is sufficient alone. A neg whose write the guard
2875
+ * SUPPRESSED does neither: eviction follows the write's fate (see below).
2876
+ *
2877
+ * It stores nothing because the neg's storage is `writeNegEnvelope`'s single L2 write
2878
+ * — that function is the neg's whole write path, and it deliberately writes one tier. What
2879
+ * this write-through would put in L1 is a `kind:'val'`-stamped copy of the neg's placeholder body:
2880
+ * an envelope claiming to be a VALUE, holding a value no origin produced. Read back, it would
2881
+ * surface that placeholder (the read path's neg rule keys on the stored `kind`, which this write
2882
+ * misstates) and, worse, stand as a grace/stale-if-error candidate — precisely what invariant 7 forbids a
2883
+ * neg from ever being. Today the default codec cannot encode `undefined` to a decodable body, so
2884
+ * such a write lands as an unreadable entry that always misses; that is an accident of one codec,
2885
+ * not a guarantee any other owes us.
2886
+ *
2887
+ * It evicts because L1 may ALREADY hold an OLDER value for this key. A fill runs when the read
2888
+ * found nothing servable — and a tier read that a classified transient failure suppressed is
2889
+ * a plain MISS over an entry that is still sitting there. Refusing the write alone leaves that
2890
+ * entry standing, and it outlives the 404: once the tier reads cleanly again L1 is consulted first
2891
+ * (steps 2/4) and answers with the superseded value, in preference to the neg L2 now holds —
2892
+ * the origin's not-found reverted by a tier that was merely unreadable for a moment. Deleting ends
2893
+ * the turn with L1 holding nothing for the key, so reads fall through to the tier that remembers
2894
+ * the 404 and a served neg still reports the tier and durability its one write proves.
2895
+ *
2896
+ * Whether a neg SHOULD also LIVE in L1 is a storage-policy question the design does not settle — it would
2897
+ * change that reported tier and durability — so this evicts rather than storing a neg envelope,
2898
+ * closing the mislabel and the resurrection without deciding it.
2899
+ *
2900
+ * EVICTION FOLLOWS THE WRITE'S FATE. Both halves above are premised on a neg that
2901
+ * LANDED: the eviction exists to stop a superseded L1 copy outliving a 404 the durable tier now
2902
+ * remembers. A neg the guard suppressed leaves the durable tier remembering no 404 at all — the
2903
+ * verdict the suppression rendered is that the val stands — so deleting that val's L1 copy would
2904
+ * contradict the verdict on the way out, and cost a refill of a value that is still good. A
2905
+ * suppressed write mutates nothing, in either tier. */
2906
+ async function hydrateL1(config, codec, key, options, attempt, fallbackBornEpoch) {
2907
+ const { l1 } = config;
2908
+ if (!l1) return;
2909
+ if (attempt.storedKind === "neg") {
2910
+ if (attempt.writeSuppressed) return;
2911
+ try {
2912
+ await l1.delete(key);
2913
+ } catch {}
2914
+ return;
2915
+ }
2916
+ const { bornMs, tags, value } = attempt;
2917
+ const bornEpoch = attempt.bornEpoch ?? fallbackBornEpoch;
2918
+ try {
2919
+ await l1.set(key, codec.encode(value), { metadata: {
2920
+ bornEpoch,
2921
+ bornMs,
2922
+ codecId: codec.id,
2923
+ enc: "json",
2924
+ kind: "val",
2925
+ scope: scopeLiteral(options),
2926
+ tags,
2927
+ v: 3
2928
+ } });
2929
+ } catch {}
2930
+ }
2931
+ /**
2932
+ * The successful foreground fill's serve: record the dependency, hydrate L1, and
2933
+ * report the fill's own outcome.
2934
+ *
2935
+ * This is the one serve that can always answer `durable` (leaves it untouched): it performed
2936
+ * the write-back itself and awaited the result, so it reports a real boolean rather than the absent
2937
+ * arm the read-path serves need.
2938
+ */
2939
+ async function filledServe(config, codec, key, options, scope, attempt, bornEpoch, ambient) {
2940
+ const storedTags = attempt.tags;
2941
+ recordDependency(ambient, {
2942
+ scope: fillScopeEvidence(scope),
2943
+ tags: storedTags
2944
+ });
2945
+ const filled = attempt.value;
2946
+ await hydrateL1(config, codec, key, options, attempt, bornEpoch);
2947
+ return {
2948
+ durable: attempt.durable,
2949
+ bornMs: attempt.bornMs,
2950
+ skipped: false,
2951
+ stale: false,
2952
+ tier: "origin",
2953
+ value: filled
2954
+ };
2955
+ }
2956
+ /**
2957
+ * One caller's error-path neg tail over a SHARED `not_found` fact. The fill
2958
+ * read no candidate of its own (a blind leader), so it recorded the fact and handed it out rather
2959
+ * than deciding, from a read that saw nothing, what every waiter should be served. This decides
2960
+ * DISPOSITION ONLY — it records nothing, because the recording already happened once,
2961
+ * inside the fill's window. It runs on behalf of one caller against ITS OWN read (a joiner that read
2962
+ * a copy of its own, or the blind leader itself), so that each caller resolves EXACTLY as a SOLO
2963
+ * caller in that same state resolves — an outcome must not depend on which caller won singleflight:
2964
+ *
2965
+ * - this caller read a `val` candidate that STILL re-validates as servable now: the guard serves
2966
+ * that val `{stale:true}` with THIS candidate's own evidence (tier/durable/age), and — a
2967
+ * 404 not being `staleIfError` — WITHOUT `servedOnError`, exactly as a solo sighted caller does.
2968
+ * The gate matches the solo path's: this arm is offered only when `grace` was declared (the solo
2969
+ * fill receives its `staleCandidate` only then).
2970
+ * - otherwise (this caller read no candidate, or its own copy re-validated dead): it takes the
2971
+ * RECORDED neg as its outcome, served `{tier:'origin'}` with that write's own evidence — the same
2972
+ * `filled` serve a solo blind caller performs over its own neg-write.
2973
+ *
2974
+ * A joiner's disposition is thus judged against its OWN read — its read is the existence proof the
2975
+ * blind leader lacked — while the fact itself was recorded exactly once, by the fill that found it.
2976
+ */
2977
+ async function runOwnNotFoundTail(config, options, inval, consistency, key, staleCandidate, fact, outerBornEpoch, ambient) {
2978
+ if (options.grace !== void 0 && staleCandidate !== void 0 && await gracedStillServable(inval, consistency, staleCandidate.hit.envelope, config.defaults?.onUnavailable, fact.error)) return gracedServe(negSuppressed(config, key, staleCandidate), ambient);
2979
+ return await filledServe(config, codecOf(config), key, options, fact.scope, fact.neg, outerBornEpoch, ambient);
2980
+ }
2981
+ /**
2982
+ * A STALE hit → the SWR path (SWR). The OBSERVABLE contract, independent of `grace`:
2983
+ * **this response always serves the stale value with `stale:true`** — never the
2984
+ * refreshed/fresh result. A refresh runs synchronously (the full background /
2985
+ * `waitUntil` / REFRESH_QUEUE machinery of step 6 stays deferred; a synchronous
2986
+ * refresh is a placeholder that produces the same observable serve), and its result
2987
+ * is written through for the NEXT read only. It never changes the value THIS call
2988
+ * returns, and its failure never propagates to the caller — SWR's promise is a fast
2989
+ * stale response.
2990
+ *
2991
+ * `servedOnError:true` iff the refresh failed RETRIABLY (per `staleIfError`)
2992
+ * within a grace window (`grace` declared on the call) AND the stale copy is still
2993
+ * servable after serve-time re-validation — the `graced` outcome. A
2994
+ * successful refresh, a fenced/non-servable refresh, a suppressed 404 ( a neg
2995
+ * never displaces a servable val, and a 404 is not `staleIfError`), and a
2996
+ * non-retriable refresh failure all serve the stale value WITHOUT `servedOnError`.
2997
+ * `grace` only affects whether the error path yields `servedOnError`, never whether
2998
+ * the stale value is served (it always is).
2999
+ */
3000
+ async function refreshOrGrace(config, options, key, inval, consistency, tier, hit, bornEpoch) {
3001
+ const codec = codecOf(config);
3002
+ let graced = false;
3003
+ try {
3004
+ const attempt = await fillAndStore(config, options, key, inval, consistency, bornEpoch, {
3005
+ hit,
3006
+ tier
3007
+ });
3008
+ if (attempt.kind === "filled") await hydrateL1(config, codec, key, options, attempt, bornEpoch);
3009
+ graced = attempt.kind === "graced" && options.grace !== void 0;
3010
+ } catch {}
3011
+ return {
3012
+ ...serveDurability(tier, hit),
3013
+ bornMs: hit.envelope.bornMs,
3014
+ skipped: false,
3015
+ stale: true,
3016
+ tier,
3017
+ value: hit.value,
3018
+ ...graced ? { servedOnError: true } : {}
3019
+ };
3020
+ }
3021
+ /**
3022
+ * The core getOrSet path: L1 → L2 → foreground fill with the fence and
3023
+ * Born-monotone write-back. Serves fresh directly; serves a soft-stale value
3024
+ * with `stale:true`; refills on miss/dead/unknown. Shared by `getOrSet` (plain) and
3025
+ * `getOrSetEntry`.
3026
+ */
3027
+ async function runGetOrSet(config, options, inval, nsVersion, apiForm, inFlight, ambient) {
3028
+ const codec = codecOf(config);
3029
+ const scope = scopeLiteral(options);
3030
+ const key = canonicalKey(nsVersion.current, config.namespace, scope, options.key);
3031
+ const consistency = consistencyOf(config, options);
3032
+ const mint = openBornMint(inval);
3033
+ let checkedThrough = 0;
3034
+ let staleCandidate;
3035
+ for (const [store, tier] of tiersOf(config)) {
3036
+ const read = await readTier(store, codec, config, inval, consistency, key);
3037
+ checkedThrough = Math.max(checkedThrough, read.checkedThrough);
3038
+ if (read.hit) {
3039
+ if (read.hit.stale && consistency !== "strong") {
3040
+ recordServedHit(ambient, read.hit);
3041
+ return await refreshOrGrace(config, options, key, inval, consistency, tier, read.hit, mint.stamp(checkedThrough));
3042
+ }
3043
+ if (!read.hit.stale) {
3044
+ recordServedHit(ambient, read.hit);
3045
+ return {
3046
+ ...serveDurability(tier, read.hit),
3047
+ bornMs: read.hit.envelope.bornMs,
3048
+ skipped: false,
3049
+ stale: false,
3050
+ tier,
3051
+ value: read.hit.value
3052
+ };
3053
+ }
3054
+ }
3055
+ const candidate = read.hit?.stale === true ? read.hit : read.staleCandidate;
3056
+ if (candidate !== void 0) staleCandidate = {
3057
+ hit: candidate,
3058
+ tier
3059
+ };
3060
+ }
3061
+ const bornEpoch = mint.stamp(checkedThrough);
3062
+ const attempt = await singleflight(inFlight, key, joinIdentity(key, consistency, apiForm, codec, options), config, options, async () => await fillAndStoreChecked(config, options, key, inval, consistency, bornEpoch, options.grace === void 0 ? void 0 : staleCandidate, ambient));
3063
+ if (attempt.kind === "filled") return await filledServe(config, codec, key, options, scope, attempt, bornEpoch, ambient);
3064
+ if (attempt.kind === "graced" || attempt.kind === "neg-suppressed") return gracedServe(attempt, ambient);
3065
+ if (attempt.kind === "retriable-failed") return await runOwnErrorTail(config, options, inval, consistency, staleCandidate, attempt.error, ambient);
3066
+ if (attempt.kind === "not-found") return await runOwnNotFoundTail(config, options, inval, consistency, key, staleCandidate, attempt, bornEpoch, ambient);
3067
+ if (attempt.kind === "skipped") return {
3068
+ durable: false,
3069
+ skipped: true,
3070
+ stale: false,
3071
+ tier: "miss"
3072
+ };
3073
+ return {
3074
+ durable: false,
3075
+ skipped: false,
3076
+ stale: false,
3077
+ tier: "miss",
3078
+ ...attempt.error === void 0 ? {} : { error: attempt.error }
3079
+ };
3080
+ }
3081
+ /** Whether a raw regex-captured unit is a known `DurationUnit`. */
3082
+ function isDurationUnit(unit) {
3083
+ return unit !== void 0 && Object.hasOwn(DURATION_UNIT_MS, unit);
3084
+ }
3085
+ /** Convert a `Duration` to milliseconds. The full `Duration` grammar is
3086
+ * deferred; the retention lease and the reader's liveness heartbeat cadence need
3087
+ * only a coarse ms value, so this parses the `<number><unit>` shape. */
3088
+ function durationMs(duration) {
3089
+ const match = /^(?<value>[0-9]+(?:\.[0-9]+)?|\.[0-9]+)(?<unit>ms|s|m|h|d)$/u.exec(duration);
3090
+ const value = Number(match?.groups?.value ?? "0");
3091
+ const rawUnit = match?.groups?.unit;
3092
+ return value * (isDurationUnit(rawUnit) ? DURATION_UNIT_MS[rawUnit] : 1);
3093
+ }
3094
+ /** Build the per-instance invalidation core when a registry + bus are wired.
3095
+ *Registry-absent instances (codec.test.ts) get `undefined` and keep the
3096
+ * always-fresh codec path.
3097
+ *
3098
+ * A registry+bus instance REQUIRES an L2 replication mirror: the reader's only safe way to
3099
+ * clear a bus gap is to resync off the `state`/`delta` mirror there. Constructing
3100
+ * it without one is rejected LOUDLY here (dead-branch ruling) — the reader has no
3101
+ * honest job with no store. This throw is dead today (a registry+bus config is only ever wired
3102
+ * with L2), and it is guarded precisely because that reachability would change silently if L2
3103
+ * became optional: the old handling — clearing suspicion without resyncing — was a fail-OPEN
3104
+ * hole. Fail loud instead of silently reopening it. */
3105
+ function buildInvalidation(config) {
3106
+ const { registry, bus, l2 } = config;
3107
+ if (!registry || !bus) return;
3108
+ if (!l2) throw new Error("createCache: a registry+bus instance requires an l2 store (the §4.2 replication mirror the reader resyncs off — E20)");
3109
+ const emit = (event) => {
3110
+ emitTelemetry(config, event);
3111
+ };
3112
+ const heartbeat = config.defaults?.heartbeatInterval;
3113
+ return new Invalidation(registry, l2, bus, heartbeat === void 0 ? void 0 : durationMs(heartbeat), emit);
3114
+ }
3115
+ /**
3116
+ * A facade → the instance it fronts. THE ONE PLACE an alternate identity for one cache is recorded.
3117
+ *
3118
+ * WHY THIS EXISTS, AND WHY IT IS ONE MECHANISM RATHER THAN A HABIT. Several internal seams hang
3119
+ * per-instance state off a `Cache` through a module-private WeakMap, keyed by the object itself. A
3120
+ * composition that returns a WRAPPER around a `createCache` instance — `createWorkersCache` does, to
3121
+ * hang its freshness carrier on the read path — hands callers an object those maps have never heard
3122
+ * of. The address a seam is keyed by is therefore the identity CALLERS HOLD, and a facade that
3123
+ * re-registers one seam and forgets another does not fail loudly: it produces a seam that silently
3124
+ * answers `undefined` forever. Registering per-seam is the fix that does not generalize — the next
3125
+ * seam, or the next facade, reopens it.
3126
+ *
3127
+ * So identity is aliased ONCE, here, and every seam lookup resolves through `resolveCacheIdentity`
3128
+ * BEFORE keying. A new seam is covered because it resolves like the others; a new facade is covered
3129
+ * because it aliases like the others. Neither has to remember the list.
3130
+ */
3131
+ const identityAliases = /* @__PURE__ */ new WeakMap();
3132
+ /**
3133
+ * The instance `cache` stands for — itself, unless it is a facade over another.
3134
+ *
3135
+ * SAME-PACKAGE internal seam, NOT public API (not re-exported from `index.ts`). Every WeakMap keyed
3136
+ * by a `Cache` resolves through this before keying, so a facade and the instance it fronts address
3137
+ * ONE slot. A cache that is nobody's facade resolves to itself, which is every raw `createCache`
3138
+ * instance and therefore the overwhelmingly common path.
3139
+ *
3140
+ * ONE STEP IS ENOUGH because `aliasCacheIdentity` flattens the chain at registration.
3141
+ */
3142
+ function resolveCacheIdentity(cache) {
3143
+ return identityAliases.get(cache) ?? cache;
3144
+ }
3145
+ /**
3146
+ * Record that `facade` is an alternate identity for `inner` — called ONCE, by whatever
3147
+ * composition returns a wrapper, at construction. SAME-PACKAGE internal seam, NOT public API.
3148
+ *
3149
+ * THE TARGET IS RESOLVED AS IT IS STORED, so a facade over a facade lands on the instance at the
3150
+ * bottom rather than on the middle link. That keeps `resolveCacheIdentity` a single lookup on the
3151
+ * hot path while making a stack of facades correct BY CONSTRUCTION rather than by anyone noticing
3152
+ * that it is a stack — the chain is flattened once, here, where it is built.
3153
+ */
3154
+ function aliasCacheIdentity(facade, inner) {
3155
+ identityAliases.set(facade, resolveCacheIdentity(inner));
3156
+ }
3157
+ /** Cache instance → its replication poller. Module-private internal state: the
3158
+ * poller is attached WITHOUT touching the public `Cache` surface. the request-piggyback driver
3159
+ * (the RR adapter) is the READER of this map — the seam arrived with its consumer, so the map is no
3160
+ * longer write-only and the `sonarjs/no-unused-collection` suppression that carried the named "C4"
3161
+ * expiry is DELETED (its premise is gone; a suppression outliving its premise is a lie). */
3162
+ const replicationPollers = /* @__PURE__ */ new WeakMap();
3163
+ /**
3164
+ * The replication poller attached to `cache`, or `undefined` for a registry-absent instance (which
3165
+ * has no reader to drive). SAME-PACKAGE internal seam, NOT public API: it is deliberately not
3166
+ * re-exported from `index.ts`, and its ONE consumer is the request-piggyback tick driver in the
3167
+ * RR adapter (`src/adapters/react-router/poll-driver.ts`), which owns the staleness gate + the
3168
+ * failure swallow. Kept as narrow as that consumer needs: a `Cache` in, its poller out — no other
3169
+ * kernel internal crosses the boundary.
3170
+ *
3171
+ * Resolves identity first, so a facade over an instance finds that instance's poller. Without that
3172
+ * an RR app on a wrapping factory would lose its replication poll SILENTLY — `tickIfStale` reads a
3173
+ * missing poller as "registry-absent, nothing to drive" and returns.
3174
+ */
3175
+ function replicationPollerOf(cache) {
3176
+ return replicationPollers.get(resolveCacheIdentity(cache));
3177
+ }
3178
+ /** A `RenderCollector` → its internal scope-bearing writer. Module-private: the
3179
+ * public collector interface cannot carry the scope half (a public `dependsOn` has no entry
3180
+ * behind it), so the kernel's automatic hit/fill recording reaches it through this map
3181
+ * instead of through a widened public type or a cast. */
3182
+ const collectorStates = /* @__PURE__ */ new WeakMap();
3183
+ /**
3184
+ * An `L3Emission` → how many dependencies the render had consumed when that emission was
3185
+ * committed — hits, fills, and explicit `dependsOn` declarations. Same
3186
+ * module-private-WeakMap seam as `collectorStates` above, and for a kindred reason: the count is
3187
+ * a fact the ADAPTER's header decision needs and no caller of the public type does, and
3188
+ * `L3Emission` is a spec-verbatim shape — so the count rides the private channel rather
3189
+ * than widening a published surface with a field only this package reads.
3190
+ */
3191
+ const emissionCounts = /* @__PURE__ */ new WeakMap();
3192
+ /** Bank an emission's consumed-dependency count on the seam and return the emission (the ONE
3193
+ * writer of `emissionCounts` — the count is banked with the emission it belongs to, so the two
3194
+ * can never drift apart). */
3195
+ function withManagedDependencies(emission, count) {
3196
+ emissionCounts.set(emission, count);
3197
+ return emission;
3198
+ }
3199
+ /**
3200
+ * How many dependencies the render had consumed when `emission` was committed.
3201
+ * SAME-PACKAGE internal seam, NOT public API: deliberately not re-exported from `index.ts`, and
3202
+ * its ONE consumer is the React-Router middleware's header decision, where ZERO is the adapter's
3203
+ * JURISDICTION FLOOR — a render that consumed no managed dependency gets a safe DEFAULT and
3204
+ * nothing more (`private` filled in only where the app wrote no `Cache-Control`, an app-set
3205
+ * `Cache-Tag` left where it is, nothing overridden). It is what separates that floor from an
3206
+ * eligible render whose dependencies simply emit no tag.
3207
+ *
3208
+ * An emission this kernel never minted reads as 0, i.e. as the floor — the direction in which the
3209
+ * adapter asserts least, which is the safe one for an unrecognized input.
3210
+ */
3211
+ function managedDependenciesOf(emission) {
3212
+ return emissionCounts.get(emission) ?? 0;
3213
+ }
3214
+ /**
3215
+ * A `Cache` → the accessor that yields the AMBIENT render collector for the request in
3216
+ * flight. Same module-private-WeakMap seam as `replicationPollers` above, and the
3217
+ * same reason: the collector must reach the kernel WITHOUT widening the public `Cache`
3218
+ * surface and WITHOUT the kernel importing `node:async_hooks` (the request frame is the
3219
+ * adapter's — a Workers deployment without the ALS compat flag must still be able to import
3220
+ * this module).
3221
+ *
3222
+ * Keyed by RESOLVED identity, like every other instance-keyed seam here: an adapter binds against
3223
+ * the cache the app holds, and the kernel looks up against the instance running the read. Those are
3224
+ * different objects whenever a facade is in play, and this map is where they have to meet — a
3225
+ * privacy gate that reads an empty collector concludes the render consumed nothing and asserts
3226
+ * nothing, which is the loudest possible silence.
3227
+ */
3228
+ const renderCollectors = /* @__PURE__ */ new WeakMap();
3229
+ /**
3230
+ * Bind the adapter's ambient-collector accessor to `cache`. SAME-PACKAGE internal
3231
+ * seam, NOT public API: deliberately not re-exported from `index.ts`, and its ONE consumer is
3232
+ * the React-Router middleware (`src/adapters/react-router/middleware.ts`), which owns the
3233
+ * `AsyncLocalStorage` request frame the accessor reads.
3234
+ *
3235
+ * The consequence is the safety property, not a limitation: a cache used WITHOUT the adapter
3236
+ * has no bound accessor, so the kernel records nothing, no `commitHeaders` ever runs, and no
3237
+ * header is ever emitted. Nothing to leak, by construction — it fails closed.
3238
+ */
3239
+ function bindRenderCollector(cache, accessor) {
3240
+ renderCollectors.set(resolveCacheIdentity(cache), accessor);
3241
+ }
3242
+ /** The ambient collector's internal writer for `cache`, or `undefined` when the adapter bound
3243
+ * none / no request frame is open. */
3244
+ function ambientCollectorOf(cache) {
3245
+ const collector = renderCollectors.get(resolveCacheIdentity(cache))?.();
3246
+ return collector === void 0 ? void 0 : collectorStates.get(collector);
3247
+ }
3248
+ /** Build the tick-driven replication poll driver alongside the reader, under the
3249
+ * SAME condition (registry+bus+L2, already validated by `buildInvalidation`). It is CONSTRUCTED
3250
+ * here — never a knip entry: the construction makes `replication-poller.ts` reachable, so deleting
3251
+ * this wiring reds knip, which is the point (the dead-code gate becomes the wiring's tooth). The
3252
+ * poller runs no timer of its own; C4 drives it (deterministic ticks drive it in tests). The
3253
+ * `maxSyncLag` base cadence defaults to the Workers profile (60s) unless config overrides. */
3254
+ function buildReplicationPoller(config, reader) {
3255
+ const { l2, registry } = config;
3256
+ if (!l2 || !registry) throw new Error("createCache: the replication poller requires the same registry+l2 as the reader (§4.2)");
3257
+ const maxSyncLag = config.defaults?.maxSyncLag;
3258
+ const options = maxSyncLag === void 0 ? {} : { pollBaseMs: durationMs(maxSyncLag) };
3259
+ return new ReplicationPoller(reader, l2, mirrorKeys(registry.regId), config.rng, options);
3260
+ }
3261
+ /**
3262
+ * Register this instance's retention lease — DEFERRED TO FIRST USE, best-effort
3263
+ * and fire-and-forget; a registration failure never blocks anything (the GC horizon extends under
3264
+ * uncertainty).
3265
+ *
3266
+ * IT USED TO FIRE FROM `createCache`, AND THAT IS AN RPC AT CONSTRUCTION. Construction performs no
3267
+ * I/O: at module scope the platform refuses the call outright, and the bare `try{}catch{}` around it
3268
+ * — swallowing by design, which is right for a best-effort lease — meant the refusal was invisible.
3269
+ * A lease that never registered and never said so is the worst of both: the Coordinator's GC horizon
3270
+ * is not extended by an instance it never heard from, and nothing anywhere reports it.
3271
+ *
3272
+ * Returns a function the first read calls. It runs AT MOST ONCE per instance and never blocks: the
3273
+ * lease is a hint to the GC horizon, not a precondition for reading, so making a read wait on it
3274
+ * would trade a correctness-neutral registration for real latency on the request that happened to be
3275
+ * first.
3276
+ */
3277
+ function retentionRegistrar(config) {
3278
+ const { registry } = config;
3279
+ const retention = config.defaults?.maxEntryRetention;
3280
+ if (!registry || retention === void 0) return () => {};
3281
+ let registered = false;
3282
+ return () => {
3283
+ if (registered) return;
3284
+ registered = true;
3285
+ (async () => {
3286
+ try {
3287
+ await registry.registerRetention(durationMs(retention));
3288
+ } catch {}
3289
+ })();
3290
+ };
3291
+ }
3292
+ /** Resolve a purge selector to its registry tag(s). A key selector maps to
3293
+ * the reserved `__k` tag digest of that key's canonical form — the
3294
+ * IDENTICAL digest `expire`/`delete` by tag would never see, since the literal
3295
+ * `__k` form is internal and only `expire({key})`/`delete({key})` may target
3296
+ * it. No `scope` and no request on the key selector ⇒ the same `pub` default
3297
+ * a scope-less, contextless `getOrSet` call stamps, so
3298
+ * `expire({key: options.key})` targets exactly the tag a matching contextless
3299
+ * `getOrSet({key: options.key})` carries. */
3300
+ function selectorTags(selector, config, nsVersion) {
3301
+ if ("tag" in selector) return [selector.tag];
3302
+ return [keyTag(canonicalKey(nsVersion.current, config.namespace, scopeLiteral(), selector.key))];
3303
+ }
3304
+ /** Wrap a registry ack into a `PurgeResult`. `flushed`/`edgePurged`
3305
+ * resolve minimally here — the registry mutation already durably accepted + emitted
3306
+ * the __delta/__state batch (steps 1–3); the KV-acceptance handoff and CDN
3307
+ * drain (step 4) are deferred to a later cluster. */
3308
+ function purgeResult(ack) {
3309
+ return {
3310
+ edgePurged: async () => {
3311
+ await Promise.resolve();
3312
+ },
3313
+ epoch: ack.epoch,
3314
+ flushed: async () => {
3315
+ await Promise.resolve();
3316
+ },
3317
+ matchedHint: "unknown"
3318
+ };
3319
+ }
3320
+ /**
3321
+ * Build a cache instance. Wires the dual-epoch invalidation core when a
3322
+ * registry + bus are present; otherwise runs the registry-absent codec path.
3323
+ * Not-yet-implemented verbs still throw `NotImplementedError`.
3324
+ *
3325
+ * CONSTRUCTION PERFORMS NO I/O. What happens here is bookkeeping over the arguments —
3326
+ * addresses are recorded, components are pointed at each other, and no door is opened. Every
3327
+ * deferred piece (the bus subscription via `Invalidation`'s lazy dial; the retention lease via
3328
+ * `registerRetention` below) waits for the first read that needs it and runs inside that read's
3329
+ * request context. This is what makes one cache per isolate, built at module scope, a legal and
3330
+ * expected shape rather than an isolate that refuses to start.
3331
+ */
3332
+ function createCache(config) {
3333
+ const inval = buildInvalidation(config);
3334
+ const poller = inval === void 0 ? void 0 : buildReplicationPoller(config, inval);
3335
+ /** The lease, armed but NOT fired — the first read pays it, and only the first. */
3336
+ const registerRetention = retentionRegistrar(config);
3337
+ const nsVersion = new NsVersion();
3338
+ const inFlight = newSingleflight();
3339
+ const ambientCell = { lookup() {} };
3340
+ const ambient = () => ambientCell.lookup();
3341
+ const instance = {
3342
+ async clear() {
3343
+ const { registry } = config;
3344
+ if (!registry) return notImplemented("cache.clear (no registry configured)", config);
3345
+ inval?.ensureSubscribed();
3346
+ nsVersion.bump();
3347
+ return purgeResult(await registry.delete([nsTag(config.namespace)]));
3348
+ },
3349
+ collect() {
3350
+ const required = [];
3351
+ let committed = false;
3352
+ let lateTag = false;
3353
+ let managedDependencies = 0;
3354
+ let poisoned = false;
3355
+ let scopeUnreadable = false;
3356
+ const records = [];
3357
+ const state = {
3358
+ get records() {
3359
+ return records;
3360
+ },
3361
+ record(dependency) {
3362
+ records.push(dependency);
3363
+ const { l3, scope, tags } = dependency;
3364
+ managedDependencies += 1;
3365
+ if (scope.kind === "unreadable") {
3366
+ scopeUnreadable = true;
3367
+ poisoned = true;
3368
+ } else if (scope.kind === "readable" && scope.literal !== PUBLIC_SCOPE) poisoned = true;
3369
+ if (l3 === false) return;
3370
+ if (committed) {
3371
+ lateTag = true;
3372
+ return;
3373
+ }
3374
+ required.push(...tags);
3375
+ }
3376
+ };
3377
+ const collector = {
3378
+ commitHeaders(budget) {
3379
+ const cacheTags = required.filter((tag) => !isReservedTag(tag));
3380
+ committed = true;
3381
+ if (poisoned) return withManagedDependencies({
3382
+ cacheTags: [],
3383
+ eligible: false,
3384
+ ineligibleReason: scopeUnreadable ? "scope-unreadable" : "scope"
3385
+ }, managedDependencies);
3386
+ if (lateTag) return withManagedDependencies({
3387
+ cacheTags,
3388
+ eligible: false,
3389
+ ineligibleReason: "late-tag"
3390
+ }, managedDependencies);
3391
+ const maxTags = budget?.maxTags;
3392
+ if (maxTags !== void 0 && cacheTags.length > maxTags) return withManagedDependencies({
3393
+ cacheTags,
3394
+ eligible: false,
3395
+ ineligibleReason: "budget"
3396
+ }, managedDependencies);
3397
+ const maxBytes = budget?.maxBytes;
3398
+ if (maxBytes !== void 0) {
3399
+ if (new TextEncoder().encode(cacheTags.join(",")).length > maxBytes) return withManagedDependencies({
3400
+ cacheTags,
3401
+ eligible: false,
3402
+ ineligibleReason: "budget"
3403
+ }, managedDependencies);
3404
+ }
3405
+ return withManagedDependencies({
3406
+ cacheTags,
3407
+ eligible: true
3408
+ }, managedDependencies);
3409
+ },
3410
+ dependsOn(tag, dependsOptions) {
3411
+ state.record({
3412
+ scope: { kind: "none" },
3413
+ tags: [tag],
3414
+ ...dependsOptions?.l3 === void 0 ? {} : { l3: dependsOptions.l3 }
3415
+ });
3416
+ }
3417
+ };
3418
+ collectorStates.set(collector, state);
3419
+ return collector;
3420
+ },
3421
+ async delete(selector) {
3422
+ const { registry } = config;
3423
+ if (!registry) return notImplemented("cache.delete (no registry configured)", selector);
3424
+ inval?.ensureSubscribed();
3425
+ return purgeResult(await registry.delete(selectorTags(selector, config, nsVersion)));
3426
+ },
3427
+ /** Deferred to v0.4 — namespace-wide hard purge behind the origin-load guard. */
3428
+ async deleteAll(guard) {
3429
+ return notImplemented("cache.deleteAll (deferred to v0.4)", config, guard);
3430
+ },
3431
+ async expire(selector) {
3432
+ const { registry } = config;
3433
+ if (!registry) return notImplemented("cache.expire (no registry configured)", selector);
3434
+ inval?.ensureSubscribed();
3435
+ return purgeResult(await registry.expire(selectorTags(selector, config, nsVersion)));
3436
+ },
3437
+ /** Deferred to v0.4 — namespace-wide soft purge behind the origin-load guard. */
3438
+ async expireAll(guard) {
3439
+ return notImplemented("cache.expireAll (deferred to v0.4)", config, guard);
3440
+ },
3441
+ /**
3442
+ * Witness a key's state — introspection, not testimony: every field is drawn from
3443
+ * an ALREADY-KNOWN source, and nothing here mutates state. The tier is a NON-mutating presence
3444
+ * probe (`store.get`, never `hydrateL1` ); the verdict is a pure classify over the
3445
+ * reader's CURRENT knowledge (no live check, no resync ); the reader state is exposed
3446
+ * via read-only accessors; the request attribution is the bound render collector's
3447
+ * request-scoped records. The key is canonicalized at the DEFAULT (`pub`) scope — the
3448
+ * signature carries no scope, so `explain` introspects the default-scoped entry for that key.
3449
+ */
3450
+ async explain(key) {
3451
+ const codec = codecOf(config);
3452
+ const canonical = canonicalKey(nsVersion.current, config.namespace, scopeLiteral(), key);
3453
+ const reader = {
3454
+ appliedEpoch: inval?.appliedEpoch ?? 0,
3455
+ channel: inval?.channel ?? "never-established",
3456
+ suspect: inval?.suspect ?? false,
3457
+ terminal: poller?.terminal ?? false
3458
+ };
3459
+ const requestDependencies = explainAttribution(ambient);
3460
+ const attribution = requestDependencies === void 0 ? {} : { requestDependencies };
3461
+ let readFailed = false;
3462
+ for (const [store, tier] of tiersOf(config)) {
3463
+ const probed = await probeStoredEntry(store, codec, config, canonical);
3464
+ if (probed.kind !== "found") {
3465
+ readFailed ||= probed.kind === READ_FAILED_KIND;
3466
+ continue;
3467
+ }
3468
+ return {
3469
+ identity: {
3470
+ bornEpoch: probed.envelope.bornEpoch,
3471
+ kind: probed.envelope.kind,
3472
+ scope: probed.scope,
3473
+ tags: probed.envelope.tags,
3474
+ ttl: probed.ttl
3475
+ },
3476
+ key,
3477
+ kind: "present",
3478
+ reader,
3479
+ tier,
3480
+ verdict: inval ? inval.explainVerdict(probed.envelope) : FRESH_VERDICT,
3481
+ ...attribution
3482
+ };
3483
+ }
3484
+ if (readFailed) return {
3485
+ key,
3486
+ kind: READ_FAILED_KIND,
3487
+ reader,
3488
+ ...attribution
3489
+ };
3490
+ return {
3491
+ key,
3492
+ kind: "absent",
3493
+ reader,
3494
+ tier: "miss",
3495
+ ...attribution
3496
+ };
3497
+ },
3498
+ async getOrSet(options) {
3499
+ registerRetention();
3500
+ inval?.ensureSubscribed();
3501
+ const outcome = await runGetOrSet(config, options, inval, nsVersion, "getOrSet", inFlight, ambient);
3502
+ if (outcome.value === void 0 && outcome.tier === "miss") {
3503
+ if (outcome.error !== void 0) throw outcome.error;
3504
+ throw new FencedError(options.key);
3505
+ }
3506
+ return outcome.value;
3507
+ },
3508
+ async getOrSetEntry(options) {
3509
+ registerRetention();
3510
+ inval?.ensureSubscribed();
3511
+ const outcome = await runGetOrSet(config, options, inval, nsVersion, "getOrSetEntry", inFlight, ambient);
3512
+ return {
3513
+ age: outcome.bornMs === void 0 ? 0 : Math.max(0, config.clock.now() - outcome.bornMs),
3514
+ ...outcome.durable === void 0 ? {} : { durable: outcome.durable },
3515
+ skipped: outcome.skipped,
3516
+ stale: outcome.stale,
3517
+ tier: outcome.tier,
3518
+ value: outcome.value,
3519
+ ...outcome.servedOnError === void 0 ? {} : { servedOnError: outcome.servedOnError }
3520
+ };
3521
+ }
3522
+ };
3523
+ ambientCell.lookup = () => ambientCollectorOf(instance);
3524
+ if (poller !== void 0) replicationPollers.set(instance, poller);
3525
+ return instance;
3526
+ }
3527
+ //#endregion
3528
+ export { UnknownTagError as A, FactorySettledError as C, InvalidTagError as D, InvalidDurationError as E, NotImplementedError as O, CacheTimeoutError as S, HttpError as T, t as _, replicationPollerOf as a, isRegId as b, isRetriableHttp as c, memory as d, readFrame as f, globalTag as g, compound as h, managedDependenciesOf as i, RegistryUnavailableError as k, duration as l, safeEmit as m, bindRenderCollector as n, resolveCacheIdentity as o, TELEMETRY_EVENTS as p, createCache as r, httpError as s, aliasCacheIdentity as t, isStoreWriteError as u, DEFAULT_REG_ID as v, FencedError as w, mirrorKeys as x, assertRegId as y };