@pwa-platform/sw-runtime 0.1.0-beta.0 → 0.1.0-beta.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.
- package/dist/build/config.d.ts.map +1 -1
- package/dist/build/config.js +35 -2
- package/dist/build/config.js.map +1 -1
- package/dist/messages/index.d.ts +30 -0
- package/dist/messages/index.d.ts.map +1 -1
- package/dist/messages/index.js +32 -0
- package/dist/messages/index.js.map +1 -1
- package/dist/recovery-worker/index.d.ts.map +1 -1
- package/dist/recovery-worker/index.js +22 -5
- package/dist/recovery-worker/index.js.map +1 -1
- package/dist/shared/config.d.ts +33 -0
- package/dist/shared/config.d.ts.map +1 -1
- package/dist/shared/config.js +67 -1
- package/dist/shared/config.js.map +1 -1
- package/dist/shared/expiration-records.d.ts +9 -0
- package/dist/shared/expiration-records.d.ts.map +1 -0
- package/dist/shared/expiration-records.js +85 -0
- package/dist/shared/expiration-records.js.map +1 -0
- package/dist/worker/admit.d.ts +8 -0
- package/dist/worker/admit.d.ts.map +1 -0
- package/dist/worker/admit.js +99 -0
- package/dist/worker/admit.js.map +1 -0
- package/dist/worker/decide.d.ts +23 -2
- package/dist/worker/decide.d.ts.map +1 -1
- package/dist/worker/decide.js +40 -11
- package/dist/worker/decide.js.map +1 -1
- package/dist/worker/handlers.d.ts +7 -2
- package/dist/worker/handlers.d.ts.map +1 -1
- package/dist/worker/handlers.js +247 -10
- package/dist/worker/handlers.js.map +1 -1
- package/dist/worker/index.d.ts.map +1 -1
- package/dist/worker/index.js +2 -2
- package/dist/worker/index.js.map +1 -1
- package/dist/worker/runtime-cleanup.d.ts +14 -0
- package/dist/worker/runtime-cleanup.d.ts.map +1 -0
- package/dist/worker/runtime-cleanup.js +23 -0
- package/dist/worker/runtime-cleanup.js.map +1 -0
- 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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"}
|
package/dist/worker/decide.d.ts
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/worker/decide.js
CHANGED
|
@@ -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 (
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
45
|
-
*
|
|
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).
|
|
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
|
|
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":"AAGA,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;
|
|
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":"
|
|
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"}
|
package/dist/worker/handlers.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
175
|
-
|
|
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
|
|
277
|
+
const response = await networkPromise;
|
|
178
278
|
if (response.type !== "error")
|
|
179
279
|
return response;
|
|
180
280
|
}
|
|
181
281
|
catch {
|
|
182
|
-
//
|
|
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
|