@pwa-platform/sw-runtime 0.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +9 -0
- package/dist/build/config.d.ts +10 -0
- package/dist/build/config.d.ts.map +1 -0
- package/dist/build/config.js +40 -0
- package/dist/build/config.js.map +1 -0
- package/dist/build/index.d.ts +6 -0
- package/dist/build/index.d.ts.map +1 -0
- package/dist/build/index.js +9 -0
- package/dist/build/index.js.map +1 -0
- package/dist/build/inject.d.ts +13 -0
- package/dist/build/inject.d.ts.map +1 -0
- package/dist/build/inject.js +29 -0
- package/dist/build/inject.js.map +1 -0
- package/dist/entries/platform-worker-entry.d.ts +2 -0
- package/dist/entries/platform-worker-entry.d.ts.map +1 -0
- package/dist/entries/platform-worker-entry.js +3 -0
- package/dist/entries/platform-worker-entry.js.map +1 -0
- package/dist/entries/recovery-worker-entry.d.ts +2 -0
- package/dist/entries/recovery-worker-entry.d.ts.map +1 -0
- package/dist/entries/recovery-worker-entry.js +4 -0
- package/dist/entries/recovery-worker-entry.js.map +1 -0
- package/dist/messages/index.d.ts +49 -0
- package/dist/messages/index.d.ts.map +1 -0
- package/dist/messages/index.js +90 -0
- package/dist/messages/index.js.map +1 -0
- package/dist/push-payload/index.d.ts +46 -0
- package/dist/push-payload/index.d.ts.map +1 -0
- package/dist/push-payload/index.js +115 -0
- package/dist/push-payload/index.js.map +1 -0
- package/dist/recovery-worker/index.d.ts +18 -0
- package/dist/recovery-worker/index.d.ts.map +1 -0
- package/dist/recovery-worker/index.js +60 -0
- package/dist/recovery-worker/index.js.map +1 -0
- package/dist/shared/config.d.ts +71 -0
- package/dist/shared/config.d.ts.map +1 -0
- package/dist/shared/config.js +216 -0
- package/dist/shared/config.js.map +1 -0
- package/dist/shared/path-match.d.ts +20 -0
- package/dist/shared/path-match.d.ts.map +1 -0
- package/dist/shared/path-match.js +56 -0
- package/dist/shared/path-match.js.map +1 -0
- package/dist/worker/decide.d.ts +65 -0
- package/dist/worker/decide.d.ts.map +1 -0
- package/dist/worker/decide.js +65 -0
- package/dist/worker/decide.js.map +1 -0
- package/dist/worker/handlers.d.ts +16 -0
- package/dist/worker/handlers.d.ts.map +1 -0
- package/dist/worker/handlers.js +196 -0
- package/dist/worker/handlers.js.map +1 -0
- package/dist/worker/index.d.ts +17 -0
- package/dist/worker/index.d.ts.map +1 -0
- package/dist/worker/index.js +16 -0
- package/dist/worker/index.js.map +1 -0
- package/dist/worker/notification-target.d.ts +9 -0
- package/dist/worker/notification-target.d.ts.map +1 -0
- package/dist/worker/notification-target.js +28 -0
- package/dist/worker/notification-target.js.map +1 -0
- package/dist/worker/offline-write-flush.d.ts +10 -0
- package/dist/worker/offline-write-flush.d.ts.map +1 -0
- package/dist/worker/offline-write-flush.js +32 -0
- package/dist/worker/offline-write-flush.js.map +1 -0
- package/dist/worker/offline-write-intent.d.ts +36 -0
- package/dist/worker/offline-write-intent.d.ts.map +1 -0
- package/dist/worker/offline-write-intent.js +84 -0
- package/dist/worker/offline-write-intent.js.map +1 -0
- package/dist/worker/offline-write-store.d.ts +27 -0
- package/dist/worker/offline-write-store.d.ts.map +1 -0
- package/dist/worker/offline-write-store.js +152 -0
- package/dist/worker/offline-write-store.js.map +1 -0
- package/package.json +65 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Recovery worker (service worker scope): activates at once, deletes this app's caches and offline-write database, then claims clients.
|
|
2
|
+
// It imports no package, so the recovery artifact never carries Workbox or a fetch listener.
|
|
3
|
+
import { validateRecoveryWorkerConfig } from "../shared/config.js";
|
|
4
|
+
/**
|
|
5
|
+
* Registers the recovery worker (ADR-0005): it takes over immediately only after it removes every cache and the
|
|
6
|
+
* identity-derived offline-write database of this app in this environment, then serves nothing, so page requests go
|
|
7
|
+
* straight to the network. On activation it also cancels this registration's push subscription, after deletion and
|
|
8
|
+
* before claiming clients, so the business backend starts seeing failed sends and can clean up (ADR-0021, "对
|
|
9
|
+
* ADR-0012 的修订").
|
|
10
|
+
*
|
|
11
|
+
* It registers only `install` and `activate`; it never registers `fetch`, `push` or `notificationclick`, never opens
|
|
12
|
+
* a cache and never deletes a cache outside `config.appCachePrefix`.
|
|
13
|
+
*/
|
|
14
|
+
export function registerRecoveryWorker({ scope, config }) {
|
|
15
|
+
const { appCachePrefix, offlineWriteDatabaseName } = validateRecoveryWorkerConfig(config);
|
|
16
|
+
scope.addEventListener("install", (event) => {
|
|
17
|
+
// Recovery never waits for a reload: the broken worker must stop controlling pages as soon as possible.
|
|
18
|
+
event.waitUntil(scope.skipWaiting());
|
|
19
|
+
});
|
|
20
|
+
scope.addEventListener("activate", (event) => {
|
|
21
|
+
event.waitUntil(recover(scope, appCachePrefix, offlineWriteDatabaseName));
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Deletes this app's caches and identity-derived queue database, cancels this registration's push subscription (if
|
|
26
|
+
* any), then claims the open clients so they stop being controlled by the broken worker. Cancelling the subscription
|
|
27
|
+
* never blocks deletion or takeover: it runs after deletion and before claim, and its own failure is swallowed.
|
|
28
|
+
*/
|
|
29
|
+
async function recover(scope, appCachePrefix, offlineWriteDatabaseName) {
|
|
30
|
+
for (const name of await scope.caches.keys()) {
|
|
31
|
+
if (name.startsWith(appCachePrefix))
|
|
32
|
+
await scope.caches.delete(name);
|
|
33
|
+
}
|
|
34
|
+
await deleteOfflineWriteDatabase(offlineWriteDatabaseName);
|
|
35
|
+
await cancelPushSubscription(scope);
|
|
36
|
+
await scope.clients.claim();
|
|
37
|
+
}
|
|
38
|
+
/** Rejects on failure or blocked deletion so recovery does not take control while sensitive queue data remains. */
|
|
39
|
+
function deleteOfflineWriteDatabase(name) {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const request = indexedDB.deleteDatabase(name);
|
|
42
|
+
request.onsuccess = () => resolve();
|
|
43
|
+
request.onerror = () => reject(request.error ?? new Error("offline-write database deletion failed"));
|
|
44
|
+
request.onblocked = () => reject(new Error("offline-write database deletion blocked"));
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/** Best-effort: environments without a `pushManager` are skipped, and any rejection here is swallowed (ADR-0021). */
|
|
48
|
+
async function cancelPushSubscription(scope) {
|
|
49
|
+
if (!("pushManager" in scope.registration))
|
|
50
|
+
return;
|
|
51
|
+
try {
|
|
52
|
+
const subscription = await scope.registration.pushManager.getSubscription();
|
|
53
|
+
if (subscription !== null)
|
|
54
|
+
await subscription.unsubscribe();
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Swallowed: cancelling the subscription must never block cache deletion or client takeover.
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/recovery-worker/index.ts"],"names":[],"mappings":"AAAA,wIAAwI;AACxI,6FAA6F;AAC7F,OAAO,EAAE,4BAA4B,EAAgC,MAAM,qBAAqB,CAAC;AAQjG;;;;;;;;;GASG;AACH,MAAM,UAAU,sBAAsB,CAAC,EAAE,KAAK,EAAE,MAAM,EAA4B;IAChF,MAAM,EAAE,cAAc,EAAE,wBAAwB,EAAE,GAAG,4BAA4B,CAAC,MAAM,CAAC,CAAC;IAE1F,KAAK,CAAC,gBAAgB,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,EAAE;QAC1C,wGAAwG;QACxG,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;IAEH,KAAK,CAAC,gBAAgB,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE,EAAE;QAC3C,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,EAAE,cAAc,EAAE,wBAAwB,CAAC,CAAC,CAAC;IAC5E,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;GAIG;AACH,KAAK,UAAU,OAAO,CAAC,KAA+B,EAAE,cAAsB,EAAE,wBAAgC;IAC9G,KAAK,MAAM,IAAI,IAAI,MAAM,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC;QAC7C,IAAI,IAAI,CAAC,UAAU,CAAC,cAAc,CAAC;YAAE,MAAM,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACvE,CAAC;IACD,MAAM,0BAA0B,CAAC,wBAAwB,CAAC,CAAC;IAC3D,MAAM,sBAAsB,CAAC,KAAK,CAAC,CAAC;IACpC,MAAM,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;AAC9B,CAAC;AAED,mHAAmH;AACnH,SAAS,0BAA0B,CAAC,IAAY;IAC9C,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,OAAO,GAAG,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;QAC/C,OAAO,CAAC,SAAS,GAAG,GAAS,EAAE,CAAC,OAAO,EAAE,CAAC;QAC1C,OAAO,CAAC,OAAO,GAAG,GAAS,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,IAAI,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC,CAAC;QAC3G,OAAO,CAAC,SAAS,GAAG,GAAS,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC,CAAC;IAC/F,CAAC,CAAC,CAAC;AACL,CAAC;AAED,qHAAqH;AACrH,KAAK,UAAU,sBAAsB,CAAC,KAA+B;IACnE,IAAI,CAAC,CAAC,aAAa,IAAI,KAAK,CAAC,YAAY,CAAC;QAAE,OAAO;IACnD,IAAI,CAAC;QACH,MAAM,YAAY,GAAG,MAAM,KAAK,CAAC,YAAY,CAAC,WAAW,CAAC,eAAe,EAAE,CAAC;QAC5E,IAAI,YAAY,KAAK,IAAI;YAAE,MAAM,YAAY,CAAC,WAAW,EAAE,CAAC;IAC9D,CAAC;IAAC,MAAM,CAAC;QACP,6FAA6F;IAC/F,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** contracts' `REQUEST_BASELINE_DENIALS`, in the same canonical order (a unit test keeps them equal). */
|
|
2
|
+
export declare const WORKER_BASELINE_DENIALS: readonly ["non-get", "cross-origin", "no-store", "opaque-response", "redirect", "websocket", "unclassified"];
|
|
3
|
+
/**
|
|
4
|
+
* `"exclude"` and `"deny"` followed by contracts' `CACHE_STRATEGIES` (a unit test keeps them equal). `"exclude"` only
|
|
5
|
+
* appears in a shared-origin root app's plan, once per child scope (ADR-0019).
|
|
6
|
+
*/
|
|
7
|
+
export declare const WORKER_PATH_RULE_ACTIONS: readonly ["exclude", "deny", "none", "cache-first", "network-first", "stale-while-revalidate"];
|
|
8
|
+
export type PwaWorkerBaselineDenial = (typeof WORKER_BASELINE_DENIALS)[number];
|
|
9
|
+
export type PwaWorkerPathRuleAction = (typeof WORKER_PATH_RULE_ACTIONS)[number];
|
|
10
|
+
export type PwaWorkerPath = `/${string}`;
|
|
11
|
+
export type PwaWorkerPathRule = {
|
|
12
|
+
/** Canonical absolute path prefix, in the plan's spelling. */
|
|
13
|
+
readonly pathPrefix: PwaWorkerPath;
|
|
14
|
+
readonly action: PwaWorkerPathRuleAction;
|
|
15
|
+
};
|
|
16
|
+
export type PwaWorkerOfflineWriteTarget = {
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly pathPrefix: PwaWorkerPath;
|
|
19
|
+
readonly maxBodyBytes: number;
|
|
20
|
+
};
|
|
21
|
+
export type PwaWorkerOfflineWrites = {
|
|
22
|
+
readonly enabled: false;
|
|
23
|
+
} | {
|
|
24
|
+
readonly enabled: true;
|
|
25
|
+
readonly databaseName: string;
|
|
26
|
+
readonly maxEntries: number;
|
|
27
|
+
readonly maxTotalBodyBytes: number;
|
|
28
|
+
readonly targets: readonly PwaWorkerOfflineWriteTarget[];
|
|
29
|
+
};
|
|
30
|
+
export type PwaPlatformWorkerConfig = {
|
|
31
|
+
readonly kind: "platform";
|
|
32
|
+
readonly version: 1;
|
|
33
|
+
/** Audit only: the worker derives its origin from `scope.location`, so nothing reads this field at runtime. */
|
|
34
|
+
readonly scope: PwaWorkerPath;
|
|
35
|
+
/** contracts `cacheName(identity, "precache")`. */
|
|
36
|
+
readonly precacheCacheName: string;
|
|
37
|
+
/**
|
|
38
|
+
* Audit only: the request-level denials are enforced by the decision table itself and the response-level ones
|
|
39
|
+
* govern runtime caching, which v1 does not do. Validation still requires this list to equal
|
|
40
|
+
* `WORKER_BASELINE_DENIALS` exactly, so adding a denial in contracts without updating that constant makes an
|
|
41
|
+
* already published worker fail to start — a deliberate fail-closed coupling, kept in step by a unit test.
|
|
42
|
+
*/
|
|
43
|
+
readonly requestBaselineDenials: readonly PwaWorkerBaselineDenial[];
|
|
44
|
+
/** In plan order: the first matching rule applies. */
|
|
45
|
+
readonly pathRules: readonly PwaWorkerPathRule[];
|
|
46
|
+
readonly offlineFallback: {
|
|
47
|
+
readonly enabled: false;
|
|
48
|
+
} | {
|
|
49
|
+
readonly enabled: true;
|
|
50
|
+
readonly path: PwaWorkerPath;
|
|
51
|
+
};
|
|
52
|
+
readonly updateMode: "prompt";
|
|
53
|
+
/** Closed configuration for explicit offline writes; `false` for every v1 plan. */
|
|
54
|
+
readonly offlineWrites: PwaWorkerOfflineWrites;
|
|
55
|
+
};
|
|
56
|
+
export type PwaRecoveryWorkerConfig = {
|
|
57
|
+
readonly kind: "recovery";
|
|
58
|
+
readonly version: 1;
|
|
59
|
+
/** contracts `appCachePrefix(identity)`: every cache of this app in this environment, across identity revisions. */
|
|
60
|
+
readonly appCachePrefix: string;
|
|
61
|
+
/** The only IndexedDB database recovery may delete: derived from this exact app identity. */
|
|
62
|
+
readonly offlineWriteDatabaseName: string;
|
|
63
|
+
};
|
|
64
|
+
export type PwaWorkerConfig = PwaPlatformWorkerConfig | PwaRecoveryWorkerConfig;
|
|
65
|
+
/** Validates a platform worker config and returns a fresh copy with keys in canonical order. Messages name fields only. */
|
|
66
|
+
export declare function validatePlatformWorkerConfig(value: unknown): PwaPlatformWorkerConfig;
|
|
67
|
+
/** Validates a recovery worker config and returns a fresh copy with keys in canonical order. Messages name fields only. */
|
|
68
|
+
export declare function validateRecoveryWorkerConfig(value: unknown): PwaRecoveryWorkerConfig;
|
|
69
|
+
/** Validates either config, selected by its `kind`. */
|
|
70
|
+
export declare function validateWorkerConfig(value: unknown): PwaWorkerConfig;
|
|
71
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/shared/config.ts"],"names":[],"mappings":"AAGA,yGAAyG;AACzG,eAAO,MAAM,uBAAuB,8GAQ1B,CAAC;AAEX;;;GAGG;AACH,eAAO,MAAM,wBAAwB,gGAAiG,CAAC;AAEvI,MAAM,MAAM,uBAAuB,GAAG,CAAC,OAAO,uBAAuB,CAAC,CAAC,MAAM,CAAC,CAAC;AAC/E,MAAM,MAAM,uBAAuB,GAAG,CAAC,OAAO,wBAAwB,CAAC,CAAC,MAAM,CAAC,CAAC;AAChF,MAAM,MAAM,aAAa,GAAG,IAAI,MAAM,EAAE,CAAC;AAEzC,MAAM,MAAM,iBAAiB,GAAG;IAC9B,8DAA8D;IAC9D,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,uBAAuB,CAAC;CAC1C,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC;IACnC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAC9B;IAAE,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAA;CAAE,GAC3B;IACE,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,SAAS,2BAA2B,EAAE,CAAC;CAC1D,CAAC;AAEN,MAAM,MAAM,uBAAuB,GAAG;IACpC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IACpB,+GAA+G;IAC/G,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAC9B,mDAAmD;IACnD,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC;;;;;OAKG;IACH,QAAQ,CAAC,sBAAsB,EAAE,SAAS,uBAAuB,EAAE,CAAC;IACpE,sDAAsD;IACtD,QAAQ,CAAC,SAAS,EAAE,SAAS,iBAAiB,EAAE,CAAC;IACjD,QAAQ,CAAC,eAAe,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAA;KAAE,GAAG;QAAE,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAA;KAAE,CAAC;IACjH,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC;IAC9B,mFAAmF;IACnF,QAAQ,CAAC,aAAa,EAAE,sBAAsB,CAAC;CAChD,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IACpB,oHAAoH;IACpH,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,6FAA6F;IAC7F,QAAQ,CAAC,wBAAwB,EAAE,MAAM,CAAC;CAC3C,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,uBAAuB,GAAG,uBAAuB,CAAC;AAwBhF,2HAA2H;AAC3H,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,uBAAuB,CA4DpF;AAED,2HAA2H;AAC3H,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,uBAAuB,CAcpF;AAED,uDAAuD;AACvD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,CAKpE"}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// Worker config shapes and their validation, shared by the build-time entry and both workers. It imports nothing,
|
|
2
|
+
// so the same rules run before injection and again when a worker starts.
|
|
3
|
+
/** contracts' `REQUEST_BASELINE_DENIALS`, in the same canonical order (a unit test keeps them equal). */
|
|
4
|
+
export const WORKER_BASELINE_DENIALS = [
|
|
5
|
+
"non-get",
|
|
6
|
+
"cross-origin",
|
|
7
|
+
"no-store",
|
|
8
|
+
"opaque-response",
|
|
9
|
+
"redirect",
|
|
10
|
+
"websocket",
|
|
11
|
+
"unclassified",
|
|
12
|
+
];
|
|
13
|
+
/**
|
|
14
|
+
* `"exclude"` and `"deny"` followed by contracts' `CACHE_STRATEGIES` (a unit test keeps them equal). `"exclude"` only
|
|
15
|
+
* appears in a shared-origin root app's plan, once per child scope (ADR-0019).
|
|
16
|
+
*/
|
|
17
|
+
export const WORKER_PATH_RULE_ACTIONS = ["exclude", "deny", "none", "cache-first", "network-first", "stale-while-revalidate"];
|
|
18
|
+
// contracts escapes every segment with encodeURIComponent, so a segment never contains ":". Matching the whole
|
|
19
|
+
// structure keeps a truncated prefix such as "pwa:" from ever selecting other apps' caches.
|
|
20
|
+
const PRECACHE_CACHE_NAME = /^pwa:[^:]+:[^:]+:[^:]+:precache$/;
|
|
21
|
+
const APP_CACHE_PREFIX = /^pwa:[^:]+:[^:]+:$/;
|
|
22
|
+
const OFFLINE_WRITE_DATABASE_NAME = /^pwa-offline-write:[^:]+:[^:]+:[^:]+$/;
|
|
23
|
+
const OFFLINE_WRITE_ID = /^[a-z][a-z0-9-]{0,63}$/;
|
|
24
|
+
// Only used to check the form of paths; never requested.
|
|
25
|
+
const PATH_BASE = "https://sw-runtime.invalid";
|
|
26
|
+
const PLATFORM_KEYS = [
|
|
27
|
+
"kind",
|
|
28
|
+
"version",
|
|
29
|
+
"scope",
|
|
30
|
+
"precacheCacheName",
|
|
31
|
+
"requestBaselineDenials",
|
|
32
|
+
"pathRules",
|
|
33
|
+
"offlineFallback",
|
|
34
|
+
"updateMode",
|
|
35
|
+
"offlineWrites",
|
|
36
|
+
];
|
|
37
|
+
const RECOVERY_KEYS = ["kind", "version", "appCachePrefix", "offlineWriteDatabaseName"];
|
|
38
|
+
/** Validates a platform worker config and returns a fresh copy with keys in canonical order. Messages name fields only. */
|
|
39
|
+
export function validatePlatformWorkerConfig(value) {
|
|
40
|
+
const config = plainObject(value, "config");
|
|
41
|
+
exactKeys(config, PLATFORM_KEYS, "config");
|
|
42
|
+
if (config["kind"] !== "platform")
|
|
43
|
+
fail('config.kind must be "platform"');
|
|
44
|
+
if (config["version"] !== 1)
|
|
45
|
+
fail("config.version must be 1");
|
|
46
|
+
const scope = canonicalPath(config["scope"], "config.scope");
|
|
47
|
+
if (!scope.endsWith("/"))
|
|
48
|
+
fail('config.scope must end with "/"');
|
|
49
|
+
const precacheCacheName = config["precacheCacheName"];
|
|
50
|
+
if (typeof precacheCacheName !== "string" || !PRECACHE_CACHE_NAME.test(precacheCacheName)) {
|
|
51
|
+
fail('config.precacheCacheName must have the form "pwa:<app>:<environment>:<seed>:precache"');
|
|
52
|
+
}
|
|
53
|
+
const denials = config["requestBaselineDenials"];
|
|
54
|
+
const completeDenials = Array.isArray(denials) &&
|
|
55
|
+
denials.length === WORKER_BASELINE_DENIALS.length &&
|
|
56
|
+
WORKER_BASELINE_DENIALS.every((denial, index) => denials[index] === denial);
|
|
57
|
+
if (!completeDenials)
|
|
58
|
+
fail("config.requestBaselineDenials must list every baseline denial once, in canonical order");
|
|
59
|
+
const rules = config["pathRules"];
|
|
60
|
+
if (!Array.isArray(rules))
|
|
61
|
+
fail("config.pathRules must be an array");
|
|
62
|
+
const pathRules = rules.map((entry, index) => {
|
|
63
|
+
const label = `config.pathRules[${index}]`;
|
|
64
|
+
const rule = plainObject(entry, label);
|
|
65
|
+
exactKeys(rule, ["pathPrefix", "action"], label);
|
|
66
|
+
const pathPrefix = canonicalPath(rule["pathPrefix"], `${label}.pathPrefix`);
|
|
67
|
+
const action = rule["action"];
|
|
68
|
+
if (!WORKER_PATH_RULE_ACTIONS.some((known) => known === action))
|
|
69
|
+
fail(`${label}.action must be "exclude", "deny" or a cache strategy`);
|
|
70
|
+
return { pathPrefix, action: action };
|
|
71
|
+
});
|
|
72
|
+
const fallback = plainObject(config["offlineFallback"], "config.offlineFallback");
|
|
73
|
+
let offlineFallback;
|
|
74
|
+
if (fallback["enabled"] === false) {
|
|
75
|
+
exactKeys(fallback, ["enabled"], "config.offlineFallback");
|
|
76
|
+
offlineFallback = { enabled: false };
|
|
77
|
+
}
|
|
78
|
+
else if (fallback["enabled"] === true) {
|
|
79
|
+
exactKeys(fallback, ["enabled", "path"], "config.offlineFallback");
|
|
80
|
+
offlineFallback = { enabled: true, path: canonicalPath(fallback["path"], "config.offlineFallback.path") };
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
fail("config.offlineFallback.enabled must be true or false");
|
|
84
|
+
}
|
|
85
|
+
if (config["updateMode"] !== "prompt")
|
|
86
|
+
fail('config.updateMode must be "prompt"');
|
|
87
|
+
const offlineWrites = offlineWriteConfig(config["offlineWrites"], scope);
|
|
88
|
+
return {
|
|
89
|
+
kind: "platform",
|
|
90
|
+
version: 1,
|
|
91
|
+
scope,
|
|
92
|
+
precacheCacheName: precacheCacheName,
|
|
93
|
+
requestBaselineDenials: [...WORKER_BASELINE_DENIALS],
|
|
94
|
+
pathRules,
|
|
95
|
+
offlineFallback,
|
|
96
|
+
updateMode: "prompt",
|
|
97
|
+
offlineWrites,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/** Validates a recovery worker config and returns a fresh copy with keys in canonical order. Messages name fields only. */
|
|
101
|
+
export function validateRecoveryWorkerConfig(value) {
|
|
102
|
+
const config = plainObject(value, "config");
|
|
103
|
+
exactKeys(config, RECOVERY_KEYS, "config");
|
|
104
|
+
if (config["kind"] !== "recovery")
|
|
105
|
+
fail('config.kind must be "recovery"');
|
|
106
|
+
if (config["version"] !== 1)
|
|
107
|
+
fail("config.version must be 1");
|
|
108
|
+
const appCachePrefix = config["appCachePrefix"];
|
|
109
|
+
if (typeof appCachePrefix !== "string" || !APP_CACHE_PREFIX.test(appCachePrefix)) {
|
|
110
|
+
fail('config.appCachePrefix must have the form "pwa:<app>:<environment>:"');
|
|
111
|
+
}
|
|
112
|
+
const offlineWriteDatabaseName = config["offlineWriteDatabaseName"];
|
|
113
|
+
if (typeof offlineWriteDatabaseName !== "string" || !OFFLINE_WRITE_DATABASE_NAME.test(offlineWriteDatabaseName)) {
|
|
114
|
+
fail('config.offlineWriteDatabaseName must have the form "pwa-offline-write:<app>:<environment>:<seed>"');
|
|
115
|
+
}
|
|
116
|
+
return { kind: "recovery", version: 1, appCachePrefix: appCachePrefix, offlineWriteDatabaseName };
|
|
117
|
+
}
|
|
118
|
+
/** Validates either config, selected by its `kind`. */
|
|
119
|
+
export function validateWorkerConfig(value) {
|
|
120
|
+
const kind = plainObject(value, "config")["kind"];
|
|
121
|
+
if (kind === "platform")
|
|
122
|
+
return validatePlatformWorkerConfig(value);
|
|
123
|
+
if (kind === "recovery")
|
|
124
|
+
return validateRecoveryWorkerConfig(value);
|
|
125
|
+
return fail('config.kind must be "platform" or "recovery"');
|
|
126
|
+
}
|
|
127
|
+
function fail(detail) {
|
|
128
|
+
throw new Error(`Invalid sw-runtime worker config: ${detail}`);
|
|
129
|
+
}
|
|
130
|
+
function plainObject(value, label) {
|
|
131
|
+
const prototype = typeof value === "object" && value !== null ? Object.getPrototypeOf(value) : undefined;
|
|
132
|
+
if (Array.isArray(value) || (prototype !== Object.prototype && prototype !== null))
|
|
133
|
+
fail(`${label} must be a plain object`);
|
|
134
|
+
return value;
|
|
135
|
+
}
|
|
136
|
+
function exactKeys(record, keys, label) {
|
|
137
|
+
const actual = Reflect.ownKeys(record);
|
|
138
|
+
if (actual.length !== keys.length || !keys.every((key) => Object.hasOwn(record, key))) {
|
|
139
|
+
fail(`${label} must have exactly the fields ${keys.join(", ")}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function offlineWriteConfig(value, scope) {
|
|
143
|
+
const config = plainObject(value, "config.offlineWrites");
|
|
144
|
+
if (config["enabled"] === false) {
|
|
145
|
+
exactKeys(config, ["enabled"], "config.offlineWrites");
|
|
146
|
+
return { enabled: false };
|
|
147
|
+
}
|
|
148
|
+
if (config["enabled"] !== true)
|
|
149
|
+
fail("config.offlineWrites.enabled must be true or false");
|
|
150
|
+
exactKeys(config, ["enabled", "databaseName", "maxEntries", "maxTotalBodyBytes", "targets"], "config.offlineWrites");
|
|
151
|
+
const databaseName = config["databaseName"];
|
|
152
|
+
if (typeof databaseName !== "string" || !OFFLINE_WRITE_DATABASE_NAME.test(databaseName)) {
|
|
153
|
+
fail('config.offlineWrites.databaseName must have the form "pwa-offline-write:<app>:<environment>:<seed>"');
|
|
154
|
+
}
|
|
155
|
+
const maxEntries = boundedPositiveInteger(config["maxEntries"], 50, "config.offlineWrites.maxEntries");
|
|
156
|
+
const maxTotalBodyBytes = boundedPositiveInteger(config["maxTotalBodyBytes"], 524_288, "config.offlineWrites.maxTotalBodyBytes");
|
|
157
|
+
const rawTargets = config["targets"];
|
|
158
|
+
if (!Array.isArray(rawTargets) || rawTargets.length === 0)
|
|
159
|
+
fail("config.offlineWrites.targets must be a non-empty array");
|
|
160
|
+
const targetIds = new Set();
|
|
161
|
+
const targetPaths = new Set();
|
|
162
|
+
const targets = rawTargets.map((entry, index) => {
|
|
163
|
+
const label = `config.offlineWrites.targets[${index}]`;
|
|
164
|
+
const target = plainObject(entry, label);
|
|
165
|
+
exactKeys(target, ["id", "pathPrefix", "maxBodyBytes"], label);
|
|
166
|
+
const id = target["id"];
|
|
167
|
+
if (typeof id !== "string" || !OFFLINE_WRITE_ID.test(id))
|
|
168
|
+
fail(`${label}.id must be an ASCII slug`);
|
|
169
|
+
const pathPrefix = canonicalPath(target["pathPrefix"], `${label}.pathPrefix`);
|
|
170
|
+
if (!isWithinScope(pathPrefix, scope))
|
|
171
|
+
fail(`${label}.pathPrefix must be inside config.scope`);
|
|
172
|
+
const maxBodyBytes = boundedPositiveInteger(target["maxBodyBytes"], 16_384, `${label}.maxBodyBytes`);
|
|
173
|
+
if (targetIds.has(id))
|
|
174
|
+
fail(`${label}.id must be unique`);
|
|
175
|
+
const pathKey = decodedPathKey(pathPrefix, `${label}.pathPrefix`);
|
|
176
|
+
if (targetPaths.has(pathKey))
|
|
177
|
+
fail(`${label}.pathPrefix must be unique after URL decoding`);
|
|
178
|
+
targetIds.add(id);
|
|
179
|
+
targetPaths.add(pathKey);
|
|
180
|
+
return { id, pathPrefix, maxBodyBytes };
|
|
181
|
+
});
|
|
182
|
+
return { enabled: true, databaseName, maxEntries, maxTotalBodyBytes, targets };
|
|
183
|
+
}
|
|
184
|
+
function boundedPositiveInteger(value, maximum, label) {
|
|
185
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > maximum) {
|
|
186
|
+
fail(`${label} must be an integer between 1 and ${maximum}`);
|
|
187
|
+
}
|
|
188
|
+
return value;
|
|
189
|
+
}
|
|
190
|
+
function isWithinScope(path, scope) {
|
|
191
|
+
return scope === "/" || path === scope.slice(0, -1) || path.startsWith(scope);
|
|
192
|
+
}
|
|
193
|
+
function decodedPathKey(path, label) {
|
|
194
|
+
try {
|
|
195
|
+
return path.split("/").map(decodeURIComponent).join("/");
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
fail(`${label} must contain valid percent escapes`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* The rule contracts' `isCanonicalPath` applies to plan paths: an absolute path already in WHATWG URL serialized form,
|
|
203
|
+
* without "//", backslashes, query, fragment or characters the URL parser would change.
|
|
204
|
+
*/
|
|
205
|
+
function canonicalPath(value, label) {
|
|
206
|
+
const canonical = typeof value === "string" &&
|
|
207
|
+
value.startsWith("/") &&
|
|
208
|
+
!value.includes("//") &&
|
|
209
|
+
!value.includes("\\") &&
|
|
210
|
+
URL.canParse(value, PATH_BASE) &&
|
|
211
|
+
new URL(value, PATH_BASE).pathname === value;
|
|
212
|
+
if (!canonical)
|
|
213
|
+
fail(`${label} must be a canonical absolute path`);
|
|
214
|
+
return value;
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/shared/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type PwaPathMatcher<Rule> = {
|
|
2
|
+
/** The first rule whose prefix contains `path`, or `undefined` when no rule matches (unclassified). */
|
|
3
|
+
match(path: string): Rule | undefined;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Comparison key for a path, decoded per segment like the URL standard: valid `%XX` escapes become bytes, invalid
|
|
7
|
+
* escapes stay literal, and bytes that are not UTF-8 become U+FFFD. Distinct spellings can only collapse into one
|
|
8
|
+
* key, never split.
|
|
9
|
+
*/
|
|
10
|
+
export declare function decodedPathKey(path: string): string;
|
|
11
|
+
/** Whole-segment containment of decoded keys: `/app/api/x` is within `/app/api`, `/app/apis` is not. */
|
|
12
|
+
export declare function isWithinPrefix(pathKey: string, prefixKey: string): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Matcher over the plan's path rules, whose order is the evaluation order. Prefix keys are decoded once, so each
|
|
15
|
+
* request only decodes its own path. The query and fragment are ignored.
|
|
16
|
+
*/
|
|
17
|
+
export declare function createPathMatcher<Rule extends {
|
|
18
|
+
readonly pathPrefix: string;
|
|
19
|
+
}>(rules: readonly Rule[]): PwaPathMatcher<Rule>;
|
|
20
|
+
//# sourceMappingURL=path-match.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path-match.d.ts","sourceRoot":"","sources":["../../src/shared/path-match.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,cAAc,CAAC,IAAI,IAAI;IACjC,uGAAuG;IACvG,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CACvC,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKnD;AAED,wGAAwG;AACxG,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAE1E;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,SAAS;IAAE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAAE,EAAE,KAAK,EAAE,SAAS,IAAI,EAAE,GAAG,cAAc,CAAC,IAAI,CAAC,CAS5H"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Runtime path matching: the same decoding and whole-segment rules policy-compiler applies when it orders rules and
|
|
2
|
+
// selects precache entries (a parity test against core keeps them equal). Imports nothing, so it runs in a worker.
|
|
3
|
+
// `%2F` must not become a segment boundary the URL does not have, so it is replaced by a lone surrogate: UTF-8
|
|
4
|
+
// decoding never produces one, so it cannot collide with decoded text.
|
|
5
|
+
const ENCODED_SLASH = "\uD800";
|
|
6
|
+
const UTF8_DECODER = new TextDecoder("utf-8", { ignoreBOM: true });
|
|
7
|
+
const UTF8_ENCODER = new TextEncoder();
|
|
8
|
+
/**
|
|
9
|
+
* Comparison key for a path, decoded per segment like the URL standard: valid `%XX` escapes become bytes, invalid
|
|
10
|
+
* escapes stay literal, and bytes that are not UTF-8 become U+FFFD. Distinct spellings can only collapse into one
|
|
11
|
+
* key, never split.
|
|
12
|
+
*/
|
|
13
|
+
export function decodedPathKey(path) {
|
|
14
|
+
return path
|
|
15
|
+
.split("/")
|
|
16
|
+
.map((segment) => segment.split(/%2f/i).map(percentDecode).join(ENCODED_SLASH))
|
|
17
|
+
.join("/");
|
|
18
|
+
}
|
|
19
|
+
/** Whole-segment containment of decoded keys: `/app/api/x` is within `/app/api`, `/app/apis` is not. */
|
|
20
|
+
export function isWithinPrefix(pathKey, prefixKey) {
|
|
21
|
+
return prefixKey === "/" || pathKey === prefixKey || pathKey.startsWith(`${prefixKey}/`);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Matcher over the plan's path rules, whose order is the evaluation order. Prefix keys are decoded once, so each
|
|
25
|
+
* request only decodes its own path. The query and fragment are ignored.
|
|
26
|
+
*/
|
|
27
|
+
export function createPathMatcher(rules) {
|
|
28
|
+
const prefixKeys = rules.map((rule) => decodedPathKey(rule.pathPrefix));
|
|
29
|
+
return {
|
|
30
|
+
match(path) {
|
|
31
|
+
const pathKey = decodedPathKey(pathOnly(path));
|
|
32
|
+
const index = prefixKeys.findIndex((prefixKey) => isWithinPrefix(pathKey, prefixKey));
|
|
33
|
+
return index === -1 ? undefined : rules[index];
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function pathOnly(path) {
|
|
38
|
+
const end = path.search(/[?#]/);
|
|
39
|
+
return end === -1 ? path : path.slice(0, end);
|
|
40
|
+
}
|
|
41
|
+
function percentDecode(text) {
|
|
42
|
+
const bytes = [];
|
|
43
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
44
|
+
const escape = text.slice(index + 1, index + 3);
|
|
45
|
+
if (text[index] === "%" && /^[0-9A-Fa-f]{2}$/.test(escape)) {
|
|
46
|
+
bytes.push(Number.parseInt(escape, 16));
|
|
47
|
+
index += 2;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const character = String.fromCodePoint(text.codePointAt(index) ?? 0);
|
|
51
|
+
bytes.push(...UTF8_ENCODER.encode(character));
|
|
52
|
+
index += character.length - 1;
|
|
53
|
+
}
|
|
54
|
+
return UTF8_DECODER.decode(Uint8Array.from(bytes));
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=path-match.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path-match.js","sourceRoot":"","sources":["../../src/shared/path-match.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { PwaPlatformWorkerConfig } from "../shared/config.js";
|
|
2
|
+
export type PwaPassthroughReason =
|
|
3
|
+
/** Baseline denials that are visible on the request itself. */
|
|
4
|
+
"non-get" | "cross-origin"
|
|
5
|
+
/**
|
|
6
|
+
* The first matching path rule is a shared-origin root's exclude rule: the path belongs to a child app on the same
|
|
7
|
+
* origin, and this worker answers nothing there — not even an offline page, unlike `denied` (ADR-0019).
|
|
8
|
+
*/
|
|
9
|
+
| "excluded"
|
|
10
|
+
/**
|
|
11
|
+
* The first matching path rule denies caching. A denied *navigation* is only passed through when there is no
|
|
12
|
+
* offline page to fall back to; otherwise it is a `navigate` decision whose only fallback is that page.
|
|
13
|
+
*/
|
|
14
|
+
| "denied" | "unclassified"
|
|
15
|
+
/** An allowed request the plan does not precache; the platform has no runtime cache in v1. */
|
|
16
|
+
| "not-precached"
|
|
17
|
+
/** A URL the worker cannot parse. */
|
|
18
|
+
| "unparsable"
|
|
19
|
+
/**
|
|
20
|
+
* A manifest-hit, non-navigation request that carries a `Range` header (any value). The precache stores full
|
|
21
|
+
* responses, so `engine.match` cannot answer with a genuine partial response; the network answers instead
|
|
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.
|
|
24
|
+
*/
|
|
25
|
+
| "range";
|
|
26
|
+
export type PwaRequestDecision =
|
|
27
|
+
/** The worker does not call `respondWith`: the request behaves exactly as it would without a worker. */
|
|
28
|
+
{
|
|
29
|
+
readonly kind: "passthrough";
|
|
30
|
+
readonly reason: PwaPassthroughReason;
|
|
31
|
+
}
|
|
32
|
+
/** Read this exact manifest URL from the precache. */
|
|
33
|
+
| {
|
|
34
|
+
readonly kind: "precache";
|
|
35
|
+
readonly manifestUrl: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Network first; only when the network fails, try `fallbacks` in order, then fail. Nothing is ever written to a
|
|
39
|
+
* cache on this path, whatever the network returns.
|
|
40
|
+
*/
|
|
41
|
+
| {
|
|
42
|
+
readonly kind: "navigate";
|
|
43
|
+
readonly fallbacks: readonly string[];
|
|
44
|
+
};
|
|
45
|
+
export type PwaRequestInput = {
|
|
46
|
+
readonly method: string;
|
|
47
|
+
/** Absolute request URL. */
|
|
48
|
+
readonly url: string;
|
|
49
|
+
/** `request.mode === "navigate"`. */
|
|
50
|
+
readonly navigation: boolean;
|
|
51
|
+
/** `request.headers.has("range")`, whatever the header's value. Ignored for navigations (ADR-0023). */
|
|
52
|
+
readonly range: boolean;
|
|
53
|
+
};
|
|
54
|
+
export type PwaRouterOptions = {
|
|
55
|
+
readonly config: PwaPlatformWorkerConfig;
|
|
56
|
+
/** The injected manifest's URLs, as the engine reports them (paths, without a query). */
|
|
57
|
+
readonly manifestUrls: readonly string[];
|
|
58
|
+
/** The worker's own origin; every other origin is a baseline denial. */
|
|
59
|
+
readonly origin: string;
|
|
60
|
+
};
|
|
61
|
+
export type PwaRouter = {
|
|
62
|
+
decide(request: PwaRequestInput): PwaRequestDecision;
|
|
63
|
+
};
|
|
64
|
+
export declare function createRouter({ config, manifestUrls, origin }: PwaRouterOptions): PwaRouter;
|
|
65
|
+
//# sourceMappingURL=decide.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createPathMatcher } from "../shared/path-match.js";
|
|
2
|
+
const PASSTHROUGH = (reason) => ({ kind: "passthrough", reason });
|
|
3
|
+
export function createRouter({ config, manifestUrls, origin }) {
|
|
4
|
+
const matcher = createPathMatcher(config.pathRules);
|
|
5
|
+
const manifest = new Set(manifestUrls);
|
|
6
|
+
return {
|
|
7
|
+
decide({ method, url, navigation, range }) {
|
|
8
|
+
if (method !== "GET")
|
|
9
|
+
return PASSTHROUGH("non-get");
|
|
10
|
+
if (!URL.canParse(url))
|
|
11
|
+
return PASSTHROUGH("unparsable");
|
|
12
|
+
const requested = new URL(url);
|
|
13
|
+
if (requested.origin !== origin)
|
|
14
|
+
return PASSTHROUGH("cross-origin");
|
|
15
|
+
const rule = matcher.match(requested.pathname);
|
|
16
|
+
if (rule === undefined)
|
|
17
|
+
return PASSTHROUGH("unclassified");
|
|
18
|
+
// Checked before `deny` on purpose: a denied navigation may fall back to the offline page, an excluded one never
|
|
19
|
+
// may — that page belongs to this app, and the excluded path belongs to another app on the same origin.
|
|
20
|
+
if (rule.action === "exclude")
|
|
21
|
+
return PASSTHROUGH("excluded");
|
|
22
|
+
if (rule.action === "deny") {
|
|
23
|
+
// A denied navigation still never touches a cache: the network answers whenever it responds at all, and only
|
|
24
|
+
// a failed request falls back — to the offline page, never to anything cached for this path, since a
|
|
25
|
+
// denied path has nothing in the precache (ADR-0012, 2026-09-17 amendment). Without an offline page there
|
|
26
|
+
// is nothing to offer, so the worker stays out of the way exactly as before.
|
|
27
|
+
const offline = navigation ? offlineFallback(config, manifest) : undefined;
|
|
28
|
+
return offline === undefined ? PASSTHROUGH("denied") : { kind: "navigate", fallbacks: [offline] };
|
|
29
|
+
}
|
|
30
|
+
// 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.
|
|
32
|
+
const exact = `${requested.pathname}${requested.search}`;
|
|
33
|
+
if (navigation)
|
|
34
|
+
return { kind: "navigate", fallbacks: navigationFallbacks(config, manifest, requested) };
|
|
35
|
+
if (!manifest.has(exact))
|
|
36
|
+
return PASSTHROUGH("not-precached");
|
|
37
|
+
// The precache holds full responses, so a Range request would otherwise get a full 200 body instead of a 206;
|
|
38
|
+
// stepping aside here lets the network answer it directly (ADR-0023). Navigations never look at range.
|
|
39
|
+
return range ? PASSTHROUGH("range") : { kind: "precache", manifestUrl: exact };
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
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.
|
|
47
|
+
*
|
|
48
|
+
* The route's `index.html` is tried for URLs with and without a trailing slash. Frameworks that prerender write
|
|
49
|
+
* `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.
|
|
51
|
+
*/
|
|
52
|
+
function navigationFallbacks(config, manifest, requested) {
|
|
53
|
+
const { pathname, search } = requested;
|
|
54
|
+
const candidates = [`${pathname}${search}`];
|
|
55
|
+
candidates.push(pathname.endsWith("/") ? `${pathname}index.html${search}` : `${pathname}/index.html${search}`);
|
|
56
|
+
const offline = offlineFallback(config, manifest);
|
|
57
|
+
if (offline !== undefined)
|
|
58
|
+
candidates.push(offline);
|
|
59
|
+
return [...new Set(candidates.filter((candidate) => manifest.has(candidate)))];
|
|
60
|
+
}
|
|
61
|
+
/** The offline page, when the plan enables one and the precache holds it. */
|
|
62
|
+
function offlineFallback(config, manifest) {
|
|
63
|
+
return config.offlineFallback.enabled && manifest.has(config.offlineFallback.path) ? config.offlineFallback.path : undefined;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=decide.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decide.js","sourceRoot":"","sources":["../../src/worker/decide.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PwaPrecacheEngine } from "@pwa-platform/engine-workbox/worker";
|
|
2
|
+
import type { PwaPlatformWorkerConfig } from "../shared/config.js";
|
|
3
|
+
export type PwaPlatformWorkerHandlers = {
|
|
4
|
+
readonly scope: ServiceWorkerGlobalScope;
|
|
5
|
+
/** Already validated by `registerPlatformWorker`. */
|
|
6
|
+
readonly config: PwaPlatformWorkerConfig;
|
|
7
|
+
readonly engine: PwaPrecacheEngine;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Registers `install`, `activate`, `fetch`, `message`, `push` and `notificationclick`, and nothing else. The worker
|
|
11
|
+
* never calls `skipWaiting` on its own (updates are prompted) and never claims clients. `push` and
|
|
12
|
+
* `notificationclick` are always on: see ADR-0021 for why a fixed pair of listeners has no observable effect on an
|
|
13
|
+
* application that never subscribes to push.
|
|
14
|
+
*/
|
|
15
|
+
export declare function attachPlatformWorker({ scope, config, engine }: PwaPlatformWorkerHandlers): void;
|
|
16
|
+
//# sourceMappingURL=handlers.d.ts.map
|
|
@@ -0,0 +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"}
|