@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,92 @@
|
|
|
1
|
+
import { Ct as TelemetrySink, O as RequestContext, _t as SinkErrorHook, t as Cache, x as L3Budget, yt as TELEMETRY_EVENTS } from "../../cache-YWn0VZpn.js";
|
|
2
|
+
import { MiddlewareFunction, RouterContext } from "react-router";
|
|
3
|
+
//#region src/adapters/react-router/middleware.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The RR-v8 middleware argument object (the first parameter RR passes a `MiddlewareFunction`:
|
|
6
|
+
* `{ request, url, params, context }`, `context` being the request's `RouterContextProvider`).
|
|
7
|
+
* Re-exported off `MiddlewareFunction` so the `request` derivation option below is typed without
|
|
8
|
+
* naming react-router's private `DataFunctionArgs`.
|
|
9
|
+
*/
|
|
10
|
+
type CacheMiddlewareArgs = Parameters<MiddlewareFunction>[0];
|
|
11
|
+
/** The single telemetry event a shared-cache-INELIGIBLE response emits — exactly
|
|
12
|
+
* one per response, naming the reason CLASS in its `reason` field and nothing else. No tag names
|
|
13
|
+
* ride the payload: which dependency did it is a question for `explain()`, and an operator
|
|
14
|
+
* watching this counter only needs to see THAT a route stopped being CDN-cacheable. */
|
|
15
|
+
declare const L3_INELIGIBLE: typeof TELEMETRY_EVENTS.l3_ineligible;
|
|
16
|
+
/**
|
|
17
|
+
* The shared-cache header budget: 16 KB aggregate, ~1,000 tags. (The 1,024-char cap is a
|
|
18
|
+
* purge-API limit, not a header one — header tags carry no per-tag cap.) Overridable per
|
|
19
|
+
* middleware via `CacheMiddlewareOptions.l3Budget`.
|
|
20
|
+
*/
|
|
21
|
+
declare const L3_BUDGET_DEFAULT: L3Budget;
|
|
22
|
+
/**
|
|
23
|
+
* `cacheMiddleware` configuration. `cache` is the already-built instance the middleware provides
|
|
24
|
+
* into the request lifecycle (a `createWorkersCache(...)` / `createCache(...)` result). `request`
|
|
25
|
+
* is the OPTIONAL principal derivation: it maps the RR request to the identity fields the kernel
|
|
26
|
+
* reads (`userId`/`tenant`) — the app owns that mapping because identity is app-domain.
|
|
27
|
+
* Omit it and the request carries no derivable principal (the shared `pub` class, 4).
|
|
28
|
+
*
|
|
29
|
+
* `waitUntil` is the Workers `ExecutionContext.waitUntil` the poll tick rides on — pass
|
|
30
|
+
* `context.cloudflare.ctx.waitUntil` (RR's Cloudflare load context) or `waitUntil` from
|
|
31
|
+
* `cloudflare:workers`, bound. It is OPTIONAL and never load-bearing for correctness: without it the
|
|
32
|
+
* tick still fires (unawaited, best-effort) and simply is not kept alive past the response. It is
|
|
33
|
+
* never awaited on the request path either way — the tick races the loaders, it does not gate them.
|
|
34
|
+
*
|
|
35
|
+
* `telemetry` receives the driver's swallowed-tick counter (`poll_tick_failed`) and the single
|
|
36
|
+
* `l3_ineligible` event a demoted response emits. Every delivery to it goes through
|
|
37
|
+
* `safeEmit`, so a throwing/rejecting sink is swallowed; `onSinkError` is the OPTIONAL injectable
|
|
38
|
+
* hook `safeEmit` calls when it swallows such a failure — a sink for reporting the reporter.
|
|
39
|
+
*
|
|
40
|
+
* `l3Budget` overrides the header budget (`L3_BUDGET_DEFAULT`).
|
|
41
|
+
*/
|
|
42
|
+
interface CacheMiddlewareOptions {
|
|
43
|
+
readonly cache: Cache;
|
|
44
|
+
readonly request?: (args: CacheMiddlewareArgs) => RequestContext;
|
|
45
|
+
readonly waitUntil?: (promise: Promise<unknown>) => void;
|
|
46
|
+
readonly telemetry?: TelemetrySink;
|
|
47
|
+
readonly onSinkError?: SinkErrorHook;
|
|
48
|
+
readonly l3Budget?: L3Budget;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The `RouterContext<Cache>` key the middleware writes the cache under. A loader/action reads the
|
|
52
|
+
* request's cache with `context.get(cacheContext)`. Created with no default value, so a `get` in a
|
|
53
|
+
* request the middleware never ran on throws (an honest misconfiguration signal, not a silent miss).
|
|
54
|
+
*/
|
|
55
|
+
declare const cacheContext: RouterContext<Cache>;
|
|
56
|
+
/**
|
|
57
|
+
* The current request's principal-bearing `RequestContext`, or `undefined` outside a middleware
|
|
58
|
+
* frame. Pass it straight through to `getOrSet`'s `request` option so the kernel derives the
|
|
59
|
+
* request's scope: `cache.getOrSet({ key, request: currentRequest(), factory })`.
|
|
60
|
+
*/
|
|
61
|
+
declare function currentRequest(): RequestContext | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* Build the RR-v8 root middleware. Always-on in v8 (the `unstable_`/`v8_middleware` flag
|
|
64
|
+
* was removed at 8.0.0): register the returned function in a route module's `middleware` array. It
|
|
65
|
+
* provides the cache into `context`, opens the request + collector frames, fires the replication
|
|
66
|
+
* poll tick (unawaited), `await`s `next()`, commits the collector, and applies the
|
|
67
|
+
* header algorithm.
|
|
68
|
+
*/
|
|
69
|
+
declare function cacheMiddleware(options: CacheMiddlewareOptions): MiddlewareFunction;
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region src/adapters/react-router/poll-driver.d.ts
|
|
72
|
+
/**
|
|
73
|
+
* Minimum ms between two piggybacked ticks on ONE cache instance. Set to the ladder's fastest rung
|
|
74
|
+
* (~1s): ticking faster can never make the poller act sooner (every path it drives is gated by its
|
|
75
|
+
* own due-time), so anything below this is pure waste under load.
|
|
76
|
+
*/
|
|
77
|
+
declare const TICK_MIN_INTERVAL_MS = 1000;
|
|
78
|
+
/**
|
|
79
|
+
* The counter a swallowed tick failure increments. The tick runs OUT-OF-BAND from the response,
|
|
80
|
+
* so its failure must never surface to the caller — but a silently failing replication poll is
|
|
81
|
+
* exactly the kind of degradation an operator has to be able to see, so the swallow is paired with
|
|
82
|
+
* this event. Deliberately payload-free apart from the error's NAME (a code, never free-form detail
|
|
83
|
+
* or a key/tag reference), so a plain sink can be pointed at it safely.
|
|
84
|
+
*
|
|
85
|
+
* NOTE: `registry_degraded` / `state_stale` — the richer telemetry names this event may
|
|
86
|
+
* eventually be folded into — are now ENROLLED in the const-union catalog (`TELEMETRY_EVENTS`) as
|
|
87
|
+
* RESERVED members, but their emit condition is unruled: `poll_tick_failed` keeps emitting as-is.
|
|
88
|
+
* (FOR FABLE: whether to enrich/rename this counter into a degraded-mode signal is an open question.)
|
|
89
|
+
*/
|
|
90
|
+
declare const POLL_TICK_FAILED: typeof TELEMETRY_EVENTS.poll_tick_failed;
|
|
91
|
+
//#endregion
|
|
92
|
+
export { type CacheMiddlewareArgs, type CacheMiddlewareOptions, L3_BUDGET_DEFAULT, L3_INELIGIBLE, POLL_TICK_FAILED, TICK_MIN_INTERVAL_MS, cacheContext, cacheMiddleware, currentRequest };
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { a as replicationPollerOf, i as managedDependenciesOf, m as safeEmit, n as bindRenderCollector, o as resolveCacheIdentity, p as TELEMETRY_EVENTS } from "../../cache-Z4cJzPmm.js";
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
import { createContext } from "react-router";
|
|
4
|
+
//#region src/adapters/react-router/poll-driver.ts
|
|
5
|
+
/**
|
|
6
|
+
* Minimum ms between two piggybacked ticks on ONE cache instance. Set to the ladder's fastest rung
|
|
7
|
+
* (~1s): ticking faster can never make the poller act sooner (every path it drives is gated by its
|
|
8
|
+
* own due-time), so anything below this is pure waste under load.
|
|
9
|
+
*/
|
|
10
|
+
const TICK_MIN_INTERVAL_MS = 1e3;
|
|
11
|
+
/**
|
|
12
|
+
* The counter a swallowed tick failure increments. The tick runs OUT-OF-BAND from the response,
|
|
13
|
+
* so its failure must never surface to the caller — but a silently failing replication poll is
|
|
14
|
+
* exactly the kind of degradation an operator has to be able to see, so the swallow is paired with
|
|
15
|
+
* this event. Deliberately payload-free apart from the error's NAME (a code, never free-form detail
|
|
16
|
+
* or a key/tag reference), so a plain sink can be pointed at it safely.
|
|
17
|
+
*
|
|
18
|
+
* NOTE: `registry_degraded` / `state_stale` — the richer telemetry names this event may
|
|
19
|
+
* eventually be folded into — are now ENROLLED in the const-union catalog (`TELEMETRY_EVENTS`) as
|
|
20
|
+
* RESERVED members, but their emit condition is unruled: `poll_tick_failed` keeps emitting as-is.
|
|
21
|
+
* (FOR FABLE: whether to enrich/rename this counter into a degraded-mode signal is an open question.)
|
|
22
|
+
*/
|
|
23
|
+
const POLL_TICK_FAILED = TELEMETRY_EVENTS.poll_tick_failed;
|
|
24
|
+
/** Last piggybacked tick per cache instance — the staleness gate's cursor. Per-isolate by nature
|
|
25
|
+
* (an isolate holds one `Cache`), and keyed by the instance so two caches in one isolate never
|
|
26
|
+
* starve each other's ticks.
|
|
27
|
+
*
|
|
28
|
+
* Keyed by RESOLVED identity (the third instance-keyed seam, and the one that does not live beside
|
|
29
|
+
* the other two). A facade and the instance it fronts drive the SAME poller, so they must share the
|
|
30
|
+
* SAME gate: two gates over one poller would let an app holding both objects tick it twice per
|
|
31
|
+
* window, which is exactly the hammering the gate exists to stop. */
|
|
32
|
+
const lastTickMs = /* @__PURE__ */ new WeakMap();
|
|
33
|
+
/**
|
|
34
|
+
* THE SWALLOW WRAP (adapter-side; the guard itself now lives in the shared `safeEmit`).
|
|
35
|
+
* `ctx.waitUntil(tick())` on a rejecting promise surfaces as an unhandled rejection in the request's
|
|
36
|
+
* lifecycle — a failing L2 read would then be able to reach the response of a request that never
|
|
37
|
+
* needed that read. So the tick is wrapped: catch → count → swallow. Adapter code by ruling, not
|
|
38
|
+
* kernel: the kernel's `resync` is entitled to reject, and the driver that chose to run it
|
|
39
|
+
* out-of-band is what owes the caller its silence.
|
|
40
|
+
*
|
|
41
|
+
* The returned promise NEVER rejects — and the guarantee is AIRTIGHT over the whole failure surface,
|
|
42
|
+
* INCLUDING the reporting channel itself: the telemetry emit is routed through `safeEmit`, so
|
|
43
|
+
* neither a throwing sink nor a rejecting `async` sink can turn the swallow into an escape, and the
|
|
44
|
+
* injected `onSinkError` hook observes a swallowed sink failure. (The default `waitUntil` drops
|
|
45
|
+
* the promise on the floor; a rejection here would therefore become an unhandled rejection —
|
|
46
|
+
* process-fatal on the Node/RR lane, an uncaught request error on workerd. A background freshness
|
|
47
|
+
* probe must never be able to kill a user's request.)
|
|
48
|
+
*/
|
|
49
|
+
async function runTick(poller, nowMs, telemetry, onSinkError) {
|
|
50
|
+
try {
|
|
51
|
+
await poller.onPollTick(nowMs);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
safeEmit(telemetry, {
|
|
54
|
+
error: error instanceof Error ? error.name : "unknown",
|
|
55
|
+
type: POLL_TICK_FAILED
|
|
56
|
+
}, onSinkError);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Fire one replication poll tick for `cache` IF the last one is stale (see `TICK_MIN_INTERVAL_MS`).
|
|
61
|
+
*
|
|
62
|
+
* Returns the in-flight tick — hand it straight to `ctx.waitUntil` so it outlives the response — or
|
|
63
|
+
* `undefined` when nothing is due (the gate held, or the cache is registry-absent and has no reader
|
|
64
|
+
* to drive). The returned promise NEVER rejects: a store/resync failure inside the tick is caught,
|
|
65
|
+
* counted through `telemetry` (`poll_tick_failed`), and swallowed, because a background freshness
|
|
66
|
+
* probe has no business failing a user's response. The `telemetry` sink is NOT trusted to keep that
|
|
67
|
+
* promise: it is app code on the failure path, so a sink that throws — or an `async` sink that
|
|
68
|
+
* rejects — is neutralized too, and floats nothing.
|
|
69
|
+
*/
|
|
70
|
+
function tickIfStale(cache, nowMs, telemetry, onSinkError) {
|
|
71
|
+
const poller = replicationPollerOf(cache);
|
|
72
|
+
if (poller === void 0) return;
|
|
73
|
+
const gateKey = resolveCacheIdentity(cache);
|
|
74
|
+
const last = lastTickMs.get(gateKey);
|
|
75
|
+
if (last !== void 0 && nowMs - last < 1e3) return;
|
|
76
|
+
lastTickMs.set(gateKey, nowMs);
|
|
77
|
+
return runTick(poller, nowMs, telemetry, onSinkError);
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region src/adapters/react-router/middleware.ts
|
|
81
|
+
/** The single telemetry event a shared-cache-INELIGIBLE response emits — exactly
|
|
82
|
+
* one per response, naming the reason CLASS in its `reason` field and nothing else. No tag names
|
|
83
|
+
* ride the payload: which dependency did it is a question for `explain()`, and an operator
|
|
84
|
+
* watching this counter only needs to see THAT a route stopped being CDN-cacheable. */
|
|
85
|
+
const L3_INELIGIBLE = TELEMETRY_EVENTS.l3_ineligible;
|
|
86
|
+
/**
|
|
87
|
+
* The shared-cache header budget: 16 KB aggregate, ~1,000 tags. (The 1,024-char cap is a
|
|
88
|
+
* purge-API limit, not a header one — header tags carry no per-tag cap.) Overridable per
|
|
89
|
+
* middleware via `CacheMiddlewareOptions.l3Budget`.
|
|
90
|
+
*/
|
|
91
|
+
const L3_BUDGET_DEFAULT = {
|
|
92
|
+
maxBytes: 16384,
|
|
93
|
+
maxTags: 1e3
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* The reason CLASS a telemetry consumer sees, mapped from the kernel's `L3Ineligibility` union.
|
|
97
|
+
* TWO VOCABULARIES, ONE MAPPING POINT — deliberately: the kernel's union is kebab (its own existing
|
|
98
|
+
* convention, `late-tag`), while the event vocabulary is snake (`poll_tick_failed`,
|
|
99
|
+
* `singleflight_option_mismatch`), and the design fixes `scope_unreadable` as the literal class name. This
|
|
100
|
+
* function is the ONLY place the two meet; nothing downstream re-spells either.
|
|
101
|
+
*/
|
|
102
|
+
const REASON_CLASS = {
|
|
103
|
+
budget: "budget",
|
|
104
|
+
"late-tag": "late_tag",
|
|
105
|
+
scope: "scope",
|
|
106
|
+
"scope-unreadable": "scope_unreadable"
|
|
107
|
+
};
|
|
108
|
+
/** The two response headers this adapter governs. `Cache-Control` carries the shared-cache
|
|
109
|
+
* posture; `Cache-Tag` is the CDN purge handle keyed off the render's dependency tags. */
|
|
110
|
+
const CACHE_CONTROL = "Cache-Control";
|
|
111
|
+
const CACHE_TAG = "Cache-Tag";
|
|
112
|
+
/** The one directive the adapter ever WRITES to `Cache-Control`. It never writes
|
|
113
|
+
* `public` or an `s-maxage` — opting into shared caching is the application's call, and the
|
|
114
|
+
* adapter's job is to emit the purge tags when that is safe and to demote when it is not. */
|
|
115
|
+
const PRIVATE = "private";
|
|
116
|
+
/**
|
|
117
|
+
* The `RouterContext<Cache>` key the middleware writes the cache under. A loader/action reads the
|
|
118
|
+
* request's cache with `context.get(cacheContext)`. Created with no default value, so a `get` in a
|
|
119
|
+
* request the middleware never ran on throws (an honest misconfiguration signal, not a silent miss).
|
|
120
|
+
*/
|
|
121
|
+
const cacheContext = createContext();
|
|
122
|
+
/** The empty principal-bearing frame used when no `request` derivation is configured — a request
|
|
123
|
+
* with no adapter-visible identity: `derivePrincipal` finds none ⇒ the `pub` class. */
|
|
124
|
+
const EMPTY_REQUEST_CONTEXT = Object.freeze({});
|
|
125
|
+
const requestFrame = new AsyncLocalStorage();
|
|
126
|
+
const collectorFrame = new AsyncLocalStorage();
|
|
127
|
+
/**
|
|
128
|
+
* The current request's principal-bearing `RequestContext`, or `undefined` outside a middleware
|
|
129
|
+
* frame. Pass it straight through to `getOrSet`'s `request` option so the kernel derives the
|
|
130
|
+
* request's scope: `cache.getOrSet({ key, request: currentRequest(), factory })`.
|
|
131
|
+
*/
|
|
132
|
+
function currentRequest() {
|
|
133
|
+
return requestFrame.getStore();
|
|
134
|
+
}
|
|
135
|
+
/** The `waitUntil` used when the app configures none: the tick still runs, it is simply not held
|
|
136
|
+
* alive past the response (best-effort, and safe — the driver's promise never rejects). */
|
|
137
|
+
function floatingWaitUntil(promise) {}
|
|
138
|
+
/** Nothing to write — the app's headers stand exactly as authored. */
|
|
139
|
+
const NO_EDIT = Object.freeze({});
|
|
140
|
+
/**
|
|
141
|
+
* Decide the posture for the response, given the render's emission and how many
|
|
142
|
+
* dependencies that render consumed (`managed` — hits, fills, and explicit `dependsOn`
|
|
143
|
+
* declarations, read off the kernel's in-package seam; it is deliberately NOT a field of the
|
|
144
|
+
* spec-verbatim public `L3Emission`, which stays exactly the shape names).
|
|
145
|
+
*
|
|
146
|
+
* THREE postures, in precedence order:
|
|
147
|
+
*
|
|
148
|
+
* 1. NO MANAGED DEPENDENCY (the jurisdiction floor). The render consumed nothing this library
|
|
149
|
+
* manages, so the adapter asserts nothing: it fills in `Cache-Control: private` ONLY where the
|
|
150
|
+
* application set no cache header at all, and it leaves an app-set `Cache-Tag` alone. A
|
|
151
|
+
* default-fill, never an override — the adapter has observed nothing that licenses an opinion.
|
|
152
|
+
* This is also what separates the floor from an ELIGIBLE render that happens to emit no tag:
|
|
153
|
+
* the emission alone cannot tell those apart, and they get different postures.
|
|
154
|
+
*
|
|
155
|
+
* 2. INELIGIBLE (scope-poisoned, or late-tag/budget). `Cache-Control: private` is FORCED — it
|
|
156
|
+
* overwrites whatever the application set, because the application's own header is what has to
|
|
157
|
+
* lose here: the whole point of the branch is a route that asked to be publicly cached while
|
|
158
|
+
* its render read per-principal content. Any `Cache-Tag` is DELETED, including one the adapter
|
|
159
|
+
* never assembled: removing it, rather than declining to add one, is what the branch is for —
|
|
160
|
+
* a downstream cache parses the header off the wire whether or not it stores the response.
|
|
161
|
+
*
|
|
162
|
+
* 3. ELIGIBLE. The adapter's own assembled `Cache-Tag` replaces whatever was there — DEDUPLICATED
|
|
163
|
+
* here, at the join. One entry read by several loaders in a render reaches the collector once
|
|
164
|
+
* per consumption, so its tag arrives once per consumption; a header that repeats the same
|
|
165
|
+
* purge handle buys nothing and costs bytes. The dedup lives HERE and NOT in the collector on
|
|
166
|
+
* purpose: `commitHeaders`'s BUDGET counts every occurrence (that is the measure invariant
|
|
167
|
+
* 13 pins — collapsing duplicates there would make an over-budget tag list come out eligible),
|
|
168
|
+
* and emitting fewer bytes than were measured is always safe, while measuring fewer bytes than
|
|
169
|
+
* are emitted is not. The adapter does NOT fabricate public caching — no `public`, no
|
|
170
|
+
* `s-maxage`, ever. The application opts into shared caching with its own `Cache-Control`; the
|
|
171
|
+
* adapter's job is to emit the purge tags when that is safe and to demote when it is not. So
|
|
172
|
+
* the safe `private` default still fills in where the app wrote no cache header.
|
|
173
|
+
*/
|
|
174
|
+
function decidePosture(emission, managed, appSetCacheControl) {
|
|
175
|
+
const safeDefault = appSetCacheControl ? NO_EDIT : { cacheControl: PRIVATE };
|
|
176
|
+
if (managed === 0) return safeDefault;
|
|
177
|
+
if (!emission.eligible) return {
|
|
178
|
+
cacheControl: PRIVATE,
|
|
179
|
+
cacheTag: null
|
|
180
|
+
};
|
|
181
|
+
const emitted = [...new Set(emission.cacheTags)];
|
|
182
|
+
return {
|
|
183
|
+
...safeDefault,
|
|
184
|
+
cacheTag: emitted.length > 0 ? emitted.join(",") : null
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/** Write one decided posture onto a mutable `Headers`. Throws iff the guard is immutable — which is
|
|
188
|
+
* exactly the signal `applyHeaders` acts on, and never a partial write: an immutable guard rejects
|
|
189
|
+
* the FIRST mutation, so either the whole posture lands or none of it does. */
|
|
190
|
+
function writePosture(headers, posture) {
|
|
191
|
+
if (posture.cacheControl !== void 0) headers.set(CACHE_CONTROL, posture.cacheControl);
|
|
192
|
+
if (posture.cacheTag === null) headers.delete(CACHE_TAG);
|
|
193
|
+
else if (posture.cacheTag !== void 0) headers.set(CACHE_TAG, posture.cacheTag);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Rebuild `response` with a MUTABLE header guard and the posture written onto it, preserving status,
|
|
197
|
+
* statusText, body, and every header the original carried (the `Response(body, response)` init form
|
|
198
|
+
* copies all three). `undefined` when the platform refuses to reconstruct the response at all — see
|
|
199
|
+
* `applyHeaders` for why that residue is safe.
|
|
200
|
+
*/
|
|
201
|
+
function rebuiltWithPosture(response, posture) {
|
|
202
|
+
try {
|
|
203
|
+
const rebuilt = new Response(response.body, response);
|
|
204
|
+
writePosture(rebuilt.headers, posture);
|
|
205
|
+
return rebuilt;
|
|
206
|
+
} catch {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Apply the posture to the response and return the response to send.
|
|
212
|
+
*
|
|
213
|
+
* NOT ALL RESPONSES ARE WRITABLE. A `Response` obtained from `Response.redirect(...)`, from a
|
|
214
|
+
* `fetch()` passthrough, or from `caches.match(...)` carries an IMMUTABLE header guard: every
|
|
215
|
+
* `set`/`delete` on it throws `TypeError`. A middleware that mutates headers unconditionally
|
|
216
|
+
* therefore rejects its own promise on such a response and turns it into a 500 — a route that
|
|
217
|
+
* merely redirects would break. So the write is attempted, and on refusal the SAME decided posture
|
|
218
|
+
* is written onto a rebuilt copy (status, statusText, body and headers all preserved) which is
|
|
219
|
+
* returned in the original's place.
|
|
220
|
+
*
|
|
221
|
+
* THE RESIDUE, and why it is safe: a response the platform will not let us reconstruct either
|
|
222
|
+
* (`Response.error()` and the other sub-200 network-error forms) is returned untouched. Such a
|
|
223
|
+
* response carries NO readable or writable headers at all — there is no app-set `Cache-Control` to
|
|
224
|
+
* override and no `Cache-Tag` for a shared cache to read off it — so declining to write a posture
|
|
225
|
+
* onto it withholds nothing and leaks nothing. The one thing that must never happen — a throw out
|
|
226
|
+
* of the middleware — cannot.
|
|
227
|
+
*
|
|
228
|
+
* The telemetry event an ineligible response owes is emitted exactly once, off the
|
|
229
|
+
* DECISION, so it is independent of which of the three write paths the response takes: one event per
|
|
230
|
+
* response, naming the reason CLASS, carrying no tag names.
|
|
231
|
+
*/
|
|
232
|
+
function applyHeaders(response, emission, telemetry, onSinkError) {
|
|
233
|
+
const managed = managedDependenciesOf(emission);
|
|
234
|
+
const posture = decidePosture(emission, managed, response.headers.has(CACHE_CONTROL));
|
|
235
|
+
if (managed > 0 && !emission.eligible && emission.ineligibleReason !== void 0) safeEmit(telemetry, {
|
|
236
|
+
reason: REASON_CLASS[emission.ineligibleReason],
|
|
237
|
+
type: L3_INELIGIBLE
|
|
238
|
+
}, onSinkError);
|
|
239
|
+
if (posture === NO_EDIT) return response;
|
|
240
|
+
try {
|
|
241
|
+
writePosture(response.headers, posture);
|
|
242
|
+
return response;
|
|
243
|
+
} catch {}
|
|
244
|
+
return rebuiltWithPosture(response, posture) ?? response;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Build the RR-v8 root middleware. Always-on in v8 (the `unstable_`/`v8_middleware` flag
|
|
248
|
+
* was removed at 8.0.0): register the returned function in a route module's `middleware` array. It
|
|
249
|
+
* provides the cache into `context`, opens the request + collector frames, fires the replication
|
|
250
|
+
* poll tick (unawaited), `await`s `next()`, commits the collector, and applies the
|
|
251
|
+
* header algorithm.
|
|
252
|
+
*/
|
|
253
|
+
function cacheMiddleware(options) {
|
|
254
|
+
const { cache, l3Budget = L3_BUDGET_DEFAULT, onSinkError, request: deriveRequest, telemetry, waitUntil = floatingWaitUntil } = options;
|
|
255
|
+
bindRenderCollector(cache, () => collectorFrame.getStore());
|
|
256
|
+
return async (args, next) => {
|
|
257
|
+
const requestContext = deriveRequest?.(args) ?? EMPTY_REQUEST_CONTEXT;
|
|
258
|
+
const collector = cache.collect();
|
|
259
|
+
return await requestFrame.run(requestContext, async () => {
|
|
260
|
+
args.context.set(cacheContext, cache);
|
|
261
|
+
const tick = tickIfStale(cache, Date.now(), telemetry, onSinkError);
|
|
262
|
+
if (tick !== void 0) waitUntil(tick);
|
|
263
|
+
return await collectorFrame.run(collector, async () => {
|
|
264
|
+
const response = await next();
|
|
265
|
+
if (response instanceof Response) return applyHeaders(response, collector.commitHeaders(l3Budget), telemetry, onSinkError);
|
|
266
|
+
return response;
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
//#endregion
|
|
272
|
+
export { L3_BUDGET_DEFAULT, L3_INELIGIBLE, POLL_TICK_FAILED, TICK_MIN_INTERVAL_MS, cacheContext, cacheMiddleware, currentRequest };
|