@astilba/cache 0.0.0-placeholder.0 → 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.
- package/LICENSE +21 -0
- package/README.md +13 -1
- package/dist/adapters/cloudflare/index.d.ts +299 -0
- package/dist/adapters/cloudflare/index.js +1699 -0
- package/dist/adapters/react-router/index.d.ts +92 -0
- package/dist/adapters/react-router/index.js +272 -0
- package/dist/cache-YWn0VZpn.d.ts +930 -0
- package/dist/cache-Z4cJzPmm.js +3528 -0
- package/dist/index.d.ts +131 -0
- package/dist/index.js +2 -0
- package/package.json +91 -4
|
@@ -0,0 +1,930 @@
|
|
|
1
|
+
//#region src/knowledge.d.ts
|
|
2
|
+
/** Public consistency levels. `local` was deleted from the public union (M6). */
|
|
3
|
+
type Consistency = "strong" | "eventual";
|
|
4
|
+
/**
|
|
5
|
+
* Dual watermark epochs for a tag — independent, monotone, max-on-write. A
|
|
6
|
+
* later soft purge can never regress a hard purge (invariant 14).
|
|
7
|
+
*/
|
|
8
|
+
interface Watermark {
|
|
9
|
+
readonly softEpoch: number;
|
|
10
|
+
readonly hardEpoch: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Tri-state tag knowledge. Either `known` (with epoch provenance) or
|
|
14
|
+
* absent = `unknown`. An absent record is `unknown`, never `{0,0}`. `unknown`
|
|
15
|
+
* fails closed: never fresh, never grace (invariant 11) — a gap of unknown
|
|
16
|
+
* hardness may hide a hard purge.
|
|
17
|
+
*/
|
|
18
|
+
type TagKnowledge = {
|
|
19
|
+
readonly known: true;
|
|
20
|
+
readonly softEpoch: number;
|
|
21
|
+
readonly hardEpoch: number;
|
|
22
|
+
readonly throughEpoch: number;
|
|
23
|
+
} | {
|
|
24
|
+
readonly known: false;
|
|
25
|
+
};
|
|
26
|
+
/** The outcome of validating an entry against tag knowledge (table). */
|
|
27
|
+
type Validity = "fresh" | "stale" | "dead" | "unknown";
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region src/telemetry.d.ts
|
|
30
|
+
/**
|
|
31
|
+
* The telemetry event-name CATALOG — the single, kernel-hosted registry of every
|
|
32
|
+
* event `type` this library emits, the same shape as the exported error classes. Each member is an
|
|
33
|
+
* event NAME; the value equals the key so a producer can reference a name as `TELEMETRY_EVENTS.<name>`
|
|
34
|
+
* (a literal type, refactor-safe) while `TelemetryEventName` (below) is the union of them all.
|
|
35
|
+
*
|
|
36
|
+
* The names are grouped by their emitting LAYER (kernel / L1 driver / react-router adapter /
|
|
37
|
+
* cloudflare adapter) — the annotation the charter requires. The machine-checked layer map (which
|
|
38
|
+
* fails to compile if a member is unannotated) lives in the catalog's exhaustiveness tooth in the
|
|
39
|
+
* test lane.
|
|
40
|
+
*
|
|
41
|
+
* GROWTH: adding a name is ADDITIVE and consumer-SAFE (a sink RECEIVES events, so a producer that
|
|
42
|
+
* emits a wider vocabulary hands a consumer a value from a set it already types against — narrowing,
|
|
43
|
+
* not widening, at the receiver) — so a new name is a MINOR-version change. Removing or renaming one
|
|
44
|
+
* is breaking.
|
|
45
|
+
*/
|
|
46
|
+
declare const TELEMETRY_EVENTS: {
|
|
47
|
+
readonly writeback_throttled: "writeback_throttled";
|
|
48
|
+
readonly l2_write_error: "l2_write_error";
|
|
49
|
+
readonly singleflight_option_mismatch: "singleflight_option_mismatch";
|
|
50
|
+
readonly store_read_suppressed: "store_read_suppressed";
|
|
51
|
+
readonly neg_suppressed: "neg_suppressed";
|
|
52
|
+
readonly private_evicted: "private_evicted";
|
|
53
|
+
readonly poll_tick_failed: "poll_tick_failed";
|
|
54
|
+
readonly l3_ineligible: "l3_ineligible";
|
|
55
|
+
readonly regid_divergence: "regid_divergence";
|
|
56
|
+
readonly bus_dial_failed: "bus_dial_failed";
|
|
57
|
+
readonly strong_degraded: "strong_degraded";
|
|
58
|
+
readonly registry_degraded: "registry_degraded";
|
|
59
|
+
readonly state_stale: "state_stale";
|
|
60
|
+
};
|
|
61
|
+
/** The closed vocabulary of telemetry event names. `TelemetryEvent.type` is this
|
|
62
|
+
* union, and `safeEmit` accepts only members of it — an off-catalog emit is a COMPILE error. */
|
|
63
|
+
type TelemetryEventName = (typeof TELEMETRY_EVENTS)[keyof typeof TELEMETRY_EVENTS];
|
|
64
|
+
/**
|
|
65
|
+
* The reason CLASS a `strong_degraded` payload reports — why a strong read could not
|
|
66
|
+
* live-check and served under eventual's posture instead. A CLASS, never the error's own words: the
|
|
67
|
+
* registry's failure text is arbitrary app/platform detail (it can carry a URL, an id, a stack), and
|
|
68
|
+
* this event's whole job is to be safe to point a plain sink at. `registry_unreachable` is the only
|
|
69
|
+
* member today; it is a union rather than a bare literal so a second cause arrives as a widened
|
|
70
|
+
* payload the sink already types against, not as a new event name.
|
|
71
|
+
*/
|
|
72
|
+
type StrongDegradedReason = "registry_unreachable";
|
|
73
|
+
/**
|
|
74
|
+
* A telemetry event. `type` is a CATALOG member; additional fields are
|
|
75
|
+
* event-specific. Hosted sinks pseudonymize all string fields (invariant 15). The `type`
|
|
76
|
+
* narrowing from a bare `string` is consumer-safe by variance: a sink is a RECEIVER, so an app
|
|
77
|
+
* typed against the older `string` still accepts every catalog member.
|
|
78
|
+
*/
|
|
79
|
+
interface TelemetryEvent {
|
|
80
|
+
readonly type: TelemetryEventName;
|
|
81
|
+
readonly [field: string]: unknown;
|
|
82
|
+
}
|
|
83
|
+
/** A telemetry sink (`telemetry`). Receives events as they are emitted. */
|
|
84
|
+
type TelemetrySink = (event: TelemetryEvent) => void;
|
|
85
|
+
/**
|
|
86
|
+
* The hook `safeEmit` calls when it SWALLOWS an app-supplied sink's failure. It is
|
|
87
|
+
* INJECTABLE — passed in at the emit site, never a module global — so "observed exactly once" is
|
|
88
|
+
* testable with no module-latch order-coupling: two instances each carry their own hook and never
|
|
89
|
+
* cross-talk. It receives the swallowed error, and is itself the LAST link in the swallow chain, so
|
|
90
|
+
* if it throws that too is swallowed (safeEmit's never-throws contract is total).
|
|
91
|
+
*/
|
|
92
|
+
type SinkErrorHook = (error: unknown) => void;
|
|
93
|
+
/**
|
|
94
|
+
* Hosted telemetry configuration. When `hosted` is set, the kernel pseudonymizes every
|
|
95
|
+
* emitted string field before delivery — key/tag references become per-project HMAC-salted hashes
|
|
96
|
+
* (never raw), and `detail` becomes error codes + hashed refs. `salt` is the per-project HMAC salt
|
|
97
|
+
* that keeps short or guessable identifiers from being reversed out of the hash. `onSinkError` (see
|
|
98
|
+
* `SinkErrorHook`) is the injectable broken-sink hook `safeEmit` calls on a swallowed sink failure.
|
|
99
|
+
* Invariant 15.
|
|
100
|
+
*/
|
|
101
|
+
interface TelemetryConfig {
|
|
102
|
+
readonly sink: TelemetrySink;
|
|
103
|
+
readonly hosted?: boolean;
|
|
104
|
+
readonly salt?: string;
|
|
105
|
+
readonly onSinkError?: SinkErrorHook;
|
|
106
|
+
}
|
|
107
|
+
//#endregion
|
|
108
|
+
//#region src/drivers.d.ts
|
|
109
|
+
/** Injected logical clock — the ONLY source of `now` in kernel code. */
|
|
110
|
+
interface Clock {
|
|
111
|
+
/** Milliseconds on the logical timeline. Never a wall clock in the correctness path. */
|
|
112
|
+
now(): number;
|
|
113
|
+
}
|
|
114
|
+
/** Injected deterministic RNG — the ONLY source of randomness in kernel code. */
|
|
115
|
+
interface Rng {
|
|
116
|
+
/** A float in [0, 1). Seeded and reproducible. */
|
|
117
|
+
next(): number;
|
|
118
|
+
}
|
|
119
|
+
type StoreMetadata = Readonly<Record<string, unknown>>;
|
|
120
|
+
/** A value read from / written to a KV-like store. */
|
|
121
|
+
interface StoreValue {
|
|
122
|
+
readonly value: string;
|
|
123
|
+
readonly metadata?: StoreMetadata;
|
|
124
|
+
}
|
|
125
|
+
interface StoreWriteOptions {
|
|
126
|
+
/** Physical residency in seconds (KV floors this at 60s). */
|
|
127
|
+
readonly expirationTtl?: number;
|
|
128
|
+
readonly metadata?: StoreMetadata;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Which immutable mirror object a WRITE carries. The executor passes it to
|
|
132
|
+
* `putImmutable` so a host maps kind → physical residency, NEVER by parsing the (coped) key —
|
|
133
|
+
* the fourth-class "key edition" 2b-2-0 cured. The single home for the mirror-object taxonomy;
|
|
134
|
+
* `@astilba/cache/registry` re-exports it.
|
|
135
|
+
*/
|
|
136
|
+
type ImmutableKind = "delta" | "snap";
|
|
137
|
+
/**
|
|
138
|
+
* The mirror-object intent a READ carries — the write taxonomy PLUS the mutable `pointer`
|
|
139
|
+
* (`__state`). `Store.get`'s optional hint, symmetric with `putImmutable`: intent travels as a
|
|
140
|
+
* PARAMETER so a driver maps kind → read-cache policy, never by parsing the key. One vocabulary for
|
|
141
|
+
* mirror-object intent in both directions.
|
|
142
|
+
*/
|
|
143
|
+
type ReadKind = "pointer" | ImmutableKind;
|
|
144
|
+
/**
|
|
145
|
+
* A KV-like tier driver. `memory` (L1, isolate-local) and `cloudflareKV` /
|
|
146
|
+
* `redis` (L2, durable) implement it. The harness `FakeStore` models N replica
|
|
147
|
+
* views with per-key visibility delay, per-key write throttle (1 write/s → 429),
|
|
148
|
+
* TTL, and injectable get/set failures; a single-view `FakeStore` is the
|
|
149
|
+
* strongly-consistent Redis stand-in.
|
|
150
|
+
*/
|
|
151
|
+
interface Store {
|
|
152
|
+
/** `readKind` is an optional read-cache hint: a driver may map it to a
|
|
153
|
+
* per-object read-cache policy (the DO KV `cacheTtl`). Unhinted reads inherit the platform
|
|
154
|
+
* default — for L2 KV that is a 60s edge cache, NOT minimal caching (Appendix C trap), so mirror
|
|
155
|
+
* reads pass the hint and entry reads stay allowed eventual. Fakes ignore or record it. */
|
|
156
|
+
get(key: string, readKind?: ReadKind): Promise<StoreValue | undefined>;
|
|
157
|
+
set(key: string, value: string, options?: StoreWriteOptions): Promise<void>;
|
|
158
|
+
delete(key: string): Promise<void>;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* The structural failure contract a `Store` driver raises to the kernel on a rejected
|
|
162
|
+
* write. It is a plain SHAPE, never a class: the kernel discriminates on
|
|
163
|
+
* `code` alone (`isStoreWriteError`) so a fake's throttle error and a real driver's
|
|
164
|
+
* classified rejection speak ONE contract WITHOUT sharing a class — cross-realm and
|
|
165
|
+
* cross-package `instanceof` identity is exactly the trap that makes a shared error
|
|
166
|
+
* class unshippable. The kernel absorbs a classified write failure into an outcome +
|
|
167
|
+
* telemetry; a caller never sees the raw rejection.
|
|
168
|
+
*
|
|
169
|
+
* - `throttled` — the write hit the tier's per-key write-rate ceiling (KV's
|
|
170
|
+
* 1 write/s/key). Transient: `retryable` is true.
|
|
171
|
+
* - `too_large` — the value exceeds the tier's max value size (KV's 25 MiB
|
|
172
|
+
* ceiling). Typed and rejected, NEVER truncated; not retryable.
|
|
173
|
+
* - `unavailable` — a transient tier failure (the durable store was momentarily
|
|
174
|
+
* unreachable).
|
|
175
|
+
*/
|
|
176
|
+
interface StoreWriteError {
|
|
177
|
+
readonly code: "throttled" | "too_large" | "unavailable";
|
|
178
|
+
readonly retryable: boolean;
|
|
179
|
+
readonly cause?: unknown;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Structural type-guard for `StoreWriteError`. Purely shape-based — it NEVER
|
|
183
|
+
* uses `instanceof`, so a harness fake and a real driver are recognized by the SAME
|
|
184
|
+
* check without a shared class. Total and safe on arbitrary `unknown`: rejects
|
|
185
|
+
* non-objects/null, requires `code` to be exactly one of the three literals and
|
|
186
|
+
* `retryable` to be a boolean.
|
|
187
|
+
*/
|
|
188
|
+
declare function isStoreWriteError(e: unknown): e is StoreWriteError;
|
|
189
|
+
/**
|
|
190
|
+
* Ordering key an atomic compare-and-set must never regress: epoch first,
|
|
191
|
+
* then fence. A born-monotone write-back is arbitrated on exactly this pair (INV-17).
|
|
192
|
+
*/
|
|
193
|
+
interface CasOrder {
|
|
194
|
+
readonly epoch: number;
|
|
195
|
+
readonly fence: number;
|
|
196
|
+
}
|
|
197
|
+
/** A versioned CAS slot: `token` is the optimistic-concurrency version. */
|
|
198
|
+
interface CasRecord {
|
|
199
|
+
readonly token: number;
|
|
200
|
+
readonly order: CasOrder;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* The atomic compare-and-set a CAS-capable durable driver (Redis Lua, DO
|
|
204
|
+
* transactional storage) layers ON TOP OF the base `Store`. An OPTIONAL
|
|
205
|
+
* capability: the base `Store` — and the v0.1 fakes — expose none, so the kernel's
|
|
206
|
+
* floor stays read-compare-write (INV-17) and prefer-CAS consumption is deferred to
|
|
207
|
+
* the first real CAS driver. Declared here so the conformance layer can pin the
|
|
208
|
+
* capability and a real driver can later satisfy the same shape.
|
|
209
|
+
*/
|
|
210
|
+
interface CasStore {
|
|
211
|
+
seed(key: string, order: CasOrder): Promise<void>;
|
|
212
|
+
load(key: string): Promise<CasRecord | undefined>;
|
|
213
|
+
/** Apply `next` iff the stored token still equals `expectedToken`; else a lost race. */
|
|
214
|
+
swap(key: string, expectedToken: number, next: CasOrder): Promise<number | undefined>;
|
|
215
|
+
}
|
|
216
|
+
/** Acknowledgement of a durably-accepted registry mutation. */
|
|
217
|
+
interface RegistryAck {
|
|
218
|
+
/** The Coordinator epoch after applying this mutation. */
|
|
219
|
+
readonly epoch: number;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* The authoritative registry / Coordinator. Holds dual watermark
|
|
223
|
+
* epochs per tag (monotone, max-on-write) and a persisted monotone epoch counter.
|
|
224
|
+
* `check` is the live, un-memoized strong read used by strong reads and the
|
|
225
|
+
* write-back fence. `doRegistry` / `redisRegistry` implement it in phase 2;
|
|
226
|
+
* the harness `FakeRegistry` is the fake.
|
|
227
|
+
*/
|
|
228
|
+
interface Registry {
|
|
229
|
+
/** The registry id scoping this Coordinator's mirror keyspace. The per-isolate
|
|
230
|
+
* reader scopes the keys it resyncs off to this value AND asserts the pointer payload echoes
|
|
231
|
+
* it — a mismatch reads an empty keyspace and fails closed. `doRegistry`/`FakeRegistry`
|
|
232
|
+
* default it to `"default"`; the authoring channel is `configure` (env `REGISTRY_ID`). */
|
|
233
|
+
readonly regId: string;
|
|
234
|
+
/** Live watermark knowledge for these tags. */
|
|
235
|
+
check(tags: readonly string[]): Promise<readonly TagKnowledge[]>;
|
|
236
|
+
/** Soft purge: max-on-write bump of `softEpoch`. */
|
|
237
|
+
expire(tags: readonly string[]): Promise<RegistryAck>;
|
|
238
|
+
/** Hard purge: max-on-write bump of `hardEpoch`. */
|
|
239
|
+
delete(tags: readonly string[]): Promise<RegistryAck>;
|
|
240
|
+
/** Register this instance's retention lease at bind. */
|
|
241
|
+
registerRetention(retentionMs: number): Promise<void>;
|
|
242
|
+
}
|
|
243
|
+
/** One tag's watermark change within a replication frame. */
|
|
244
|
+
interface TagChange {
|
|
245
|
+
readonly tag: string;
|
|
246
|
+
readonly softEpoch?: number;
|
|
247
|
+
readonly hardEpoch?: number;
|
|
248
|
+
}
|
|
249
|
+
/** A bus frame carrying a contiguous epoch range of changes. */
|
|
250
|
+
interface BusFrame {
|
|
251
|
+
readonly fromEpoch: number;
|
|
252
|
+
readonly toEpoch: number;
|
|
253
|
+
readonly changes: readonly TagChange[];
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* One event delivered to a bus subscriber. The transport hands the
|
|
257
|
+
* subscriber one of these discriminated shapes; epoch contiguity is validated by the
|
|
258
|
+
* KERNEL, never asserted by the bus (bus transports, it does not vouch):
|
|
259
|
+
*
|
|
260
|
+
* - `frame`: a replication frame carrying its own `{fromEpoch,toEpoch}` range.
|
|
261
|
+
* - `gap`: a frame was dropped or lost in transit — the subscriber must treat its
|
|
262
|
+
* knowledge as suspect and resync before warm trust resumes. Carries `head`:
|
|
263
|
+
*a lower bound ≥ the `toEpoch` of every frame emitted before the
|
|
264
|
+
* signal, so the loss-revealing event ITSELF declares the head the reader must
|
|
265
|
+
* reach to account for what it missed (detection carries its own remedy).
|
|
266
|
+
* - `reset`: the transport was (re)established — emitted BEFORE any frame on every
|
|
267
|
+
* establishment; it suspends all warm trust until a resync
|
|
268
|
+
* handshake reconciles the subscriber's applied epoch.
|
|
269
|
+
* - `hello`: the live channel declares its current head epoch — emitted right AFTER
|
|
270
|
+
* `reset` on every establishment, before any frame. Convergence
|
|
271
|
+
* anchors to a head DECLARED (`hello`/`gap`/a non-contiguous frame's `toEpoch`),
|
|
272
|
+
* never to pointer freshness; an idle channel's `hello` head equals the applied
|
|
273
|
+
* epoch, so a caught-up subscriber reconverges at once.
|
|
274
|
+
* - `lost`: the transport is DOWN — a dial that did not come up, or an established socket
|
|
275
|
+
* that closed. It is the counterpart of `reset`, and it exists because its
|
|
276
|
+
* absence is unreadable: with only `reset`/`hello` on the wire, a channel that never
|
|
277
|
+
* connected and a healthy channel with nothing to say produce the SAME stream —
|
|
278
|
+
* none. A subscriber cannot distinguish silence-because-quiet from
|
|
279
|
+
* silence-because-dead by waiting longer, so the transport declares it instead.
|
|
280
|
+
* It carries no epoch: a channel that is down declares nothing ABOUT epochs, and a
|
|
281
|
+
* `head` here would be a number invented by the thing that just failed. It does NOT
|
|
282
|
+
* raise suspicion — the suspicion gate owns that posture and a down channel is not a gapped map
|
|
283
|
+
* (a never-established reader is DECLARED degraded, which is a different, honest
|
|
284
|
+
* state); it is tracked and surfaced, and what holds the reader correct in the
|
|
285
|
+
* meantime is the polling driver plus its refusal to guess at a tag it cannot
|
|
286
|
+
* establish.
|
|
287
|
+
*/
|
|
288
|
+
type BusEvent = {
|
|
289
|
+
readonly kind: "frame";
|
|
290
|
+
readonly frame: BusFrame;
|
|
291
|
+
} | {
|
|
292
|
+
readonly kind: "gap";
|
|
293
|
+
readonly head: number;
|
|
294
|
+
} | {
|
|
295
|
+
readonly kind: "reset";
|
|
296
|
+
} | {
|
|
297
|
+
readonly kind: "hello";
|
|
298
|
+
readonly head: number;
|
|
299
|
+
} | {
|
|
300
|
+
readonly kind: "lost";
|
|
301
|
+
};
|
|
302
|
+
interface Subscription {
|
|
303
|
+
close(): void;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Cross-instance invalidation bus. A subscriber consumes a `BusEvent`
|
|
307
|
+
* stream — frames plus the out-of-band `gap`/`reset` signals — and the KERNEL, not
|
|
308
|
+
* the bus, asserts frame contiguity and forces a resync on any gap or reset.
|
|
309
|
+
* `doBus` / `redisBus` implement it in phase 2; the harness `FakeBus` models drop,
|
|
310
|
+
* delay, reorder, and disconnect/reconnect over this same union.
|
|
311
|
+
*/
|
|
312
|
+
interface Bus {
|
|
313
|
+
subscribe(onEvent: (event: BusEvent) => void): Subscription;
|
|
314
|
+
}
|
|
315
|
+
/** CDN purge acceptance modes for `delete`. */
|
|
316
|
+
type CdnMode = "enqueue" | "await" | "block";
|
|
317
|
+
/**
|
|
318
|
+
* CDN / L3 purge driver. Its own persisted drain queue with its own
|
|
319
|
+
* alarm; acceptance surfaces via `edgePurged()`. `cloudflarePurge`
|
|
320
|
+
* implements it in phase 2.
|
|
321
|
+
*/
|
|
322
|
+
interface Cdn {
|
|
323
|
+
purge(tags: readonly string[], mode: CdnMode): Promise<void>;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* A pluggable value codec. `id` is the codec name + wire version — the
|
|
327
|
+
* `Envelope.codecId` source of truth. A decode failure or an `id` mismatch
|
|
328
|
+
* (not in `acceptCodecs`) fails closed to a miss (invariants 8, 27).
|
|
329
|
+
*/
|
|
330
|
+
interface Codec {
|
|
331
|
+
readonly id: string;
|
|
332
|
+
encode(value: unknown): string;
|
|
333
|
+
decode(value: string): unknown;
|
|
334
|
+
}
|
|
335
|
+
/** A held cross-isolate lock carrying a monotonic fencing token. */
|
|
336
|
+
interface LockHandle {
|
|
337
|
+
/**
|
|
338
|
+
* Monotonic fencing token — the tie-breaker used above an equal `bornEpoch`
|
|
339
|
+
* and always above `bornMs`; a locked fill carries it into `Envelope.fence`.
|
|
340
|
+
*/
|
|
341
|
+
readonly fence: number;
|
|
342
|
+
release(): Promise<void>;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Opt-in cross-isolate lock — DO or Redis primary only. Grants mutual
|
|
346
|
+
* exclusion plus a monotonic fencing token; the born-monotone write-back rule
|
|
347
|
+
* is what bounds a stale lease-holder's late write. `doRegistry` /
|
|
348
|
+
* `redis` back it in phase 2; the harness `FakeLock` is the fake.
|
|
349
|
+
*/
|
|
350
|
+
interface Lock {
|
|
351
|
+
acquire(key: string): Promise<LockHandle>;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* L1 driver options. The `clock` is OPTIONAL: a clock-less `memory({ maxEntries,
|
|
355
|
+
* maxBytes })` stays valid (the quickstart form) and compliant with the no-ambient-time rule — the kernel
|
|
356
|
+
* passes no `expirationTtl` to L1 today, so residency is production-inert there. A
|
|
357
|
+
* clock-PRESENT `memory({ clock, … })` honors `expirationTtl` (TTL residency); a set that
|
|
358
|
+
* carries `expirationTtl` with no injected clock fails loud rather than silently ignore it.
|
|
359
|
+
*
|
|
360
|
+
* `telemetry` is the OPTIONAL private-eviction disclosure channel: when a PRIVATE (`usr:`-scoped) entry is
|
|
361
|
+
* evicted from the LRU under memory pressure, a `private_evicted` event is emitted through it
|
|
362
|
+
* (payload: reason class + counts + bytes ONLY, never a key/hash/tag — payload discipline). It
|
|
363
|
+
* is guarded by `safeEmit`, so a throwing/rejecting sink is swallowed and `onSinkError` notified.
|
|
364
|
+
*/
|
|
365
|
+
interface MemoryOptions {
|
|
366
|
+
/** Injected logical clock — the ONLY `now` source; required to honor `expirationTtl`. */
|
|
367
|
+
readonly clock?: Clock;
|
|
368
|
+
readonly maxEntries?: number;
|
|
369
|
+
readonly maxBytes?: number;
|
|
370
|
+
/** Private-eviction disclosure sink. Optional; off by default. */
|
|
371
|
+
readonly telemetry?: TelemetrySink;
|
|
372
|
+
/** Injectable broken-sink hook `safeEmit` calls when it swallows a `telemetry` failure. */
|
|
373
|
+
readonly onSinkError?: SinkErrorHook;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* In-process L1 store — isolate-local, bounded by `maxEntries` / `maxBytes` with LRU
|
|
377
|
+
* eviction on either bound (an insertion-ordered `Map`, re-inserted on touch, so recency —
|
|
378
|
+
* not mere insertion order — picks the victim). Each `memory()` owns a PER-INSTANCE byte
|
|
379
|
+
* accounter (Σ value UTF-8 byte lengths).
|
|
380
|
+
*
|
|
381
|
+
* an optional `telemetry` sink receives a `private_evicted` event when a
|
|
382
|
+
* PRIVATE (`usr:`-scoped) entry is evicted from the LRU under memory pressure — reason class +
|
|
383
|
+
* counts + bytes ONLY, so an operator sees L1 pressure on private data without the event ever
|
|
384
|
+
* naming WHAT was evicted (payload discipline). The `usr:`-priority-protected LRU (evict
|
|
385
|
+
* private entries LAST) is a spec MAY and stays UNBUILT: this discloses whatever the current LRU
|
|
386
|
+
* policy evicts, it does not change the victim. The cross-instance shared/joint reserve (warn
|
|
387
|
+
* on Σ`maxBytes` overflow across isolate L1s) is a separate advisory extra, still DEFERRED.
|
|
388
|
+
*/
|
|
389
|
+
declare function memory(options?: MemoryOptions): Store;
|
|
390
|
+
//#endregion
|
|
391
|
+
//#region src/duration.d.ts
|
|
392
|
+
/** Duration units. `m` = minutes (no month unit). */
|
|
393
|
+
type DurationUnit = "ms" | "s" | "m" | "h" | "d";
|
|
394
|
+
/** A duration literal, e.g. `'5m'`, `'1.5h'`, `'250ms'`. */
|
|
395
|
+
type Duration = `${number}${DurationUnit}`;
|
|
396
|
+
/**
|
|
397
|
+
* Build a `Duration` from computed values — the trust point for
|
|
398
|
+
* programmatically-derived durations (as opposed to a `'5m'` string literal the type
|
|
399
|
+
* system already constrains). Rejects a non-positive or non-finite `value`, and — and,
|
|
400
|
+
* the case a naive impl passes — a `value × unitFactor` product that is not a finite,
|
|
401
|
+
* positive, SAFE integer once multiplied out. So `duration(Number.MAX_SAFE_INTEGER, 'd')`
|
|
402
|
+
* is REJECTED even though the input is a safe integer: the overflow lives in the product.
|
|
403
|
+
* Throws `InvalidDurationError` on any violation; never coerces.
|
|
404
|
+
*/
|
|
405
|
+
declare function duration(value: number, unit: DurationUnit): Duration;
|
|
406
|
+
//#endregion
|
|
407
|
+
//#region src/envelope.d.ts
|
|
408
|
+
/** `val` = a cached value; `neg` = a remembered 404 (hard-expire only). */
|
|
409
|
+
type EnvelopeKind = "val" | "neg";
|
|
410
|
+
/** Wire container of the stored bytes. `codecId` is the codec identity. */
|
|
411
|
+
type EnvelopeEnc = "json" | "json+gz" | "bin";
|
|
412
|
+
/**
|
|
413
|
+
* The stored entry (schema version 3). `bornEpoch` is the invalidation clock;
|
|
414
|
+
* `bornMs` is TTL/grace/GC/age arithmetic ONLY and is never an arbiter.
|
|
415
|
+
*`tags` are sorted, deduped, and include `__k:<h>` and `__ns:<name>`.
|
|
416
|
+
*/
|
|
417
|
+
interface Envelope<T = unknown> {
|
|
418
|
+
readonly v: 3;
|
|
419
|
+
/** `<ns-version>:<ns>:<scope>:<user-key>`. */
|
|
420
|
+
readonly key: string;
|
|
421
|
+
readonly val: T;
|
|
422
|
+
/** Writer's applied registry epoch at FILL START — the invalidation clock. */
|
|
423
|
+
readonly bornEpoch: number;
|
|
424
|
+
/** Wall time at fill start — TTL/grace/GC/age arithmetic only; never an arbiter. */
|
|
425
|
+
readonly bornMs: number;
|
|
426
|
+
/** Observability only. */
|
|
427
|
+
readonly storedAt: number;
|
|
428
|
+
readonly ttl: number;
|
|
429
|
+
readonly grace: number;
|
|
430
|
+
/** Sorted, deduped; includes `__k:<h>` and `__ns:<name>`. */
|
|
431
|
+
readonly tags: readonly string[];
|
|
432
|
+
/** `'pub' | 'usr:<h>' | 'ten:<h>'`. */
|
|
433
|
+
readonly scope: string;
|
|
434
|
+
readonly src: string;
|
|
435
|
+
readonly kind: EnvelopeKind;
|
|
436
|
+
readonly enc: EnvelopeEnc;
|
|
437
|
+
readonly size: number;
|
|
438
|
+
/** Codec name + wire version; decode fails closed on mismatch. */
|
|
439
|
+
readonly codecId: string;
|
|
440
|
+
/** Lock fencing token, carried by locked fills; an arbiter above `bornMs`. */
|
|
441
|
+
readonly fence?: number;
|
|
442
|
+
}
|
|
443
|
+
//#endregion
|
|
444
|
+
//#region src/invalidation.d.ts
|
|
445
|
+
/**
|
|
446
|
+
* The live channel's state, as the reader has observed it. A LOCAL FACT — three states,
|
|
447
|
+
* because two would hide the one that matters:
|
|
448
|
+
*
|
|
449
|
+
* - `never-established` — no `hello` has ever arrived on any socket. The reader has NEVER had a
|
|
450
|
+
* live channel, so it runs degraded and SAYS so: what keeps it correct meanwhile is the poll
|
|
451
|
+
* carrier plus failing closed on any unknown tag, and each of those checks teaches the map
|
|
452
|
+
* something it keeps. This is NOT suspicion — the suspect flag is for a map that
|
|
453
|
+
* knows it MISSED something; a reader that never connected has missed nothing, because it never
|
|
454
|
+
* had a channel to miss anything on. Conflating them
|
|
455
|
+
* would either make every cold boot look gapped or make a dead bus look healthy; it is its own
|
|
456
|
+
* state precisely so it can be neither.
|
|
457
|
+
* - `established` — a `hello` arrived and the socket is up.
|
|
458
|
+
* - `lost` — a socket that HAD established is now down. Distinguished from `never-established`
|
|
459
|
+
* because the two have different remedies and different histories: one has knowledge to resync
|
|
460
|
+
* against a head it once heard, the other has never heard one.
|
|
461
|
+
*/
|
|
462
|
+
type ChannelState = "never-established" | "established" | "lost";
|
|
463
|
+
//#endregion
|
|
464
|
+
//#region src/tags.d.ts
|
|
465
|
+
/**
|
|
466
|
+
* A validated cache tag. Produced ONLY by `t`, `compound`, or `globalTag`; a raw
|
|
467
|
+
* string is never assignable to `Tag`. This nominal brand is how the library kills
|
|
468
|
+
* stringly-typed tags (the failure class it exists to prevent).
|
|
469
|
+
*/
|
|
470
|
+
type Tag = string & {
|
|
471
|
+
readonly "astilba.tag": true;
|
|
472
|
+
};
|
|
473
|
+
/** A positional argument to `compound()` / an interpolation in `t`. */
|
|
474
|
+
type TagPart = string | number;
|
|
475
|
+
/**
|
|
476
|
+
* Template helper for a single tag, e.g. ``t`book:${id}` ``. The tag TRUST
|
|
477
|
+
* BOUNDARY: the interpolations (`values`) are UNTRUSTED user data, so `t` assembles the
|
|
478
|
+
* template, then VALIDATES-and-REJECTS the final string against the full grammar
|
|
479
|
+
* (non-empty, charset, ≤256 UTF-8 bytes, not `__`-prefixed) — throwing `InvalidTagError`
|
|
480
|
+
* on any violation. It never escapes-by-guess: escaping is `compound()`'s separate
|
|
481
|
+
* contract, and silently rewriting an interpolation is exactly how tag injection slips
|
|
482
|
+
* through. On success returns the branded `Tag`.
|
|
483
|
+
*/
|
|
484
|
+
declare function t(strings: TemplateStringsArray, ...values: readonly TagPart[]): Tag;
|
|
485
|
+
/**
|
|
486
|
+
* Injective compound tag over an argument vector, e.g. `compound('book', id,
|
|
487
|
+
* 'locale', locale)`. Escaping; empty vector distinct from
|
|
488
|
+
* `['']`.
|
|
489
|
+
*
|
|
490
|
+
* Every part is escaped so an escaped part can never itself
|
|
491
|
+
* contain an unescaped `|`; joining escaped parts on `|` is therefore an
|
|
492
|
+
* injective join. An explicit leading arity count (itself `|`-terminated)
|
|
493
|
+
* makes the count unambiguous — it is always a decimal-digit run followed by
|
|
494
|
+
* the first `|` — which in turn makes vectors of differing length (including
|
|
495
|
+
* the empty vector vs. `['']`) distinct regardless of what the escaped parts
|
|
496
|
+
* contain.
|
|
497
|
+
*/
|
|
498
|
+
declare function compound(...parts: readonly TagPart[]): Tag;
|
|
499
|
+
/**
|
|
500
|
+
* A globally-scoped tag — its own scope-prefix escape hatch. Deferred to v0.4:
|
|
501
|
+
* its escape-FORM is a wire-compatibility decision reserved for the scope-qualification
|
|
502
|
+
* design, and ships with that consumer rather than standalone here.
|
|
503
|
+
*/
|
|
504
|
+
declare function globalTag(name: string): Tag;
|
|
505
|
+
//#endregion
|
|
506
|
+
//#region src/cache.d.ts
|
|
507
|
+
/**
|
|
508
|
+
* A scope claim. `'public'` is an enforced shared-tier claim; `{tenant}` is
|
|
509
|
+
* tenant scoping. Omitting `scope` is private-by-default (principal-derived,
|
|
510
|
+
* `usr:` ⇒ L1-only).
|
|
511
|
+
*/
|
|
512
|
+
type Scope = "public" | {
|
|
513
|
+
readonly tenant: string;
|
|
514
|
+
};
|
|
515
|
+
/** The tier an entry was served from (`tier`). */
|
|
516
|
+
type Tier = "l1" | "l1.5" | "l2" | "origin" | "miss";
|
|
517
|
+
/** How fail-closed `unknown` resolves (adds `'error'`). */
|
|
518
|
+
type UnknownPolicy = "registry-check" | "miss" | "error";
|
|
519
|
+
/** Envelope provenance exposed to a factory for the graced value. */
|
|
520
|
+
interface GracedInfo {
|
|
521
|
+
readonly value: unknown;
|
|
522
|
+
readonly src: string;
|
|
523
|
+
readonly v: number;
|
|
524
|
+
readonly bornMs: number;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* An adapter-provided request object. In a public-scoped factory dev mode
|
|
528
|
+
* wraps it in a Proxy for the frame, so any property read demotes the entry off the
|
|
529
|
+
* shared tier (closures over raw request state are the boundary invariant 4 names).
|
|
530
|
+
*/
|
|
531
|
+
type RequestContext = Readonly<Record<string, unknown>>;
|
|
532
|
+
/**
|
|
533
|
+
* The factory context for `getOrSet`. Deliberately has NO `skip()` — the
|
|
534
|
+
* plain-`T` API cannot skip (M10; invariant 24). `reuseGraced()` is the typed
|
|
535
|
+
* primitive for reusing stale bytes — never launder `graced.value` (an
|
|
536
|
+
* `unknown`) to `T`.
|
|
537
|
+
*/
|
|
538
|
+
interface FactoryCtx<T> {
|
|
539
|
+
readonly signal: AbortSignal;
|
|
540
|
+
readonly graced?: GracedInfo;
|
|
541
|
+
/** The adapter request object — a guarded accessor under public scope. */
|
|
542
|
+
readonly request?: RequestContext;
|
|
543
|
+
/** Throw a classified error; bare `fail()` is non-retriable. */
|
|
544
|
+
fail(err?: unknown): never;
|
|
545
|
+
/** Contribute a dependency tag; `l3:false` marks it droppable for shared caches. */
|
|
546
|
+
dependsOn(tag: Tag, options?: {
|
|
547
|
+
readonly l3?: boolean;
|
|
548
|
+
}): void;
|
|
549
|
+
setTags(tags: readonly Tag[]): void;
|
|
550
|
+
setTtl(ttl: Duration): void;
|
|
551
|
+
/** Reuse the graced value with an identity + provenance/codec check. */
|
|
552
|
+
reuseGraced(): T;
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* The factory context for `getOrSetEntry`. Adds `skip()` — reachable ONLY
|
|
556
|
+
* through the entry form, with sound `never` typing so it cannot silently resolve
|
|
557
|
+
* `undefined` typed as `T` (M10; invariant 24).
|
|
558
|
+
*/
|
|
559
|
+
interface EntryFactoryCtx<T> extends FactoryCtx<T> {
|
|
560
|
+
skip(): never;
|
|
561
|
+
}
|
|
562
|
+
/** Options common to `getOrSet` and `getOrSetEntry`. */
|
|
563
|
+
interface GetOptions {
|
|
564
|
+
readonly key: string;
|
|
565
|
+
readonly tags?: readonly Tag[];
|
|
566
|
+
/**
|
|
567
|
+
* Declared time-to-live. In v0.2 this is part of the singleflight join identity — two
|
|
568
|
+
* concurrent same-key fills declaring different ttls are an options mismatch — but it is
|
|
569
|
+
* NOT stored with the entry and does not drive expiry: validity is decided by epoch comparison,
|
|
570
|
+
* never a wall clock, and the write path passes no residency hint to the stores. So an
|
|
571
|
+
* entry outlives its declared ttl until a purge or store eviction removes it. Wiring ttl to
|
|
572
|
+
* residency and to stored identity is the v0.4 ttl design slot, beside `FactoryCtx.setTtl`.
|
|
573
|
+
*/
|
|
574
|
+
readonly ttl?: Duration;
|
|
575
|
+
readonly grace?: Duration;
|
|
576
|
+
readonly notFoundTtl?: Duration;
|
|
577
|
+
readonly scope?: Scope;
|
|
578
|
+
/**
|
|
579
|
+
* Read consistency. Part of the singleflight join identity: a `strong`
|
|
580
|
+
* request never joins a non-strong fill.
|
|
581
|
+
*/
|
|
582
|
+
readonly consistency?: Consistency;
|
|
583
|
+
/** Opt in to a cross-isolate lock; the fill carries `Envelope.fence`. */
|
|
584
|
+
readonly lock?: boolean;
|
|
585
|
+
/** The adapter request object for this call, surfaced as a guarded accessor. */
|
|
586
|
+
readonly request?: RequestContext;
|
|
587
|
+
}
|
|
588
|
+
/** `getOrSet` options — plain-`T` factory, no `skip`. */
|
|
589
|
+
interface GetOrSetOptions<T> extends GetOptions {
|
|
590
|
+
readonly factory: (ctx: FactoryCtx<T>) => Promise<T>;
|
|
591
|
+
}
|
|
592
|
+
/** `getOrSetEntry` options — entry-form factory, `skip`-capable. */
|
|
593
|
+
interface GetOrSetEntryOptions<T> extends GetOptions {
|
|
594
|
+
readonly factory: (ctx: EntryFactoryCtx<T>) => Promise<T>;
|
|
595
|
+
}
|
|
596
|
+
/** The `getOrSetEntry` result (adds `durable`, makes it optional). */
|
|
597
|
+
interface CacheEntry<T> {
|
|
598
|
+
readonly value: T | undefined;
|
|
599
|
+
readonly skipped: boolean;
|
|
600
|
+
readonly stale: boolean;
|
|
601
|
+
readonly age: number;
|
|
602
|
+
readonly tier: Tier;
|
|
603
|
+
readonly servedOnError?: boolean;
|
|
604
|
+
/**
|
|
605
|
+
* What THIS serve can prove about the value's durability:
|
|
606
|
+
* - `true` — the value is in the shared L2 tier (a fill that wrote it there, or a read that
|
|
607
|
+
* found it there: either way, proof).
|
|
608
|
+
* - `false` — the value is known NOT durable: the fill was confined to L1 (a `usr:` scope, a
|
|
609
|
+
* dev-mode guarded-accessor demotion), its L2 write-back was suppressed,
|
|
610
|
+
* or the guard REFUSED the write outright — a `kind:'neg'` candidate over a
|
|
611
|
+
* stored val the letter protects. Only the last was never issued at all; `false`
|
|
612
|
+
* reports this turn's own write either way, because in every one of them the
|
|
613
|
+
* kernel positively knows no durable copy of what it tried to write exists.
|
|
614
|
+
* - ABSENT — unknowable on this serve: a `pub`/`ten:` entry found in L1 carries no
|
|
615
|
+
* write-back record, and a serve does not consult L2 on an L1 hit.
|
|
616
|
+
*
|
|
617
|
+
* An absent field is a real answer, not a `false` in disguise. A truthiness test collapses it into
|
|
618
|
+
* the conservative direction (not provably durable), which is a safe default; code that must
|
|
619
|
+
* distinguish known-not-durable from unknowable compares against `false` explicitly.
|
|
620
|
+
*/
|
|
621
|
+
readonly durable?: boolean;
|
|
622
|
+
}
|
|
623
|
+
/** Best-effort match hint on a purge result (renamed from `matched`). */
|
|
624
|
+
type MatchedHint = "yes" | "no-such-scope" | "unknown";
|
|
625
|
+
/** The result of `expire` / `delete` / `clear`. */
|
|
626
|
+
interface PurgeResult {
|
|
627
|
+
readonly epoch: number;
|
|
628
|
+
/** Best-effort; legal workflows must not branch on it. */
|
|
629
|
+
readonly matchedHint: MatchedHint;
|
|
630
|
+
/** Durable + bus handoff + KV batch accepted. NOT the edge. */
|
|
631
|
+
flushed(options?: {
|
|
632
|
+
readonly timeout?: Duration;
|
|
633
|
+
}): Promise<void>;
|
|
634
|
+
/** CDN purge accepted (queue drained) — a distinct promise. */
|
|
635
|
+
edgePurged(options?: {
|
|
636
|
+
readonly timeout?: Duration;
|
|
637
|
+
}): Promise<void>;
|
|
638
|
+
}
|
|
639
|
+
/** `expire` selector — a tag (optionally scope-qualified) or a key. */
|
|
640
|
+
type ExpireSelector = {
|
|
641
|
+
readonly tag: Tag;
|
|
642
|
+
readonly scope?: Scope;
|
|
643
|
+
} | {
|
|
644
|
+
readonly key: string;
|
|
645
|
+
};
|
|
646
|
+
/** `delete` selector — like `expire`, plus a CDN mode. */
|
|
647
|
+
type DeleteSelector = {
|
|
648
|
+
readonly tag: Tag;
|
|
649
|
+
readonly scope?: Scope;
|
|
650
|
+
readonly cdn?: CdnMode;
|
|
651
|
+
} | {
|
|
652
|
+
readonly key: string;
|
|
653
|
+
readonly cdn?: CdnMode;
|
|
654
|
+
};
|
|
655
|
+
/** The explicit acknowledgement `expireAll` / `deleteAll` require. */
|
|
656
|
+
interface OriginLoadGuard {
|
|
657
|
+
readonly iUnderstandTheOriginLoad: true;
|
|
658
|
+
}
|
|
659
|
+
/** Header-byte budget for shared-cache emission (16 KB / ~1,000-tag). */
|
|
660
|
+
interface L3Budget {
|
|
661
|
+
readonly maxBytes?: number;
|
|
662
|
+
readonly maxTags?: number;
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Why a response is ineligible for shared HTTP caches.
|
|
666
|
+
*
|
|
667
|
+
* - `late-tag` / `budget`: the timing + header-budget triggers.
|
|
668
|
+
* - `scope`: the render consumed a dependency whose stored scope is readable but
|
|
669
|
+
* NOT `pub` (a `usr:`/`ten:` entry) — per-principal content.
|
|
670
|
+
* - `scope-unreadable`: a consumed dependency's stored scope could not be read at
|
|
671
|
+
* all (absent / malformed). It fails CLOSED to non-public: an unverifiable scope
|
|
672
|
+
* is treated exactly like a private one.
|
|
673
|
+
*/
|
|
674
|
+
type L3Ineligibility = "late-tag" | "budget" | "scope" | "scope-unreadable";
|
|
675
|
+
/** The shared-cache emission decision at header commit. */
|
|
676
|
+
interface L3Emission {
|
|
677
|
+
/** `false` ⇒ `Cache-Control: private` / no `s-maxage`. */
|
|
678
|
+
readonly eligible: boolean;
|
|
679
|
+
/** Cache-Tag values to emit; `__k`/`__ns` are never emitted. Always empty
|
|
680
|
+
* on a scope-ineligible emission — a poisoned response emits no tag at all. */
|
|
681
|
+
readonly cacheTags: readonly string[];
|
|
682
|
+
readonly ineligibleReason?: L3Ineligibility;
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Request-scoped render-time dependency-tag collector. Cache hits and fills during
|
|
686
|
+
* the request contribute tags AND their scope evidence automatically (that automatic
|
|
687
|
+
* contribution is what makes the gate safe-by-default: a render cannot forget to
|
|
688
|
+
* declare the private entry it just read). `commitHeaders` decides shared-HTTP-cache
|
|
689
|
+
* eligibility at header-commit time: a non-public consumed dependency poisons the response
|
|
690
|
+
* outright, and beneath that gate the timing/budget rules still apply — required
|
|
691
|
+
* tags discovered late, or exceeding the L3 budget, make the response ineligible
|
|
692
|
+
* (invariant 13).
|
|
693
|
+
*/
|
|
694
|
+
interface RenderCollector {
|
|
695
|
+
dependsOn(tag: Tag, options?: {
|
|
696
|
+
readonly l3?: boolean;
|
|
697
|
+
}): void;
|
|
698
|
+
commitHeaders(budget?: L3Budget): L3Emission;
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* A stored entry's scope as read from the surface, preserving the readable-vs-
|
|
702
|
+
* unreadable distinction: a `pub` / `usr:<h>` / `ten:<h>` literal read verbatim, or the fail-closed
|
|
703
|
+
* `unreadable` marker when the surface returned no readable scope (absent / non-string).
|
|
704
|
+
*/
|
|
705
|
+
type ExplainScope = {
|
|
706
|
+
readonly kind: "readable";
|
|
707
|
+
readonly literal: string;
|
|
708
|
+
} | {
|
|
709
|
+
readonly kind: "unreadable";
|
|
710
|
+
};
|
|
711
|
+
/**
|
|
712
|
+
* A stored entry's ttl as read from the surface, preserving the stored-vs-not-stored
|
|
713
|
+
* distinction: the number the stored format carries, or `not-stored` where it carries none. The two
|
|
714
|
+
* arms are structurally distinct and there is no sentinel — a stored ttl of `0` and a ttl the format
|
|
715
|
+
* never wrote are different states, and only the store's own bytes decide which. The v0.2 format
|
|
716
|
+
* persists no ttl (see the v0.4 ttl slot named beside `FactoryCtx.setTtl`), so the witness reports
|
|
717
|
+
* `not-stored` today; a format that later carries one is reported through this same arm.
|
|
718
|
+
*/
|
|
719
|
+
type ExplainTtl = {
|
|
720
|
+
readonly kind: "stored";
|
|
721
|
+
readonly seconds: number;
|
|
722
|
+
} | {
|
|
723
|
+
readonly kind: "not-stored";
|
|
724
|
+
};
|
|
725
|
+
/**
|
|
726
|
+
* The scope evidence one recorded render dependency carries — the public projection
|
|
727
|
+
* of the collector's per-dependency scope. Adds `none` (a bare `dependsOn` with no managed entry
|
|
728
|
+
* behind it, which carries no scope claim) to the stored two-arm form.
|
|
729
|
+
*/
|
|
730
|
+
type ExplainDependencyScope = {
|
|
731
|
+
readonly kind: "none";
|
|
732
|
+
} | ExplainScope;
|
|
733
|
+
/**
|
|
734
|
+
* One render dependency `explain` attributes to the in-flight request — its tags, its
|
|
735
|
+
* scope evidence, and whether it is droppable for shared caches (`l3:false`). Request-scoped: only
|
|
736
|
+
* surfaced while a render collector is bound.
|
|
737
|
+
*/
|
|
738
|
+
interface ExplainedDependency {
|
|
739
|
+
readonly tags: readonly string[];
|
|
740
|
+
readonly scope: ExplainDependencyScope;
|
|
741
|
+
readonly l3?: boolean;
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* The entry's stored identity as `explain` reads it from the store: the
|
|
745
|
+
* stored tag set, the scope as stored (readable-vs-unreadable), the born epoch, the ttl as stored
|
|
746
|
+
* (stored-vs-not-stored), and the entry kind. Every field is read off the stored bytes: a
|
|
747
|
+
* reconstruction default the reader supplies is not stored identity, and reporting one as identity
|
|
748
|
+
* would be the witness testifying to a value the store never carried.
|
|
749
|
+
*/
|
|
750
|
+
interface ExplainIdentity {
|
|
751
|
+
readonly tags: readonly string[];
|
|
752
|
+
readonly scope: ExplainScope;
|
|
753
|
+
readonly bornEpoch: number;
|
|
754
|
+
readonly ttl: ExplainTtl;
|
|
755
|
+
readonly kind: EnvelopeKind;
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* The classification `explain` reaches and the epochs behind it: the reader's
|
|
759
|
+
* current-knowledge verdict over the entry, plus the tag-knowledge watermarks the born epoch was
|
|
760
|
+
* weighed against (the purge epochs behind a `stale`/`dead` verdict, `0` when nothing is known).
|
|
761
|
+
* `unknown` stays `unknown` — the fail-closed narration is the point, not a gap.
|
|
762
|
+
*/
|
|
763
|
+
interface ExplainVerdict {
|
|
764
|
+
readonly validity: Validity;
|
|
765
|
+
readonly softEpoch: number;
|
|
766
|
+
readonly hardEpoch: number;
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* The reader's own already-tracked state at the moment of the call,
|
|
770
|
+
* surfaced via read-only accessors — `explain` adds no tracking of its own.
|
|
771
|
+
* `appliedEpoch`/`suspect`/`channel` come from the invalidation reader; `terminal` from its
|
|
772
|
+
* replication shell. A registry-absent instance has no reader, so all four rest at their neutral
|
|
773
|
+
* values.
|
|
774
|
+
*
|
|
775
|
+
* `channel` is conformant on the letter that matters: the reader tracks it for its own use (the
|
|
776
|
+
* lazy redial needs to know whether it has a channel), so surfacing it ADDS NO BOOKKEEPING — it
|
|
777
|
+
* exposes a fact already on the object. The witness may speak; it may not touch cache state, and
|
|
778
|
+
* reading this getter does not dial, subscribe, or record.
|
|
779
|
+
*
|
|
780
|
+
* It is here because `suspect` alone cannot answer the question an operator actually asks. A reader
|
|
781
|
+
* whose bus never connected is NOT suspect — it has no gap, it has no channel — so a healthy-looking
|
|
782
|
+
* `suspect: false` is exactly what a dead bus produces. `channel` is the field that tells those two
|
|
783
|
+
* apart.
|
|
784
|
+
*/
|
|
785
|
+
interface ExplainReaderState {
|
|
786
|
+
readonly appliedEpoch: number;
|
|
787
|
+
readonly suspect: boolean;
|
|
788
|
+
readonly terminal: boolean;
|
|
789
|
+
/** The live channel's state — `never-established` until the first `hello` ever arrives. */
|
|
790
|
+
readonly channel: ChannelState;
|
|
791
|
+
}
|
|
792
|
+
/** Fields common to every arm of an `Explanation` — what the witness knows regardless of
|
|
793
|
+
* what the store said. `tier` is NOT here: it is an observation over the tiers, and a probe that
|
|
794
|
+
* failed observed none, so the arm that could not read carries no tier to report. */
|
|
795
|
+
interface ExplanationBase {
|
|
796
|
+
readonly key: string;
|
|
797
|
+
readonly reader: ExplainReaderState;
|
|
798
|
+
/** Request-scoped per-dependency attribution — present (possibly empty) only
|
|
799
|
+
* while a render collector is bound to the in-flight request; ABSENT otherwise. It lives and dies
|
|
800
|
+
* with the request. */
|
|
801
|
+
readonly requestDependencies?: readonly ExplainedDependency[];
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* What `explain(key)` reports — three structurally distinct arms on one `kind`
|
|
805
|
+
* discriminant, the same two-arm discipline `ExplainScope`/`ExplainTtl` already carry, extended to
|
|
806
|
+
* the state the store itself can withhold. There is no sentinel, and no arm is reachable by reading
|
|
807
|
+
* a field off another.
|
|
808
|
+
*
|
|
809
|
+
* - `present` — the entry was read: its stored identity and the reader's verdict, found in
|
|
810
|
+
* `tier`.
|
|
811
|
+
* - `absent` — every tier was read and none holds the key. A REPORTABLE state, not an error:
|
|
812
|
+
* `explain` never throws for a missing key.
|
|
813
|
+
* - `read-failed` — a tier's read failed and was suppressed, and no other tier held the key,
|
|
814
|
+
* so whether it exists is UNKNOWN. Distinct from `absent` because the difference
|
|
815
|
+
* is real: one is the store answering "nothing", the other is the store not
|
|
816
|
+
* answering. Reporting the second as the first would be the witness laundering
|
|
817
|
+
* an error into an answer — what the closing sentence forbids. It carries NO
|
|
818
|
+
* `tier` and NO `identity`: a probe that could not read observed neither.
|
|
819
|
+
*
|
|
820
|
+
* The precedence is what each state can support: a found entry is FOUND however the other tiers
|
|
821
|
+
* behaved; and a failed read outranks absence, because a tier that never answered cannot be counted
|
|
822
|
+
* as one that answered "no". `explain` cannot refill, so the serving-path remedy does not transfer
|
|
823
|
+
* — the honest report is the whole remedy available here.
|
|
824
|
+
*/
|
|
825
|
+
type Explanation = (ExplanationBase & {
|
|
826
|
+
readonly kind: "present";
|
|
827
|
+
/** The tier the entry was found in — an OBSERVATION over the tiers, never a hydrating read.*/
|
|
828
|
+
readonly tier: Tier;
|
|
829
|
+
readonly identity: ExplainIdentity;
|
|
830
|
+
readonly verdict: ExplainVerdict;
|
|
831
|
+
}) | (ExplanationBase & {
|
|
832
|
+
readonly kind: "absent";
|
|
833
|
+
readonly tier: "miss";
|
|
834
|
+
}) | (ExplanationBase & {
|
|
835
|
+
readonly kind: "read-failed";
|
|
836
|
+
});
|
|
837
|
+
/** Instance defaults. */
|
|
838
|
+
interface CacheDefaults {
|
|
839
|
+
readonly ttl?: Duration;
|
|
840
|
+
readonly grace?: Duration;
|
|
841
|
+
/** Registered with the Coordinator at bind. */
|
|
842
|
+
readonly maxEntryRetention?: Duration;
|
|
843
|
+
readonly consistency?: Consistency;
|
|
844
|
+
readonly unknownPolicy?: UnknownPolicy;
|
|
845
|
+
readonly staleIfError?: (err: unknown) => boolean;
|
|
846
|
+
readonly graceBackoff?: Duration;
|
|
847
|
+
readonly maxSyncLag?: Duration;
|
|
848
|
+
/** Decode-only codec compatibility for intentional migrations. */
|
|
849
|
+
readonly acceptCodecs?: readonly string[];
|
|
850
|
+
/** Paid idle-heartbeat interval for `__state` liveness. */
|
|
851
|
+
readonly heartbeatInterval?: Duration;
|
|
852
|
+
/** Strong opt-out: degrade to eventual instead of throwing. */
|
|
853
|
+
readonly onUnavailable?: "eventual";
|
|
854
|
+
/**
|
|
855
|
+
* `unknown` resolves to `'error'` — the read THROWS rather than refilling.
|
|
856
|
+
*
|
|
857
|
+
* SETTABLE HERE, not only at the top level, because `defaults` is the surface a composition
|
|
858
|
+
* actually threads: `createWorkersCache` forwards `defaults` wholesale, so a flag that existed
|
|
859
|
+
* only on `CacheConfig` could not be reached through any factory that wraps `createCache`. A
|
|
860
|
+
* legal-embargo control that only the raw path can set is a control most callers do not have.
|
|
861
|
+
*
|
|
862
|
+
* It is an OVERRIDE, not a default: it outranks `unknownPolicy` — see `unknownResolution`.
|
|
863
|
+
*/
|
|
864
|
+
readonly takedownSensitive?: boolean;
|
|
865
|
+
}
|
|
866
|
+
/** `createCache` configuration. `clock`/`rng` are injected. */
|
|
867
|
+
interface CacheConfig {
|
|
868
|
+
readonly namespace: string;
|
|
869
|
+
readonly clock: Clock;
|
|
870
|
+
readonly rng: Rng;
|
|
871
|
+
readonly l1?: Store;
|
|
872
|
+
readonly l2?: Store;
|
|
873
|
+
readonly registry?: Registry;
|
|
874
|
+
readonly bus?: Bus;
|
|
875
|
+
readonly cdn?: Cdn;
|
|
876
|
+
/** Opt-in cross-isolate lock backend. */
|
|
877
|
+
readonly lock?: Lock;
|
|
878
|
+
/** Pluggable value codec. Defaults to the library's safe-mode codec. */
|
|
879
|
+
readonly codec?: Codec;
|
|
880
|
+
readonly defaults?: CacheDefaults;
|
|
881
|
+
/** A plain sink, or a hosted config that pseudonymizes every string field. */
|
|
882
|
+
readonly telemetry?: TelemetrySink | TelemetryConfig;
|
|
883
|
+
/** `unknown` + registry-unreachable resolves to `'error'`, not `'miss'`. */
|
|
884
|
+
readonly takedownSensitive?: boolean;
|
|
885
|
+
/**
|
|
886
|
+
* Dev mode: a singleflight option mismatch throws + a guarded-accessor read in a
|
|
887
|
+
* public factory throws; prod (`false`) runs a separate fill and emits a
|
|
888
|
+
* `singleflight_option_mismatch` event instead.
|
|
889
|
+
*/
|
|
890
|
+
readonly dev?: boolean;
|
|
891
|
+
}
|
|
892
|
+
/** The public cache instance. */
|
|
893
|
+
interface Cache {
|
|
894
|
+
getOrSet<T>(options: GetOrSetOptions<T>): Promise<T>;
|
|
895
|
+
getOrSetEntry<T>(options: GetOrSetEntryOptions<T>): Promise<CacheEntry<T>>;
|
|
896
|
+
expire(selector: ExpireSelector): Promise<PurgeResult>;
|
|
897
|
+
delete(selector: DeleteSelector): Promise<PurgeResult>;
|
|
898
|
+
/** Namespace version bump + hard watermark on the reserved `__ns` tag. */
|
|
899
|
+
clear(): Promise<PurgeResult>;
|
|
900
|
+
expireAll(guard: OriginLoadGuard): Promise<PurgeResult>;
|
|
901
|
+
deleteAll(guard: OriginLoadGuard): Promise<PurgeResult>;
|
|
902
|
+
/** Open a request-scoped render-time tag collector. */
|
|
903
|
+
collect(): RenderCollector;
|
|
904
|
+
/** Witness a key's stored identity, the reader's verdict + evidence, its serving tier, the
|
|
905
|
+
* reader's own state, and any request-scoped dependency attribution. Absence is a
|
|
906
|
+
* reportable state — never throws for a missing key. Introspection adds no tracked state.
|
|
907
|
+
*
|
|
908
|
+
* Two consequences of witnessing rather than simulating, both deliberate:
|
|
909
|
+
* - `usr:`-scoped entries are addressable only through request-scoped attribution; `explain(key)`
|
|
910
|
+
* cannot construct a private key, so it fails closed rather than guess a scope.
|
|
911
|
+
* - The L1 probe reads through the store's own `get`, so it takes that tier's LRU touch: the
|
|
912
|
+
* witness perturbs recency, which is why it is an introspection tool and not a hot-path read.
|
|
913
|
+
* A non-touching `peek` on the store surface is a v0.4 slot — it waits for a second consumer. */
|
|
914
|
+
explain(key: string): Promise<Explanation>;
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Build a cache instance. Wires the dual-epoch invalidation core when a
|
|
918
|
+
* registry + bus are present; otherwise runs the registry-absent codec path.
|
|
919
|
+
* Not-yet-implemented verbs still throw `NotImplementedError`.
|
|
920
|
+
*
|
|
921
|
+
* CONSTRUCTION PERFORMS NO I/O. What happens here is bookkeeping over the arguments —
|
|
922
|
+
* addresses are recorded, components are pointed at each other, and no door is opened. Every
|
|
923
|
+
* deferred piece (the bus subscription via `Invalidation`'s lazy dial; the retention lease via
|
|
924
|
+
* `registerRetention` below) waits for the first read that needs it and runs inside that read's
|
|
925
|
+
* request context. This is what makes one cache per isolate, built at module scope, a legal and
|
|
926
|
+
* expected shape rather than an isolate that refuses to start.
|
|
927
|
+
*/
|
|
928
|
+
declare function createCache(config: CacheConfig): Cache;
|
|
929
|
+
//#endregion
|
|
930
|
+
export { Clock as $, Tier as A, EnvelopeEnc as B, L3Ineligibility as C, TelemetrySink as Ct, RenderCollector as D, Watermark as Dt, PurgeResult as E, Validity as Et, compound as F, Bus as G, Duration as H, globalTag as I, CasOrder as J, BusEvent as K, t as L, createCache as M, Tag as N, RequestContext as O, TagPart as P, CdnMode as Q, ChannelState as R, L3Emission as S, TelemetryEventName as St, OriginLoadGuard as T, TagKnowledge as Tt, DurationUnit as U, EnvelopeKind as V, duration as W, CasStore as X, CasRecord as Y, Cdn as Z, GetOptions as _, SinkErrorHook as _t, DeleteSelector as a, Registry as at, GracedInfo as b, TelemetryConfig as bt, ExplainDependencyScope as c, Store as ct, ExplainScope as d, StoreWriteError as dt, Codec as et, ExplainTtl as f, StoreWriteOptions as ft, FactoryCtx as g, memory as gt, Explanation as h, isStoreWriteError as ht, CacheEntry as i, ReadKind as it, UnknownPolicy as j, Scope as k, ExplainIdentity as l, StoreMetadata as lt, ExplainedDependency as m, TagChange as mt, CacheConfig as n, LockHandle as nt, EntryFactoryCtx as o, RegistryAck as ot, ExplainVerdict as p, Subscription as pt, BusFrame as q, CacheDefaults as r, MemoryOptions as rt, ExpireSelector as s, Rng as st, Cache as t, Lock as tt, ExplainReaderState as u, StoreValue as ut, GetOrSetEntryOptions as v, StrongDegradedReason as vt, MatchedHint as w, Consistency as wt, L3Budget as x, TelemetryEvent as xt, GetOrSetOptions as y, TELEMETRY_EVENTS as yt, Envelope as z };
|