@pwa-platform/sw-runtime 0.1.0-beta.0 → 0.1.0-beta.2

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 (38) hide show
  1. package/dist/build/config.d.ts.map +1 -1
  2. package/dist/build/config.js +35 -2
  3. package/dist/build/config.js.map +1 -1
  4. package/dist/messages/index.d.ts +30 -0
  5. package/dist/messages/index.d.ts.map +1 -1
  6. package/dist/messages/index.js +32 -0
  7. package/dist/messages/index.js.map +1 -1
  8. package/dist/recovery-worker/index.d.ts.map +1 -1
  9. package/dist/recovery-worker/index.js +22 -5
  10. package/dist/recovery-worker/index.js.map +1 -1
  11. package/dist/shared/config.d.ts +33 -0
  12. package/dist/shared/config.d.ts.map +1 -1
  13. package/dist/shared/config.js +67 -1
  14. package/dist/shared/config.js.map +1 -1
  15. package/dist/shared/expiration-records.d.ts +9 -0
  16. package/dist/shared/expiration-records.d.ts.map +1 -0
  17. package/dist/shared/expiration-records.js +85 -0
  18. package/dist/shared/expiration-records.js.map +1 -0
  19. package/dist/worker/admit.d.ts +8 -0
  20. package/dist/worker/admit.d.ts.map +1 -0
  21. package/dist/worker/admit.js +99 -0
  22. package/dist/worker/admit.js.map +1 -0
  23. package/dist/worker/decide.d.ts +23 -2
  24. package/dist/worker/decide.d.ts.map +1 -1
  25. package/dist/worker/decide.js +40 -11
  26. package/dist/worker/decide.js.map +1 -1
  27. package/dist/worker/handlers.d.ts +7 -2
  28. package/dist/worker/handlers.d.ts.map +1 -1
  29. package/dist/worker/handlers.js +247 -10
  30. package/dist/worker/handlers.js.map +1 -1
  31. package/dist/worker/index.d.ts.map +1 -1
  32. package/dist/worker/index.js +2 -2
  33. package/dist/worker/index.js.map +1 -1
  34. package/dist/worker/runtime-cleanup.d.ts +14 -0
  35. package/dist/worker/runtime-cleanup.d.ts.map +1 -0
  36. package/dist/worker/runtime-cleanup.js +23 -0
  37. package/dist/worker/runtime-cleanup.js.map +1 -0
  38. package/package.json +4 -4
@@ -0,0 +1,99 @@
1
+ // Runtime-cache response admission check, as a pure function: no Cache API, no fetch, no Workbox. It only decides
2
+ // whether a network response may be written to the runtime cache; the caller still returns the response to the page
3
+ // either way (spec "响应准入").
4
+ const ALLOWED_VARY_TOKENS = new Set(["accept", "accept-encoding"]);
5
+ /** True only if `response` may be written to the runtime cache. Never consumes `response` itself. */
6
+ export async function admitRuntimeResponse(response, options) {
7
+ if (response.type !== "basic" || response.status !== 200 || response.redirected)
8
+ return false;
9
+ if (!admittedMediaType(response.headers.get("content-type"), options.resourceClass))
10
+ return false;
11
+ if (!admittedCacheControl(response.headers.get("cache-control"), options.strategy))
12
+ return false;
13
+ if (!admittedVary(response.headers.get("vary")))
14
+ return false;
15
+ // `Set-Cookie` is intentionally not checked: the Fetch spec makes it a forbidden response-header name on a basic
16
+ // response, so a worker can never read it (see spec.public-read-cache "响应准入").
17
+ return admittedSize(response, options.maxEntryBytes);
18
+ }
19
+ function admittedMediaType(contentType, resourceClass) {
20
+ if (contentType === null)
21
+ return false;
22
+ const mediaType = contentType.split(";", 1)[0].trim().toLowerCase();
23
+ if (resourceClass === "navigation-public-dynamic")
24
+ return mediaType === "text/html";
25
+ return mediaType === "application/json" || /^application\/[^/]+\+json$/.test(mediaType);
26
+ }
27
+ /** Each directive token, with its value when the token carries one (`max-age=0` and `max-age="0"` both parse to `0`). */
28
+ function directives(headerValue) {
29
+ const found = new Map();
30
+ for (const item of headerValue.split(",")) {
31
+ const [rawToken, ...rawValue] = item.split("=");
32
+ const token = rawToken?.trim().toLowerCase();
33
+ if (token === undefined || token.length === 0)
34
+ continue;
35
+ const value = rawValue.length === 0 ? undefined : rawValue.join("=").trim().replace(/^"|"$/g, "");
36
+ found.set(token, value);
37
+ }
38
+ return found;
39
+ }
40
+ /** True when `directive` is present with a numeric value of exactly `0` (e.g. `max-age=0`, `max-age="0"`). */
41
+ function isZero(found, directive) {
42
+ const value = found.get(directive);
43
+ return value !== undefined && Number(value) === 0;
44
+ }
45
+ function admittedCacheControl(cacheControl, strategy) {
46
+ if (cacheControl === null)
47
+ return true;
48
+ const found = directives(cacheControl);
49
+ if (found.has("no-store") || found.has("private"))
50
+ return false;
51
+ if (strategy === "stale-while-revalidate") {
52
+ // SWR hands out the cached entry without revalidating; a response that demands revalidation, or one that is
53
+ // already stale on arrival, must not be written (spec "响应准入", ADR-0035 T13 评审后收紧). network-first is
54
+ // unaffected: it always tries the network first and only falls back to the cache on failure.
55
+ if (found.has("no-cache") || found.has("must-revalidate"))
56
+ return false;
57
+ if (isZero(found, "max-age") || isZero(found, "s-maxage"))
58
+ return false;
59
+ }
60
+ return true;
61
+ }
62
+ function admittedVary(vary) {
63
+ if (vary === null)
64
+ return true;
65
+ const tokens = vary
66
+ .split(",")
67
+ .map((token) => token.trim().toLowerCase())
68
+ .filter((token) => token.length > 0);
69
+ return tokens.every((token) => ALLOWED_VARY_TOKENS.has(token));
70
+ }
71
+ async function admittedSize(response, maxEntryBytes) {
72
+ const contentLength = response.headers.get("content-length");
73
+ if (contentLength !== null) {
74
+ const declared = Number(contentLength);
75
+ if (Number.isInteger(declared) && declared >= 0 && declared > maxEntryBytes)
76
+ return false;
77
+ }
78
+ const body = response.clone().body;
79
+ if (body === null)
80
+ return true;
81
+ const reader = body.getReader();
82
+ let total = 0;
83
+ for (;;) {
84
+ const { done, value } = await reader.read();
85
+ if (done) {
86
+ reader.releaseLock();
87
+ return true;
88
+ }
89
+ total += value.length;
90
+ if (total > maxEntryBytes) {
91
+ // Stop reading immediately rather than draining the rest of an oversized body. `cancel()` is fire-and-forget:
92
+ // on a cloned response's tee'd stream, Node's undici never settles this promise even though the cancel signal
93
+ // itself takes effect right away (no further reads happen upstream), so awaiting it would hang forever.
94
+ reader.cancel().catch(() => { });
95
+ return false;
96
+ }
97
+ }
98
+ }
99
+ //# sourceMappingURL=admit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admit.js","sourceRoot":"","sources":["../../src/worker/admit.ts"],"names":[],"mappings":"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"}
@@ -20,9 +20,15 @@ export type PwaPassthroughReason =
20
20
  * A manifest-hit, non-navigation request that carries a `Range` header (any value). The precache stores full
21
21
  * responses, so `engine.match` cannot answer with a genuine partial response; the network answers instead
22
22
  * (ADR-0023). Only used where the decision would otherwise be `precache`: a denied, excluded, unclassified or
23
- * not-precached request keeps its own reason.
23
+ * not-precached request keeps its own reason. Also used for a `Range` request under a runtime-cache `public-data`
24
+ * rule, for the same reason.
24
25
  */
25
- | "range";
26
+ | "range"
27
+ /**
28
+ * A non-navigation request under a runtime-cache `public-data` rule that carries an `Authorization` header. The
29
+ * runtime cache never reads or writes a request whose response could be credential-specific (spec "请求判断").
30
+ */
31
+ | "authorization";
26
32
  export type PwaRequestDecision =
27
33
  /** The worker does not call `respondWith`: the request behaves exactly as it would without a worker. */
28
34
  {
@@ -41,6 +47,19 @@ export type PwaRequestDecision =
41
47
  | {
42
48
  readonly kind: "navigate";
43
49
  readonly fallbacks: readonly string[];
50
+ }
51
+ /**
52
+ * The request matches a compiled runtime-cache rule: `cache: "pages"` for a navigation under a
53
+ * `navigation-public-dynamic` rule (whose `fallbacks` are exactly what a `navigate` decision for the same request
54
+ * would carry), `cache: "data"` for a non-navigation request under a `public-data` rule (`fallbacks: []`, the page
55
+ * gets a network error when nothing cached answers).
56
+ */
57
+ | {
58
+ readonly kind: "runtime";
59
+ readonly cache: "pages" | "data";
60
+ readonly strategy: "network-first" | "stale-while-revalidate";
61
+ readonly resourceClass: "public-data" | "navigation-public-dynamic";
62
+ readonly fallbacks: readonly string[];
44
63
  };
45
64
  export type PwaRequestInput = {
46
65
  readonly method: string;
@@ -50,6 +69,8 @@ export type PwaRequestInput = {
50
69
  readonly navigation: boolean;
51
70
  /** `request.headers.has("range")`, whatever the header's value. Ignored for navigations (ADR-0023). */
52
71
  readonly range: boolean;
72
+ /** `request.headers.has("authorization")`, whatever the header's value. Only acted on for a `public-data` rule. */
73
+ readonly authorization: boolean;
53
74
  };
54
75
  export type PwaRouterOptions = {
55
76
  readonly config: PwaPlatformWorkerConfig;
@@ -1 +1 @@
1
- {"version":3,"file":"decide.d.ts","sourceRoot":"","sources":["../../src/worker/decide.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAGnE,MAAM,MAAM,oBAAoB;AAC9B,+DAA+D;AAC7D,SAAS,GACT,cAAc;AAChB;;;GAGG;GACD,UAAU;AACZ;;;GAGG;GACD,QAAQ,GACR,cAAc;AAChB,8FAA8F;GAC5F,eAAe;AACjB,qCAAqC;GACnC,YAAY;AACd;;;;;GAKG;GACD,OAAO,CAAC;AAEZ,MAAM,MAAM,kBAAkB;AAC5B,wGAAwG;AACtG;IAAE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAA;CAAE;AACzE,sDAAsD;GACpD;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAAE;AAC7D;;;GAGG;GACD;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAAC;AAEzE,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,4BAA4B;IAC5B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,qCAAqC;IACrC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,uGAAuG;IACvG,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,MAAM,EAAE,uBAAuB,CAAC;IACzC,yFAAyF;IACzF,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,wEAAwE;IACxE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IAAE,MAAM,CAAC,OAAO,EAAE,eAAe,GAAG,kBAAkB,CAAA;CAAE,CAAC;AAIjF,wBAAgB,YAAY,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,EAAE,gBAAgB,GAAG,SAAS,CAmC1F"}
1
+ {"version":3,"file":"decide.d.ts","sourceRoot":"","sources":["../../src/worker/decide.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,uBAAuB,EAA6B,MAAM,qBAAqB,CAAC;AAG9F,MAAM,MAAM,oBAAoB;AAC9B,+DAA+D;AAC7D,SAAS,GACT,cAAc;AAChB;;;GAGG;GACD,UAAU;AACZ;;;GAGG;GACD,QAAQ,GACR,cAAc;AAChB,8FAA8F;GAC5F,eAAe;AACjB,qCAAqC;GACnC,YAAY;AACd;;;;;;GAMG;GACD,OAAO;AACT;;;GAGG;GACD,eAAe,CAAC;AAEpB,MAAM,MAAM,kBAAkB;AAC5B,wGAAwG;AACtG;IAAE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAA;CAAE;AACzE,sDAAsD;GACpD;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAAE;AAC7D;;;GAGG;GACD;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE;AACtE;;;;;GAKG;GACD;IACE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC;IACjC,QAAQ,CAAC,QAAQ,EAAE,eAAe,GAAG,wBAAwB,CAAC;IAC9D,QAAQ,CAAC,aAAa,EAAE,aAAa,GAAG,2BAA2B,CAAC;IACpE,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC,CAAC;AAEN,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,4BAA4B;IAC5B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,qCAAqC;IACrC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,uGAAuG;IACvG,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,mHAAmH;IACnH,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,MAAM,EAAE,uBAAuB,CAAC;IACzC,yFAAyF;IACzF,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,wEAAwE;IACxE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IAAE,MAAM,CAAC,OAAO,EAAE,eAAe,GAAG,kBAAkB,CAAA;CAAE,CAAC;AAIjF,wBAAgB,YAAY,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,EAAE,gBAAgB,GAAG,SAAS,CA8D1F"}
@@ -3,8 +3,12 @@ const PASSTHROUGH = (reason) => ({ kind: "passthrough", reason });
3
3
  export function createRouter({ config, manifestUrls, origin }) {
4
4
  const matcher = createPathMatcher(config.pathRules);
5
5
  const manifest = new Set(manifestUrls);
6
+ // Keyed by pathPrefix, the same key `matcher` resolves on: `config.runtimeCache.rules` is compiled from the same
7
+ // ordered path rules, filtered to the ones the runtime cache executes, so a pathPrefix match here is always the
8
+ // exact rule `matcher` itself would have picked — no second longest-prefix search is needed.
9
+ const runtimeRules = new Map(config.runtimeCache.enabled ? config.runtimeCache.rules.map((rule) => [rule.pathPrefix, rule]) : []);
6
10
  return {
7
- decide({ method, url, navigation, range }) {
11
+ decide({ method, url, navigation, range, authorization }) {
8
12
  if (method !== "GET")
9
13
  return PASSTHROUGH("non-get");
10
14
  if (!URL.canParse(url))
@@ -27,13 +31,32 @@ export function createRouter({ config, manifestUrls, origin }) {
27
31
  const offline = navigation ? offlineFallback(config, manifest) : undefined;
28
32
  return offline === undefined ? PASSTHROUGH("denied") : { kind: "navigate", fallbacks: [offline] };
29
33
  }
34
+ const runtimeRule = runtimeRules.get(rule.pathPrefix);
35
+ if (navigation) {
36
+ const fallbacks = navigationFallbacks(config, manifest, requested);
37
+ // Only a navigation-public-dynamic rule turns a navigation into a runtime decision; a navigation under a
38
+ // public-data rule behaves exactly as it does without runtime caching.
39
+ if (runtimeRule !== undefined && runtimeRule.resourceClass === "navigation-public-dynamic") {
40
+ return { kind: "runtime", cache: "pages", strategy: runtimeRule.strategy, resourceClass: runtimeRule.resourceClass, fallbacks };
41
+ }
42
+ return { kind: "navigate", fallbacks };
43
+ }
30
44
  // The manifest is the precache's membership list, so the decision stays synchronous; the query string is part
31
- // of the key, so `?utm=…` variants of a precached URL are not precached themselves.
45
+ // of the key, so `?utm=…` variants of a precached URL are not precached themselves. A manifest hit always
46
+ // wins over the runtime cache.
32
47
  const exact = `${requested.pathname}${requested.search}`;
33
- if (navigation)
34
- return { kind: "navigate", fallbacks: navigationFallbacks(config, manifest, requested) };
35
- if (!manifest.has(exact))
48
+ if (!manifest.has(exact)) {
49
+ // Only a public-data rule turns a non-navigation request into a runtime decision; a non-navigation request
50
+ // under a navigation-public-dynamic (page) rule behaves exactly as it does without runtime caching.
51
+ if (runtimeRule !== undefined && runtimeRule.resourceClass === "public-data") {
52
+ if (range)
53
+ return PASSTHROUGH("range");
54
+ if (authorization)
55
+ return PASSTHROUGH("authorization");
56
+ return { kind: "runtime", cache: "data", strategy: runtimeRule.strategy, resourceClass: runtimeRule.resourceClass, fallbacks: [] };
57
+ }
36
58
  return PASSTHROUGH("not-precached");
59
+ }
37
60
  // The precache holds full responses, so a Range request would otherwise get a full 200 body instead of a 206;
38
61
  // stepping aside here lets the network answer it directly (ADR-0023). Navigations never look at range.
39
62
  return range ? PASSTHROUGH("range") : { kind: "precache", manifestUrl: exact };
@@ -41,18 +64,24 @@ export function createRouter({ config, manifestUrls, origin }) {
41
64
  };
42
65
  }
43
66
  /**
44
- * What an offline navigation may fall back to, in order: the requested URL itself, the same route's `index.html`,
45
- * and finally the plan's offline fallback page. Nothing else: no other route's cached content may answer a
46
- * navigation.
67
+ * What an offline navigation may fall back to, in order: the requested URL itself (query string intact), the same
68
+ * route with the query string dropped, that route's `index.html` (query string dropped), and finally the plan's
69
+ * offline fallback page. Nothing else: no other route's cached content may answer a navigation.
70
+ *
71
+ * The exact, query-preserving candidate is tried first so a precached query-string variant still wins when one
72
+ * exists. Only once that misses does the query string get dropped (ADR-0034, 2026-09-23 amendment): a decorated URL
73
+ * — for example a return-path query on an entry-recovery link — used to skip its own precached document and land on
74
+ * the offline page solely because of its query string. Dropping the query string on the fallback candidates lets it
75
+ * reach that document instead, while still never answering with a different route's cached content.
47
76
  *
48
77
  * The route's `index.html` is tried for URLs with and without a trailing slash. Frameworks that prerender write
49
78
  * `about/index.html` but link to `/about` (Nuxt does by default), and a static host serves both spellings from that
50
- * one file, so both name the same route (ADR-0012, 2026-09-17 amendment). The query string stays part of every key.
79
+ * one file, so both name the same route (ADR-0012, 2026-09-17 amendment).
51
80
  */
52
81
  function navigationFallbacks(config, manifest, requested) {
53
82
  const { pathname, search } = requested;
54
- const candidates = [`${pathname}${search}`];
55
- candidates.push(pathname.endsWith("/") ? `${pathname}index.html${search}` : `${pathname}/index.html${search}`);
83
+ const candidates = [`${pathname}${search}`, pathname];
84
+ candidates.push(pathname.endsWith("/") ? `${pathname}index.html` : `${pathname}/index.html`);
56
85
  const offline = offlineFallback(config, manifest);
57
86
  if (offline !== undefined)
58
87
  candidates.push(offline);
@@ -1 +1 @@
1
- {"version":3,"file":"decide.js","sourceRoot":"","sources":["../../src/worker/decide.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"decide.js","sourceRoot":"","sources":["../../src/worker/decide.ts"],"names":[],"mappings":"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"}
@@ -1,10 +1,15 @@
1
- import type { PwaPrecacheEngine } from "@pwa-platform/engine-workbox/worker";
1
+ import type { createRuntimeCacheEngine, PwaPrecacheEngine } from "@pwa-platform/engine-workbox/worker";
2
2
  import type { PwaPlatformWorkerConfig } from "../shared/config.js";
3
3
  export type PwaPlatformWorkerHandlers = {
4
4
  readonly scope: ServiceWorkerGlobalScope;
5
5
  /** Already validated by `registerPlatformWorker`. */
6
6
  readonly config: PwaPlatformWorkerConfig;
7
7
  readonly engine: PwaPrecacheEngine;
8
+ /**
9
+ * Builds a runtime-cache engine. Required whenever `config.runtimeCache.enabled` is true; `registerPlatformWorker`
10
+ * always supplies the real Workbox-backed factory. Tests inject a fake so they never need to load Workbox.
11
+ */
12
+ readonly createRuntimeCacheEngine?: typeof createRuntimeCacheEngine;
8
13
  };
9
14
  /**
10
15
  * Registers `install`, `activate`, `fetch`, `message`, `push` and `notificationclick`, and nothing else. The worker
@@ -12,5 +17,5 @@ export type PwaPlatformWorkerHandlers = {
12
17
  * `notificationclick` are always on: see ADR-0021 for why a fixed pair of listeners has no observable effect on an
13
18
  * application that never subscribes to push.
14
19
  */
15
- export declare function attachPlatformWorker({ scope, config, engine }: PwaPlatformWorkerHandlers): void;
20
+ export declare function attachPlatformWorker({ scope, config, engine, createRuntimeCacheEngine: runtimeCacheEngineFactory, }: PwaPlatformWorkerHandlers): void;
16
21
  //# sourceMappingURL=handlers.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/worker/handlers.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qCAAqC,CAAC;AAI7E,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAOnE,MAAM,MAAM,yBAAyB,GAAG;IACtC,QAAQ,CAAC,KAAK,EAAE,wBAAwB,CAAC;IACzC,qDAAqD;IACrD,QAAQ,CAAC,MAAM,EAAE,uBAAuB,CAAC;IACzC,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;CACpC,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,yBAAyB,GAAG,IAAI,CAmE/F"}
1
+ {"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/worker/handlers.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,wBAAwB,EAAE,iBAAiB,EAAyB,MAAM,qCAAqC,CAAC;AAI9H,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAUnE,MAAM,MAAM,yBAAyB,GAAG;IACtC,QAAQ,CAAC,KAAK,EAAE,wBAAwB,CAAC;IACzC,qDAAqD;IACrD,QAAQ,CAAC,MAAM,EAAE,uBAAuB,CAAC;IACzC,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACnC;;;OAGG;IACH,QAAQ,CAAC,wBAAwB,CAAC,EAAE,OAAO,wBAAwB,CAAC;CACrE,CAAC;AASF;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,KAAK,EACL,MAAM,EACN,MAAM,EACN,wBAAwB,EAAE,yBAAyB,GACpD,EAAE,yBAAyB,GAAG,IAAI,CAyFlC"}
@@ -1,20 +1,28 @@
1
- import { isOfflineWriteMessage, isSkipWaitingMessage } from "../messages/index.js";
1
+ import { isOfflineWriteMessage, isRuntimeCachePendingMessage, isSkipWaitingMessage } from "../messages/index.js";
2
2
  import { validatePushPayload } from "../push-payload/index.js";
3
+ import { admitRuntimeResponse } from "./admit.js";
3
4
  import { createRouter } from "./decide.js";
4
5
  import { validateOfflineWriteEnqueue, validateOfflineWriteFlush } from "./offline-write-intent.js";
5
6
  import { flushOfflineWrites } from "./offline-write-flush.js";
6
7
  import { createOfflineWriteStore } from "./offline-write-store.js";
7
8
  import { resolveNotificationTarget } from "./notification-target.js";
9
+ import { currentDataCacheName, deleteRuntimeCaches } from "./runtime-cleanup.js";
10
+ const PENDING_SIGNAL_LIMIT = 16;
11
+ const PENDING_SIGNAL_TTL_MS = 30_000;
8
12
  /**
9
13
  * Registers `install`, `activate`, `fetch`, `message`, `push` and `notificationclick`, and nothing else. The worker
10
14
  * never calls `skipWaiting` on its own (updates are prompted) and never claims clients. `push` and
11
15
  * `notificationclick` are always on: see ADR-0021 for why a fixed pair of listeners has no observable effect on an
12
16
  * application that never subscribes to push.
13
17
  */
14
- export function attachPlatformWorker({ scope, config, engine }) {
18
+ export function attachPlatformWorker({ scope, config, engine, createRuntimeCacheEngine: runtimeCacheEngineFactory, }) {
15
19
  const origin = new URL(scope.location.href).origin;
16
20
  const router = createRouter({ config, manifestUrls: engine.urls(), origin });
17
21
  const offlineWriteStore = config.offlineWrites.enabled ? createOfflineWriteStore(config.offlineWrites) : undefined;
22
+ const runtimeEngines = config.runtimeCache.enabled
23
+ ? buildRuntimeEngines(config.runtimeCache, config.networkTimeoutSeconds, requireRuntimeCacheEngineFactory(runtimeCacheEngineFactory))
24
+ : undefined;
25
+ const pendingSignals = createPendingSignalStore();
18
26
  scope.addEventListener("install", (event) => {
19
27
  // The engine passes its work to event.waitUntil, so a failed download fails the install; the rejection is only
20
28
  // silenced here to keep it out of the worker's unhandled-rejection log.
@@ -24,6 +32,11 @@ export function attachPlatformWorker({ scope, config, engine }) {
24
32
  // The cleanup itself already reached event.waitUntil, so a rejection here carries only the engine's diagnostic
25
33
  // signal — it fails loudly when a Workbox update changes the shape of its result. Let that surface.
26
34
  void engine.activate(event);
35
+ // Every new activation removes runtime caches this config no longer wants: the pages cache outright (stale HTML
36
+ // could reference resources the new precache dropped), and every data cache but the current digest's — all of
37
+ // them when the runtime cache is disabled (spec "缓存命名与清理"). Activation cannot fail because of this cleanup:
38
+ // a failure here has nowhere useful to surface and must not become an unhandled rejection on the activate event.
39
+ event.waitUntil(deleteRuntimeCaches(scope, config.runtimeCache, currentDataCacheName(config.runtimeCache)).catch(() => undefined));
27
40
  });
28
41
  scope.addEventListener("fetch", (event) => {
29
42
  const decision = router.decide({
@@ -32,6 +45,7 @@ export function attachPlatformWorker({ scope, config, engine }) {
32
45
  navigation: event.request.mode === "navigate",
33
46
  // Any value counts, including "bytes=0-" (ADR-0023); decide() only acts on it for a manifest-hit request.
34
47
  range: event.request.headers.has("range"),
48
+ authorization: event.request.headers.has("authorization"),
35
49
  });
36
50
  if (decision.kind === "passthrough")
37
51
  return;
@@ -39,7 +53,11 @@ export function attachPlatformWorker({ scope, config, engine }) {
39
53
  event.respondWith(fromPrecache(scope, engine, decision.manifestUrl, event.request));
40
54
  return;
41
55
  }
42
- event.respondWith(navigate(scope, engine, decision.fallbacks, event.request));
56
+ if (decision.kind === "runtime") {
57
+ event.respondWith(respondFromRuntimeCache(scope, engine, runtimeEngineFor(runtimeEngines, decision), decision, event, pendingSignals));
58
+ return;
59
+ }
60
+ event.respondWith(navigate(scope, engine, decision.fallbacks, event.request, config.networkTimeoutSeconds));
43
61
  });
44
62
  scope.addEventListener("message", (event) => {
45
63
  if (!isSameOriginWindow(event.source, origin))
@@ -49,13 +67,23 @@ export function attachPlatformWorker({ scope, config, engine }) {
49
67
  event.waitUntil(scope.skipWaiting());
50
68
  return;
51
69
  }
70
+ if (isRuntimeCachePendingMessage(event.data)) {
71
+ const [port, ...rest] = event.ports;
72
+ if (port === undefined || rest.length !== 0)
73
+ return;
74
+ const clientId = windowClientId(event.source);
75
+ if (clientId === undefined)
76
+ return;
77
+ replyPending(port, pendingSignals.take(clientId));
78
+ return;
79
+ }
52
80
  if (!isOfflineWriteMessage(event.data))
53
81
  return;
54
82
  const [port, ...rest] = event.ports;
55
83
  if (port === undefined || rest.length !== 0)
56
84
  return;
57
85
  if (event.data.type === "pwa:offline-write:clear") {
58
- event.waitUntil(clear(event.data, offlineWriteStore, port));
86
+ event.waitUntil(clear(event.data, offlineWriteStore, port, scope, config.runtimeCache));
59
87
  return;
60
88
  }
61
89
  if (offlineWriteStore === undefined)
@@ -83,14 +111,31 @@ export function attachPlatformWorker({ scope, config, engine }) {
83
111
  event.waitUntil(focusOrOpen(scope, target));
84
112
  });
85
113
  }
86
- async function clear(message, store, port) {
114
+ async function clear(message, store, port, scope, runtimeCache) {
87
115
  try {
88
116
  await store?.clear();
89
- replyClear(port, message.requestId);
90
117
  }
91
118
  catch {
92
119
  reply(port, message.requestId, "rejected", "offline-write.storage");
120
+ return;
93
121
  }
122
+ // Every runtime cache goes on logout, current digest included — unlike activation, nothing is kept (spec "登出").
123
+ // A failure here must not reply "cleared" while the runtime cache is enabled: the registration stays in place.
124
+ // When it is disabled, this cleanup is best-effort (ADR-0035 "对 v1/v2 应用的非请求判断影响") — there is normally
125
+ // nothing to delete, and a failure must not give v1/v2 and unenabled apps a new way for logout to fail.
126
+ if (runtimeCache.enabled) {
127
+ try {
128
+ await deleteRuntimeCaches(scope, runtimeCache);
129
+ }
130
+ catch {
131
+ reply(port, message.requestId, "rejected", "offline-write.storage");
132
+ return;
133
+ }
134
+ }
135
+ else {
136
+ await deleteRuntimeCaches(scope, runtimeCache).catch(() => undefined);
137
+ }
138
+ replyClear(port, message.requestId);
94
139
  }
95
140
  async function flush(message, config, origin, store, port, send) {
96
141
  const checked = validateOfflineWriteFlush(message, config);
@@ -171,16 +216,75 @@ async function focusOrOpen(scope, target) {
171
216
  async function fromPrecache(scope, engine, manifestUrl, request) {
172
217
  return (await engine.match(manifestUrl)) ?? (await scope.fetch(request));
173
218
  }
174
- /** Navigation: the network answers whenever it responds at all, including 4xx and 5xx; offline uses the fallbacks. */
175
- async function navigate(scope, engine, fallbacks, request) {
219
+ /**
220
+ * Navigation: the network answers whenever it responds at all, including 4xx and 5xx; offline uses the fallbacks.
221
+ * When `networkTimeoutSeconds` is set (ADR-0038) and the network has not answered within that many seconds, the
222
+ * fallbacks are consulted early: a match is returned immediately and the still-pending network result is left to
223
+ * settle on its own (its rejection is swallowed, and navigate() never writes to a cache either way, so nothing is
224
+ * cached from it). With no matching fallback, navigate() keeps waiting for the network exactly as it would without a
225
+ * timeout — a timeout must never turn a request that would have succeeded into an error.
226
+ */
227
+ async function navigate(scope, engine, fallbacks, request, networkTimeoutSeconds) {
228
+ if (networkTimeoutSeconds === undefined) {
229
+ try {
230
+ const response = await scope.fetch(request);
231
+ if (response.type !== "error")
232
+ return response;
233
+ }
234
+ catch {
235
+ // A rejected fetch is the usual network-failure signal; an error response follows the same fallback path.
236
+ }
237
+ return fallbackResponse(engine, fallbacks);
238
+ }
239
+ const networkPromise = scope.fetch(request);
240
+ let timer;
241
+ // Whichever settles first: the network (success or failure) clears the timer; the timer never rejects, so it
242
+ // cannot itself produce an unhandled rejection.
243
+ const timedOut = await new Promise((resolve) => {
244
+ timer = setTimeout(() => resolve(true), networkTimeoutSeconds * 1000);
245
+ networkPromise.then(() => resolve(false), () => resolve(false));
246
+ });
247
+ if (timer !== undefined)
248
+ clearTimeout(timer);
249
+ if (!timedOut) {
250
+ try {
251
+ const response = await networkPromise;
252
+ if (response.type !== "error")
253
+ return response;
254
+ }
255
+ catch {
256
+ // A rejected fetch is the usual network-failure signal; an error response follows the same fallback path.
257
+ }
258
+ return fallbackResponse(engine, fallbacks);
259
+ }
260
+ // Timed out. The network promise above already has a handler attached (the race's own .then), so its eventual
261
+ // settlement — success or rejection — can never surface as an unhandled rejection; nothing here awaits it while
262
+ // a fallback is used.
263
+ try {
264
+ for (const candidate of fallbacks) {
265
+ const fallback = await engine.match(candidate);
266
+ if (fallback !== undefined)
267
+ return fallback;
268
+ }
269
+ }
270
+ catch {
271
+ // engine.match rejected: fall through to waiting on the network below rather than turning a possibly-
272
+ // successful request into an error (the same rule as "no fallback available").
273
+ }
274
+ // No fallback available: keep waiting for the network rather than turning a possibly-successful request into an
275
+ // error (spec "一个都没有时,继续等网络"), then apply the same rules as a request that answered in time.
176
276
  try {
177
- const response = await scope.fetch(request);
277
+ const response = await networkPromise;
178
278
  if (response.type !== "error")
179
279
  return response;
180
280
  }
181
281
  catch {
182
- // A rejected fetch is the usual network-failure signal; an error response follows the same fallback path.
282
+ // Falls through to fallbackResponse, which finds nothing (again) and returns Response.error().
183
283
  }
284
+ return fallbackResponse(engine, fallbacks);
285
+ }
286
+ /** The first precached fallback that exists, in order, or a network error when none does. */
287
+ async function fallbackResponse(engine, fallbacks) {
184
288
  for (const candidate of fallbacks) {
185
289
  const response = await engine.match(candidate);
186
290
  if (response !== undefined)
@@ -193,4 +297,137 @@ function isSameOriginWindow(source, origin) {
193
297
  return false;
194
298
  return URL.canParse(source.url) && new URL(source.url).origin === origin;
195
299
  }
300
+ /** The requesting window client's id, read without invoking an accessor other than the plain property itself. */
301
+ function windowClientId(source) {
302
+ if (source === null || typeof source !== "object" || !("id" in source))
303
+ return undefined;
304
+ const id = source.id;
305
+ return typeof id === "string" ? id : undefined;
306
+ }
307
+ function replyPending(port, served) {
308
+ try {
309
+ port.postMessage({ type: "pwa:runtime-cache:pending-result", version: 1, served });
310
+ }
311
+ catch {
312
+ // The page may have closed its port; a missed reply must not surface as an unhandled worker rejection.
313
+ }
314
+ }
315
+ function requireRuntimeCacheEngineFactory(factory) {
316
+ if (factory === undefined) {
317
+ throw new Error("attachPlatformWorker: config.runtimeCache.enabled requires a createRuntimeCacheEngine factory");
318
+ }
319
+ return factory;
320
+ }
321
+ /**
322
+ * Builds at most three runtime-cache engines, one per (cache, strategy) pair actually used, the first time each is
323
+ * needed. `networkTimeoutSeconds` (ADR-0038), when set, is passed through only for the two network-first engines —
324
+ * "pages" and "data-network-first" — never for "data-stale-while-revalidate", which never waits on the network.
325
+ */
326
+ function buildRuntimeEngines(runtimeCache, networkTimeoutSeconds, factory) {
327
+ const built = new Map();
328
+ return (key) => {
329
+ const existing = built.get(key);
330
+ if (existing !== undefined)
331
+ return existing;
332
+ const engine = key === "pages"
333
+ ? factory({
334
+ cacheName: runtimeCache.pagesCacheName,
335
+ strategy: "network-first",
336
+ maxEntries: runtimeCache.maxEntries,
337
+ maxAgeSeconds: runtimeCache.maxAgeSeconds,
338
+ admit: (response) => admitRuntimeResponse(response, {
339
+ resourceClass: "navigation-public-dynamic",
340
+ strategy: "network-first",
341
+ maxEntryBytes: runtimeCache.maxEntryBytes,
342
+ }),
343
+ ...(networkTimeoutSeconds === undefined ? {} : { networkTimeoutSeconds }),
344
+ })
345
+ : factory({
346
+ cacheName: runtimeCache.dataCacheName,
347
+ strategy: key === "data-network-first" ? "network-first" : "stale-while-revalidate",
348
+ maxEntries: runtimeCache.maxEntries,
349
+ maxAgeSeconds: runtimeCache.maxAgeSeconds,
350
+ admit: (response) => admitRuntimeResponse(response, {
351
+ resourceClass: "public-data",
352
+ strategy: key === "data-network-first" ? "network-first" : "stale-while-revalidate",
353
+ maxEntryBytes: runtimeCache.maxEntryBytes,
354
+ }),
355
+ ...(key === "data-network-first" && networkTimeoutSeconds !== undefined ? { networkTimeoutSeconds } : {}),
356
+ });
357
+ built.set(key, engine);
358
+ return engine;
359
+ };
360
+ }
361
+ /** `decide()` only ever returns a `"runtime"` decision once `config.runtimeCache.enabled` made `runtimeEngines` non-undefined. */
362
+ function runtimeEngineFor(runtimeEngines, decision) {
363
+ if (runtimeEngines === undefined)
364
+ throw new Error("Runtime-cache decision without a runtime-cache configuration");
365
+ return runtimeEngines(decision.cache === "pages" ? "pages" : decision.strategy === "network-first" ? "data-network-first" : "data-stale-while-revalidate");
366
+ }
367
+ /**
368
+ * Runs one runtime-cache decision. On success, a cache-served response's signal is delivered — posted straight to
369
+ * the requesting client for a subresource, or stashed by `event.resultingClientId` for a navigation, since a
370
+ * navigation's own client may not exist yet to receive a direct post (spec "页面信号", T1). On failure (network down
371
+ * and nothing cached), a page decision falls back exactly like `navigate()`; a data decision rejects so the page
372
+ * sees a network error, matching the spec's "read-only cache, no offline story for data" stance.
373
+ */
374
+ async function respondFromRuntimeCache(scope, precacheEngine, runtimeEngine, decision, event, pendingSignals) {
375
+ const result = await runtimeEngine.handle(event).catch((error) => {
376
+ if (decision.cache === "pages")
377
+ return undefined;
378
+ throw error;
379
+ });
380
+ if (result === undefined)
381
+ return fallbackResponse(precacheEngine, decision.fallbacks);
382
+ if (result.servedFromCache !== null) {
383
+ const signal = {
384
+ url: requestPath(event.request.url),
385
+ cachedAt: result.servedFromCache.cachedAt,
386
+ reason: result.servedFromCache.reason,
387
+ };
388
+ if (decision.cache === "pages")
389
+ pendingSignals.store(event.resultingClientId, signal);
390
+ else
391
+ event.waitUntil(postServedFromCacheSignal(scope, event.clientId, signal));
392
+ }
393
+ return result.response;
394
+ }
395
+ function requestPath(url) {
396
+ const { pathname, search } = new URL(url);
397
+ return `${pathname}${search}`;
398
+ }
399
+ async function postServedFromCacheSignal(scope, clientId, signal) {
400
+ const client = await scope.clients.get(clientId);
401
+ if (client === undefined)
402
+ return;
403
+ try {
404
+ client.postMessage({ type: "pwa:runtime-cache:served", version: 1, ...signal });
405
+ }
406
+ catch {
407
+ // The page may have navigated away or closed between the fetch and this signal; nothing to deliver to.
408
+ }
409
+ }
410
+ /** At most 16 pending navigation signals, oldest evicted first; a signal older than 30s is dropped when taken. */
411
+ function createPendingSignalStore() {
412
+ const entries = new Map();
413
+ return {
414
+ store(clientId, signal) {
415
+ entries.delete(clientId);
416
+ entries.set(clientId, { signal, storedAt: Date.now() });
417
+ while (entries.size > PENDING_SIGNAL_LIMIT) {
418
+ const oldest = entries.keys().next().value;
419
+ if (oldest === undefined)
420
+ break;
421
+ entries.delete(oldest);
422
+ }
423
+ },
424
+ take(clientId) {
425
+ const entry = entries.get(clientId);
426
+ entries.delete(clientId);
427
+ if (entry === undefined || Date.now() - entry.storedAt > PENDING_SIGNAL_TTL_MS)
428
+ return null;
429
+ return entry.signal;
430
+ },
431
+ };
432
+ }
196
433
  //# sourceMappingURL=handlers.js.map