@mercuryworkshop/proxy-bootstrap 0.0.1

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.
Files changed (102) hide show
  1. package/dist/.downloads/controller/package/dist/controller.api.js +44 -0
  2. package/dist/.downloads/controller/package/dist/controller.api.js.map +1 -0
  3. package/dist/.downloads/controller/package/dist/controller.inject.js +2 -0
  4. package/dist/.downloads/controller/package/dist/controller.inject.js.map +1 -0
  5. package/dist/.downloads/controller/package/dist/controller.sw.js +2 -0
  6. package/dist/.downloads/controller/package/dist/controller.sw.js.map +1 -0
  7. package/dist/.downloads/controller/package/dist/types/cache.d.ts +39 -0
  8. package/dist/.downloads/controller/package/dist/types/index.d.ts +74 -0
  9. package/dist/.downloads/controller/package/dist/types/inject.d.ts +16 -0
  10. package/dist/.downloads/controller/package/dist/types/sw.d.ts +2 -0
  11. package/dist/.downloads/controller/package/dist/types/symbols.d.ts +1 -0
  12. package/dist/.downloads/controller/package/dist/types/typesEntry.d.ts +5 -0
  13. package/dist/.downloads/controller/package/package.json +16 -0
  14. package/dist/.downloads/controller/package/src/cache.ts +473 -0
  15. package/dist/.downloads/controller/package/src/index.ts +809 -0
  16. package/dist/.downloads/controller/package/src/inject.ts +370 -0
  17. package/dist/.downloads/controller/package/src/sw.ts +231 -0
  18. package/dist/.downloads/controller/package/src/symbols.ts +1 -0
  19. package/dist/.downloads/controller/package/src/types.d.ts +139 -0
  20. package/dist/.downloads/controller/package/src/typesEntry.ts +6 -0
  21. package/dist/.downloads/controller/package/tsconfig.json +24 -0
  22. package/dist/.downloads/controller/package/tsconfig.types.json +16 -0
  23. package/dist/.downloads/libcurl-transport/package/LICENSE +661 -0
  24. package/dist/.downloads/libcurl-transport/package/README.md +52 -0
  25. package/dist/.downloads/libcurl-transport/package/dist/index.d.ts +25 -0
  26. package/dist/.downloads/libcurl-transport/package/dist/index.js +6500 -0
  27. package/dist/.downloads/libcurl-transport/package/dist/index.mjs +6481 -0
  28. package/dist/.downloads/libcurl-transport/package/package.json +37 -0
  29. package/dist/.downloads/scramjet/package/dist/167400cb144aab22.wasm +0 -0
  30. package/dist/.downloads/scramjet/package/dist/2919e49b986edf8c.wasm +0 -0
  31. package/dist/.downloads/scramjet/package/dist/5aed1d5e48aab205.wasm +0 -0
  32. package/dist/.downloads/scramjet/package/dist/882d77912a3c8e3a.wasm +0 -0
  33. package/dist/.downloads/scramjet/package/dist/ac6aa30297a80464.wasm +0 -0
  34. package/dist/.downloads/scramjet/package/dist/c10a57758af882c8.wasm +0 -0
  35. package/dist/.downloads/scramjet/package/dist/cfd04aaae6955b67.wasm +0 -0
  36. package/dist/.downloads/scramjet/package/dist/d06a90fd413b36cf.wasm +0 -0
  37. package/dist/.downloads/scramjet/package/dist/dda06914899a6c28.wasm +0 -0
  38. package/dist/.downloads/scramjet/package/dist/scramjet.js +34 -0
  39. package/dist/.downloads/scramjet/package/dist/scramjet.js.map +1 -0
  40. package/dist/.downloads/scramjet/package/dist/scramjet.mjs +34 -0
  41. package/dist/.downloads/scramjet/package/dist/scramjet.mjs.map +1 -0
  42. package/dist/.downloads/scramjet/package/dist/scramjet.wasm +0 -0
  43. package/dist/.downloads/scramjet/package/dist/scramjet_bundled.js +34 -0
  44. package/dist/.downloads/scramjet/package/dist/scramjet_bundled.js.map +1 -0
  45. package/dist/.downloads/scramjet/package/dist/scramjet_bundled.mjs +34 -0
  46. package/dist/.downloads/scramjet/package/dist/scramjet_bundled.mjs.map +1 -0
  47. package/dist/.downloads/scramjet/package/dist/types/Tap.d.ts +32 -0
  48. package/dist/.downloads/scramjet/package/dist/types/client/client.d.ts +115 -0
  49. package/dist/.downloads/scramjet/package/dist/types/client/entry.d.ts +5 -0
  50. package/dist/.downloads/scramjet/package/dist/types/client/events.d.ts +10 -0
  51. package/dist/.downloads/scramjet/package/dist/types/client/global.d.ts +4 -0
  52. package/dist/.downloads/scramjet/package/dist/types/client/helpers.d.ts +1 -0
  53. package/dist/.downloads/scramjet/package/dist/types/client/index.d.ts +7 -0
  54. package/dist/.downloads/scramjet/package/dist/types/client/location.d.ts +2 -0
  55. package/dist/.downloads/scramjet/package/dist/types/client/shared/eval.d.ts +3 -0
  56. package/dist/.downloads/scramjet/package/dist/types/client/shared/sourcemaps.d.ts +19 -0
  57. package/dist/.downloads/scramjet/package/dist/types/client/shared/unproxy.d.ts +19 -0
  58. package/dist/.downloads/scramjet/package/dist/types/client/shared/wrap.d.ts +4 -0
  59. package/dist/.downloads/scramjet/package/dist/types/client/singletonbox.d.ts +16 -0
  60. package/dist/.downloads/scramjet/package/dist/types/client/unproxy.generated.d.ts +50 -0
  61. package/dist/.downloads/scramjet/package/dist/types/fetch/body.d.ts +3 -0
  62. package/dist/.downloads/scramjet/package/dist/types/fetch/fetch.d.ts +7 -0
  63. package/dist/.downloads/scramjet/package/dist/types/fetch/headers.d.ts +19 -0
  64. package/dist/.downloads/scramjet/package/dist/types/fetch/index.d.ts +128 -0
  65. package/dist/.downloads/scramjet/package/dist/types/fetch/parse.d.ts +22 -0
  66. package/dist/.downloads/scramjet/package/dist/types/fetch/util.d.ts +7 -0
  67. package/dist/.downloads/scramjet/package/dist/types/index.d.ts +11 -0
  68. package/dist/.downloads/scramjet/package/dist/types/shared/cookie.d.ts +26 -0
  69. package/dist/.downloads/scramjet/package/dist/types/shared/headers.d.ts +13 -0
  70. package/dist/.downloads/scramjet/package/dist/types/shared/htmlRules.d.ts +6 -0
  71. package/dist/.downloads/scramjet/package/dist/types/shared/index.d.ts +51 -0
  72. package/dist/.downloads/scramjet/package/dist/types/shared/mime.d.ts +39 -0
  73. package/dist/.downloads/scramjet/package/dist/types/shared/refresh.d.ts +7 -0
  74. package/dist/.downloads/scramjet/package/dist/types/shared/rewriters/css.d.ts +4 -0
  75. package/dist/.downloads/scramjet/package/dist/types/shared/rewriters/html.d.ts +33 -0
  76. package/dist/.downloads/scramjet/package/dist/types/shared/rewriters/index.d.ts +6 -0
  77. package/dist/.downloads/scramjet/package/dist/types/shared/rewriters/js.d.ts +11 -0
  78. package/dist/.downloads/scramjet/package/dist/types/shared/rewriters/url.d.ts +25 -0
  79. package/dist/.downloads/scramjet/package/dist/types/shared/rewriters/wasm.d.ts +7 -0
  80. package/dist/.downloads/scramjet/package/dist/types/shared/rewriters/worker.d.ts +3 -0
  81. package/dist/.downloads/scramjet/package/dist/types/shared/set-cookie-parser.d.ts +20 -0
  82. package/dist/.downloads/scramjet/package/dist/types/shared/snapshot.d.ts +236 -0
  83. package/dist/.downloads/scramjet/package/dist/types/shared/sniffEncoding.d.ts +65 -0
  84. package/dist/.downloads/scramjet/package/dist/types/shared/util.d.ts +2 -0
  85. package/dist/.downloads/scramjet/package/dist/types/symbols.d.ts +6 -0
  86. package/dist/.downloads/scramjet/package/dist/types/types.d.ts +68 -0
  87. package/dist/.downloads/scramjet/package/lib/index.cjs +7 -0
  88. package/dist/.downloads/scramjet/package/lib/index.d.ts +8 -0
  89. package/dist/.downloads/scramjet/package/lib/types.d.ts +20 -0
  90. package/dist/.downloads/scramjet/package/package.json +93 -0
  91. package/dist/bootstrap-client.js +169 -0
  92. package/dist/bootstrap-client.js.map +1 -0
  93. package/dist/bootstrap-server.js +406 -0
  94. package/dist/bootstrap-server.js.map +1 -0
  95. package/dist/bootstrap-static.js +476 -0
  96. package/dist/bootstrap-static.js.map +1 -0
  97. package/dist/types/client.d.ts +4 -0
  98. package/dist/types/clientcommon.d.ts +2 -0
  99. package/dist/types/common.d.ts +30 -0
  100. package/dist/types/server.d.ts +24 -0
  101. package/dist/types/static.d.ts +1 -0
  102. package/package.json +30 -0
@@ -0,0 +1,473 @@
1
+ // HTTP cache plugin for ScramjetFetchHandler.
2
+ //
3
+ // Service-worker `fetch` ignores the browser's HTTP cache, so without this
4
+ // every navigation re-runs the full network fetch even for unchanged
5
+ // resources. This plugin caches the **upstream** response (the BareResponse
6
+ // as received from the network, BEFORE rewriteResponseHeaders / rewriteBody
7
+ // run). On a hit we hand that same untouched response to the pipeline, which
8
+ // then re-rewrites with the current Frame's prefix.
9
+ //
10
+ // Storing pre-rewrite means:
11
+ // - The cache is shared across Frames, Controllers, and page reloads --
12
+ // one Frame's hit serves another Frame's request because the stored
13
+ // bytes contain only the upstream's URLs, not any frame-bound prefix.
14
+ // - Redirect Location / Content-Location and Link headers come out of
15
+ // `rewriteResponseHeaders` correctly on each hit, because that runs
16
+ // on the cache-derived response just like a fresh one.
17
+ // - We don't skip the rewriter on hit; we only skip the network. That's
18
+ // where the win actually is for service-worker proxying.
19
+ //
20
+ // Implementation aims for RFC 9111 (HTTP caching) compliance for a
21
+ // PRIVATE cache (browser-local, single-user):
22
+ //
23
+ // - Only GET / HEAD are cached.
24
+ // - Cacheable status codes per RFC 9110 §15.1: 200 203 204 206 300 301 308
25
+ // 404 405 410 414 501. Other statuses pass through.
26
+ // - `Cache-Control: no-store` and `Vary: *` opt out.
27
+ // - Freshness:
28
+ // 1. `Cache-Control: s-maxage` (private cache treats this same as
29
+ // max-age),
30
+ // 2. `Cache-Control: max-age`,
31
+ // 3. `Expires`,
32
+ // 4. heuristic 10% × (Date - Last-Modified) per RFC 9111 §4.2.2.
33
+ // - `Cache-Control: no-cache` / `Pragma: no-cache` / `Cache-Control:
34
+ // immutable` are honoured.
35
+ // - `Vary` is honoured by storing one entry per (URL × selected-headers)
36
+ // pair via the underlying Cache API's built-in matching.
37
+ //
38
+ // 304 revalidation isn't handled here yet -- stale entries fall through to
39
+ // a full refetch. Adding it cleanly requires a hook position that lets us
40
+ // substitute the cached body AFTER the network 304 arrives but BEFORE
41
+ // `rewriteBody` runs, without going through `rewriteBody` again. That can
42
+ // come later.
43
+
44
+ import type * as ScramjetGlobal from "@mercuryworkshop/scramjet";
45
+ import { BareResponse } from "@mercuryworkshop/proxy-transports";
46
+ declare const $scramjet: typeof ScramjetGlobal;
47
+
48
+ export const CACHE_NAME = "scramjet-http-cache-v2";
49
+
50
+ /** Header recording when this entry entered the cache (ms since epoch). */
51
+ const STORED_AT_HEADER = "x-sj-cached-at";
52
+
53
+ /** Status codes RFC 9110 §15.1 marks as "cacheable by default". */
54
+ const DEFAULT_CACHEABLE_STATUSES = new Set([
55
+ 200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501,
56
+ ]);
57
+
58
+ /**
59
+ * Statuses for which the Fetch spec forbids a body. The Response constructor
60
+ * throws TypeError if you pair any of these with a body -- even an empty
61
+ * string or 0-byte buffer.
62
+ */
63
+ const NULL_BODY_STATUSES = new Set([101, 103, 204, 205, 304]);
64
+
65
+ interface CacheControlDirectives {
66
+ "no-store"?: boolean;
67
+ "no-cache"?: boolean;
68
+ "must-revalidate"?: boolean;
69
+ "proxy-revalidate"?: boolean;
70
+ private?: boolean;
71
+ public?: boolean;
72
+ "max-age"?: number;
73
+ "s-maxage"?: number;
74
+ "stale-while-revalidate"?: number;
75
+ "stale-if-error"?: number;
76
+ immutable?: boolean;
77
+ }
78
+
79
+ function parseCacheControl(value: string | null): CacheControlDirectives {
80
+ const out: CacheControlDirectives = {};
81
+ if (!value) return out;
82
+ for (const raw of value.split(",")) {
83
+ const part = raw.trim();
84
+ if (!part) continue;
85
+ const eq = part.indexOf("=");
86
+ const name = (eq === -1 ? part : part.slice(0, eq))
87
+ .trim()
88
+ .toLowerCase() as keyof CacheControlDirectives;
89
+ if (eq === -1) {
90
+ (out as any)[name] = true;
91
+ continue;
92
+ }
93
+ let v = part.slice(eq + 1).trim();
94
+ if (v.startsWith('"') && v.endsWith('"')) v = v.slice(1, -1);
95
+ if (
96
+ name === "max-age" ||
97
+ name === "s-maxage" ||
98
+ name === "stale-while-revalidate" ||
99
+ name === "stale-if-error"
100
+ ) {
101
+ const n = parseInt(v, 10);
102
+ if (Number.isFinite(n) && n >= 0) (out as any)[name] = n;
103
+ } else {
104
+ (out as any)[name] = true;
105
+ }
106
+ }
107
+ return out;
108
+ }
109
+
110
+ /**
111
+ * RFC 9111 §4.2.1 freshness lifetime calculation, simplified for a private
112
+ * cache (so s-maxage is treated identically to max-age).
113
+ */
114
+ function freshnessLifetimeSeconds(
115
+ headers: Headers,
116
+ cc: CacheControlDirectives,
117
+ dateMs: number
118
+ ): number | null {
119
+ if (cc["s-maxage"] !== undefined) return cc["s-maxage"];
120
+ if (cc["max-age"] !== undefined) return cc["max-age"];
121
+
122
+ const expires = headers.get("expires");
123
+ if (expires) {
124
+ const expMs = Date.parse(expires);
125
+ if (Number.isFinite(expMs)) {
126
+ return Math.max(0, (expMs - dateMs) / 1000);
127
+ }
128
+ }
129
+
130
+ const lastModified = headers.get("last-modified");
131
+ if (lastModified) {
132
+ const lmMs = Date.parse(lastModified);
133
+ if (Number.isFinite(lmMs) && lmMs <= dateMs) {
134
+ // RFC 9111 §4.2.2 heuristic: 10% of the time since Last-Modified.
135
+ return ((dateMs - lmMs) * 0.1) / 1000;
136
+ }
137
+ }
138
+
139
+ return null;
140
+ }
141
+
142
+ /** Current age (seconds) of a stored response per RFC 9111 §4.2.3. */
143
+ function currentAgeSeconds(headers: Headers, storedAtMs: number): number {
144
+ const ageHeader = headers.get("age");
145
+ const initialAge = ageHeader ? parseInt(ageHeader, 10) || 0 : 0;
146
+ const residentTime = (Date.now() - storedAtMs) / 1000;
147
+ return initialAge + residentTime;
148
+ }
149
+
150
+ function isCacheableMethod(method: string): boolean {
151
+ return method === "GET" || method === "HEAD";
152
+ }
153
+
154
+ /**
155
+ * Whether a response (status + Cache-Control + Vary) is allowed to be stored.
156
+ * RFC 9110 §15.1 + RFC 9111 §3. `headers` is the upstream's raw response
157
+ * headers, not yet through scramjet's response-header rewriter.
158
+ */
159
+ function responseIsStorable(
160
+ status: number,
161
+ headers: Headers,
162
+ method: string
163
+ ): boolean {
164
+ if (!isCacheableMethod(method)) return false;
165
+ if (!DEFAULT_CACHEABLE_STATUSES.has(status)) return false;
166
+
167
+ const cc = parseCacheControl(headers.get("cache-control"));
168
+ if (cc["no-store"]) return false;
169
+
170
+ // "Vary: *" means "never reusable".
171
+ const vary = headers.get("vary");
172
+ if (vary && vary.split(",").some((v) => v.trim() === "*")) return false;
173
+
174
+ return true;
175
+ }
176
+
177
+ /** Build a synthetic cache-key Request keyed by the *underlying* URL. */
178
+ function buildCacheKeyRequest(
179
+ parsedUrl: string,
180
+ headers: ScramjetGlobal.ScramjetHeaders
181
+ ): Request {
182
+ const native = new Headers();
183
+ for (const [k, v] of headers.toRawHeaders()) {
184
+ try {
185
+ native.append(k, v);
186
+ } catch {}
187
+ }
188
+ const cacheKeyUrl =
189
+ "https://sj-cache.invalid/" + encodeURIComponent(parsedUrl);
190
+ return new Request(cacheKeyUrl, { method: "GET", headers: native });
191
+ }
192
+
193
+ /** Rebuild a Headers object from the BareResponse's rawHeaders array. */
194
+ function nativeHeadersFromRaw(
195
+ raw: ReadonlyArray<readonly [string, string]>
196
+ ): Headers {
197
+ const h = new Headers();
198
+ for (const [k, v] of raw) {
199
+ try {
200
+ h.append(k, v);
201
+ } catch {
202
+ // some upstream headers (e.g. malformed Set-Cookie) are rejected
203
+ // by the native Headers; just drop them.
204
+ }
205
+ }
206
+ return h;
207
+ }
208
+
209
+ /** Strip our internal bookkeeping from a stored Response's headers. */
210
+ function strippedHeadersFromStored(stored: Response): Headers {
211
+ const out = new Headers();
212
+ for (const [k, v] of stored.headers.entries()) {
213
+ if (k.toLowerCase() === STORED_AT_HEADER) continue;
214
+ try {
215
+ out.append(k, v);
216
+ } catch {}
217
+ }
218
+ return out;
219
+ }
220
+
221
+ /**
222
+ * Turn an upstream BareResponse into a BareResponse that:
223
+ * - has the same headers/status/statusText
224
+ * - has its body replaced with a buffered ArrayBuffer (so the pipeline can
225
+ * read it again after we've consumed the original stream for the cache)
226
+ * Returns the buffered bytes too so the caller can hand them off elsewhere.
227
+ */
228
+ async function rebuildBareResponseWithBuffer(
229
+ bare: BareResponse
230
+ ): Promise<{ replacement: BareResponse; bodyBuffer: ArrayBuffer | null }> {
231
+ const status = bare.status;
232
+ const isNullBody = NULL_BODY_STATUSES.has(status);
233
+
234
+ const headers = nativeHeadersFromRaw(bare.rawHeaders);
235
+
236
+ if (isNullBody) {
237
+ return {
238
+ replacement: BareResponse.fromNativeResponse(
239
+ new Response(null, {
240
+ status,
241
+ statusText: bare.statusText,
242
+ headers,
243
+ })
244
+ ),
245
+ bodyBuffer: null,
246
+ };
247
+ }
248
+
249
+ const buf = await bare.arrayBuffer();
250
+ return {
251
+ replacement: BareResponse.fromNativeResponse(
252
+ new Response(buf, {
253
+ status,
254
+ statusText: bare.statusText,
255
+ headers,
256
+ })
257
+ ),
258
+ bodyBuffer: buf,
259
+ };
260
+ }
261
+
262
+ /**
263
+ * Build a `Response` to put in the Cache API. Tags it with our internal
264
+ * STORED_AT_HEADER so freshness can be computed on later lookups.
265
+ */
266
+ function buildStorableResponse(
267
+ body: ArrayBuffer | null,
268
+ status: number,
269
+ statusText: string,
270
+ rawHeaders: ReadonlyArray<readonly [string, string]>
271
+ ): Response {
272
+ const native = nativeHeadersFromRaw(rawHeaders);
273
+ native.set(STORED_AT_HEADER, String(Date.now()));
274
+ return new Response(NULL_BODY_STATUSES.has(status) ? null : body, {
275
+ status,
276
+ statusText,
277
+ headers: native,
278
+ });
279
+ }
280
+
281
+ export interface HttpCachePluginOptions {
282
+ /** Name of the underlying Cache API entry. Defaults to CACHE_NAME. */
283
+ cacheName?: string;
284
+ }
285
+
286
+ /**
287
+ * RFC-9111-ish HTTP cache for ScramjetFetchHandler. Subclasses
288
+ * `$scramjet.Plugin` so it composes with the same hook plumbing every other
289
+ * scramjet plugin uses; `install(target)` wires it onto a Frame (or any
290
+ * object exposing a `fetchHandler`), and `bust()` drops the underlying
291
+ * `caches` entry.
292
+ *
293
+ * One instance can be installed onto multiple Frames -- the WeakMap of
294
+ * "did this request come from cache?" book-keeping is per-instance, not
295
+ * per-Frame, so nothing leaks across installs.
296
+ */
297
+ export class HttpCachePlugin extends $scramjet.Plugin {
298
+ readonly cacheName: string;
299
+
300
+ private cachePromise: Promise<Cache> | null = null;
301
+ // Marks requests whose `earlyResponse` we sourced from the cache, so the
302
+ // preresponse hook below knows not to re-store them. WeakMap keys are
303
+ // the request objects so entries clean themselves up automatically.
304
+ private cameFromCache = new WeakMap<
305
+ ScramjetGlobal.ScramjetFetchRequest,
306
+ true
307
+ >();
308
+
309
+ constructor(options: HttpCachePluginOptions = {}) {
310
+ super("scramjet-http-cache");
311
+ this.cacheName = options.cacheName ?? CACHE_NAME;
312
+ }
313
+
314
+ /** Lazy-open the underlying Cache. Memoized for the plugin's lifetime. */
315
+ private openCache(): Promise<Cache> {
316
+ if (!this.cachePromise) {
317
+ this.cachePromise = caches.open(this.cacheName);
318
+ }
319
+ return this.cachePromise;
320
+ }
321
+
322
+ /**
323
+ * Wire the cache up to a Frame (or anything exposing `fetchHandler`).
324
+ * Safe to call multiple times across different Frames.
325
+ */
326
+ install(target: { fetchHandler: ScramjetGlobal.ScramjetFetchHandler }): void {
327
+ const hooks = target.fetchHandler.hooks.fetch;
328
+
329
+ // ----- request: cache lookup --------------------------------------
330
+ this.tap(hooks.request, async (ctx, props) => {
331
+ const req = ctx.request;
332
+ if (!isCacheableMethod(req.method)) return;
333
+ const reqCache = req.cache as string;
334
+ // Honour the request's own cache mode where it asks for fresh data.
335
+ if (reqCache === "no-store" || reqCache === "reload") return;
336
+ // Don't undo an earlyResponse another plugin already set.
337
+ if (props.earlyResponse) return;
338
+
339
+ const cache = await this.openCache();
340
+ const stored = await cache.match(
341
+ buildCacheKeyRequest(ctx.parsed.url.href, req.initialHeaders)
342
+ );
343
+ if (!stored) {
344
+ return;
345
+ }
346
+
347
+ const storedAt = parseInt(
348
+ stored.headers.get(STORED_AT_HEADER) ?? "0",
349
+ 10
350
+ );
351
+ const cc = parseCacheControl(stored.headers.get("cache-control"));
352
+
353
+ const pragmaNoCache = (stored.headers.get("pragma") ?? "")
354
+ .toLowerCase()
355
+ .includes("no-cache");
356
+ const mustRevalidateBeforeUse =
357
+ cc["no-cache"] === true || pragmaNoCache || reqCache === "no-cache";
358
+
359
+ const dateMs = (() => {
360
+ const d = stored.headers.get("date");
361
+ if (d) {
362
+ const v = Date.parse(d);
363
+ if (Number.isFinite(v)) return v;
364
+ }
365
+ return storedAt || Date.now();
366
+ })();
367
+
368
+ const lifetime = freshnessLifetimeSeconds(stored.headers, cc, dateMs);
369
+ const age = currentAgeSeconds(stored.headers, storedAt);
370
+ const fresh =
371
+ !mustRevalidateBeforeUse && lifetime !== null && age < lifetime;
372
+
373
+ // `immutable` short-circuits the freshness check (RFC 8246)
374
+ // provided the client hasn't asked for a forced revalidation.
375
+ const immutable =
376
+ cc.immutable === true &&
377
+ reqCache !== "no-cache" &&
378
+ reqCache !== "reload";
379
+
380
+ if (!fresh && !immutable) {
381
+ // Stale; fall through to the network. (TODO: 304 revalidation.)
382
+ return;
383
+ }
384
+
385
+ // Build a BareResponse around the stored bytes/headers and hand
386
+ // it to doNetworkFetch via earlyResponse. The pipeline will then
387
+ // run rewriteResponseHeaders/rewriteBody/etc. as if we'd just
388
+ // fetched it.
389
+ const headers = strippedHeadersFromStored(stored);
390
+ // Recompute Age the consumer sees so it isn't stuck at storage
391
+ // time.
392
+ if (storedAt) {
393
+ headers.set("age", String(Math.floor((Date.now() - storedAt) / 1000)));
394
+ }
395
+
396
+ const isNullBody = NULL_BODY_STATUSES.has(stored.status);
397
+ const earlyBody = isNullBody ? null : await stored.arrayBuffer();
398
+
399
+ const earlyResponse = BareResponse.fromNativeResponse(
400
+ new Response(earlyBody, {
401
+ status: stored.status,
402
+ statusText: stored.statusText,
403
+ headers,
404
+ })
405
+ );
406
+
407
+ this.cameFromCache.set(req, true);
408
+ props.earlyResponse = earlyResponse;
409
+ });
410
+
411
+ // ----- preresponse: cache store -----------------------------------
412
+ this.tap(hooks.preresponse, async (ctx, props) => {
413
+ const req = ctx.request;
414
+ // Skip if this body came back via cache.match -- restoring it
415
+ // would just rewrite the same bytes with a fresh STORED_AT_HEADER
416
+ // (resetting the freshness clock).
417
+ if (this.cameFromCache.has(req)) {
418
+ this.cameFromCache.delete(req);
419
+ return;
420
+ }
421
+
422
+ if ((req.cache as string) === "no-store") return;
423
+ if (!isCacheableMethod(req.method)) return;
424
+
425
+ const headers = nativeHeadersFromRaw(props.response.rawHeaders);
426
+ if (!responseIsStorable(props.response.status, headers, req.method))
427
+ return;
428
+
429
+ // Drain the stream once and rebuild the BareResponse around the
430
+ // buffered copy so the rest of doHandleFetch can still read it.
431
+ const { replacement, bodyBuffer } = await rebuildBareResponseWithBuffer(
432
+ props.response
433
+ );
434
+ props.response = replacement;
435
+
436
+ const cacheKey = buildCacheKeyRequest(
437
+ ctx.parsed.url.href,
438
+ req.initialHeaders
439
+ );
440
+ const toStore = buildStorableResponse(
441
+ bodyBuffer,
442
+ props.response.status,
443
+ props.response.statusText,
444
+ props.response.rawHeaders
445
+ );
446
+
447
+ try {
448
+ const cache = await this.openCache();
449
+ await cache.put(cacheKey, toStore);
450
+ } catch (err) {
451
+ // Cache.put can fail on opaque or oddly-headered responses;
452
+ // don't let a cache write failure break the actual fetch.
453
+ console.warn("[scramjet-http-cache] cache.put failed:", err);
454
+ }
455
+ });
456
+ }
457
+
458
+ /**
459
+ * Drop every entry in the HTTP cache. Returns whether the underlying
460
+ * Cache existed and was deleted.
461
+ */
462
+ async bust(): Promise<boolean> {
463
+ try {
464
+ // Drop the memoized handle too; the next install will re-open
465
+ // against a fresh empty cache.
466
+ this.cachePromise = null;
467
+ return await caches.delete(this.cacheName);
468
+ } catch (err) {
469
+ console.error("[scramjet-http-cache] bust failed:", err);
470
+ return false;
471
+ }
472
+ }
473
+ }