@pwa-platform/entry-resilience 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +87 -0
- package/dist/browser/index.d.ts +28 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +39 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/browser/indexeddb.d.ts +12 -0
- package/dist/browser/indexeddb.d.ts.map +1 -0
- package/dist/browser/indexeddb.js +61 -0
- package/dist/browser/indexeddb.js.map +1 -0
- package/dist/browser/probes.d.ts +13 -0
- package/dist/browser/probes.d.ts.map +1 -0
- package/dist/browser/probes.js +36 -0
- package/dist/browser/probes.js.map +1 -0
- package/dist/check.d.ts +21 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/check.js +48 -0
- package/dist/check.js.map +1 -0
- package/dist/client/index.d.ts +19 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +49 -0
- package/dist/client/index.js.map +1 -0
- package/dist/decide.d.ts +19 -0
- package/dist/decide.d.ts.map +1 -0
- package/dist/decide.js +38 -0
- package/dist/decide.js.map +1 -0
- package/dist/diagnostics.d.ts +11 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +51 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/base64url.d.ts +8 -0
- package/dist/internal/base64url.d.ts.map +1 -0
- package/dist/internal/base64url.js +32 -0
- package/dist/internal/base64url.js.map +1 -0
- package/dist/internal/iso-date.d.ts +8 -0
- package/dist/internal/iso-date.d.ts.map +1 -0
- package/dist/internal/iso-date.js +39 -0
- package/dist/internal/iso-date.js.map +1 -0
- package/dist/internal/origin.d.ts +6 -0
- package/dist/internal/origin.d.ts.map +1 -0
- package/dist/internal/origin.js +19 -0
- package/dist/internal/origin.js.map +1 -0
- package/dist/internal/plain-object.d.ts +8 -0
- package/dist/internal/plain-object.d.ts.map +1 -0
- package/dist/internal/plain-object.js +16 -0
- package/dist/internal/plain-object.js.map +1 -0
- package/dist/internal/theme-key.d.ts +2 -0
- package/dist/internal/theme-key.d.ts.map +1 -0
- package/dist/internal/theme-key.js +13 -0
- package/dist/internal/theme-key.js.map +1 -0
- package/dist/manifest.d.ts +13 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +153 -0
- package/dist/manifest.js.map +1 -0
- package/dist/page/main.d.ts +2 -0
- package/dist/page/main.d.ts.map +1 -0
- package/dist/page/main.js +88 -0
- package/dist/page/main.js.map +1 -0
- package/dist/page/messages.d.ts +24 -0
- package/dist/page/messages.d.ts.map +1 -0
- package/dist/page/messages.js +43 -0
- package/dist/page/messages.js.map +1 -0
- package/dist/page/model.d.ts +16 -0
- package/dist/page/model.d.ts.map +1 -0
- package/dist/page/model.js +51 -0
- package/dist/page/model.js.map +1 -0
- package/dist/page/render.d.ts +24 -0
- package/dist/page/render.d.ts.map +1 -0
- package/dist/page/render.js +61 -0
- package/dist/page/render.js.map +1 -0
- package/dist/resolve.d.ts +31 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/resolve.js +65 -0
- package/dist/resolve.js.map +1 -0
- package/dist/return-path.d.ts +10 -0
- package/dist/return-path.d.ts.map +1 -0
- package/dist/return-path.js +75 -0
- package/dist/return-path.js.map +1 -0
- package/dist/select.d.ts +16 -0
- package/dist/select.d.ts.map +1 -0
- package/dist/select.js +17 -0
- package/dist/select.js.map +1 -0
- package/dist/types.d.ts +43 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -0
- package/dist/update.d.ts +28 -0
- package/dist/update.d.ts.map +1 -0
- package/dist/update.js +52 -0
- package/dist/update.js.map +1 -0
- package/dist/vite/default-style.d.ts +2 -0
- package/dist/vite/default-style.d.ts.map +1 -0
- package/dist/vite/default-style.js +109 -0
- package/dist/vite/default-style.js.map +1 -0
- package/dist/vite/entry-page.d.ts +6 -0
- package/dist/vite/entry-page.d.ts.map +1 -0
- package/dist/vite/entry-page.js +22 -0
- package/dist/vite/entry-page.js.map +1 -0
- package/dist/vite/index.d.ts +24 -0
- package/dist/vite/index.d.ts.map +1 -0
- package/dist/vite/index.js +190 -0
- package/dist/vite/index.js.map +1 -0
- package/dist/vite/options.d.ts +31 -0
- package/dist/vite/options.d.ts.map +1 -0
- package/dist/vite/options.js +118 -0
- package/dist/vite/options.js.map +1 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PWA Platform contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# @pwa-platform/entry-resilience
|
|
2
|
+
|
|
3
|
+
Optional entry recovery for a PWA whose original address is moving or unavailable. Install it alongside
|
|
4
|
+
`@pwa-platform/vite`; use the same `PwaIdentity` object for both Vite plugins. The package supports Vite 5 and 8
|
|
5
|
+
on Node 22 or later.
|
|
6
|
+
|
|
7
|
+
The application obtains a manifest through its own request layer and hands the decoded object to the client API.
|
|
8
|
+
The platform checks its shape, increasing sequence and expiry, then stores it locally. A recovery page shows a
|
|
9
|
+
validated alternative address; navigation happens only after the user clicks it. Neither cookies nor login state
|
|
10
|
+
move between origins.
|
|
11
|
+
|
|
12
|
+
## Install and configure
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm install @pwa-platform/entry-resilience @pwa-platform/vite
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
// vite.config.ts — reuse the identity already passed to pwa()
|
|
20
|
+
import { pwa } from "@pwa-platform/vite";
|
|
21
|
+
import { pwaEntryResilience } from "@pwa-platform/entry-resilience/vite";
|
|
22
|
+
|
|
23
|
+
plugins: [
|
|
24
|
+
pwa({ identity, policy, install, topology }),
|
|
25
|
+
pwaEntryResilience({ identity, maxValidityDays: 30, locale: "zh-CN" }),
|
|
26
|
+
]
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The PWA policy must classify the mount-relative `/pwa-entry.html` as a `cache-first` asset, and its fingerprinted
|
|
30
|
+
script under `/assets` as an asset too. The build rejects a recovery page that is not precached. `locale` accepts
|
|
31
|
+
`"zh-CN"` or `"en"`; `messages` overrides individual strings. The `css` option appends host CSS after the
|
|
32
|
+
default recovery-page style. To change the button, override `--pwa-entry-accent` and
|
|
33
|
+
`--pwa-entry-accent-fg` in `.pwa-entry`; see the [integration guide](https://github.com/haigeerlab/pwa-platform/blob/main/docs/guides/entry-recovery-integration.md)
|
|
34
|
+
for light/dark theme selectors and strict CSP hashes.
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { updateEntryManifest, checkEntryRecovery, setPwaTheme } from "@pwa-platform/entry-resilience/client";
|
|
38
|
+
|
|
39
|
+
const result = await updateEntryManifest(await api.getEntryManifest());
|
|
40
|
+
if (!result.accepted) reportDiagnostics(result.diagnostics);
|
|
41
|
+
|
|
42
|
+
const recovery = await checkEntryRecovery({ returnPath: location.pathname });
|
|
43
|
+
if (recovery.kind === "available") showRecoveryLink(recovery.recoveryPageUrl);
|
|
44
|
+
|
|
45
|
+
setPwaTheme("system"); // or "light" / "dark"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The application owns the request, authentication, decryption and polling. Keep the manifest endpoint protected:
|
|
49
|
+
the platform validates structure and timing, but does not authenticate its source or restrict destination origins.
|
|
50
|
+
An ordinary device-wide loss of connectivity with a `normal` manifest does not create a domain-outage suggestion.
|
|
51
|
+
|
|
52
|
+
## Validate a manifest before you publish it
|
|
53
|
+
|
|
54
|
+
Use the same validator in Node before publishing a manifest so an invalid manifest fails CI or the backend job
|
|
55
|
+
instead of being silently rejected only by a browser page.
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
import { parseEntryManifest } from "@pwa-platform/entry-resilience";
|
|
59
|
+
|
|
60
|
+
const manifest = {
|
|
61
|
+
sequence: 7,
|
|
62
|
+
expiresAt: "2026-10-01T08:00:00Z",
|
|
63
|
+
status: "migrating",
|
|
64
|
+
reason: { code: "planned-migration", message: "Domain retires October 1." },
|
|
65
|
+
entries: [{ origin: "https://new.example.com", startPath: "/app/" }],
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const result = parseEntryManifest(manifest, {
|
|
69
|
+
appId: "pwaexample",
|
|
70
|
+
environment: "production",
|
|
71
|
+
// Match the maxValidityDays in this app's Vite config.
|
|
72
|
+
maxValidityDays: 30,
|
|
73
|
+
// Inject the moment at which validity should be judged.
|
|
74
|
+
now: Date.now(),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (!result.ok) {
|
|
78
|
+
for (const { code, path } of result.diagnostics) {
|
|
79
|
+
console.error(`${path || "(manifest)"}: ${code}`);
|
|
80
|
+
}
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`parseEntryManifest` never throws or reads ambient state. It is the same shape validator used by
|
|
86
|
+
`updateEntryManifest`; expiry and sequence are checked again when the browser accepts the manifest. Use an
|
|
87
|
+
`expiresAt` value in `YYYY-MM-DDTHH:mm:ssZ` format without milliseconds.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { EntryRecoveryResult } from "../check.js";
|
|
2
|
+
import type { EntryRuntimeConfig, EntryRuntimePorts } from "../resolve.js";
|
|
3
|
+
import type { EntryManifest } from "../types.js";
|
|
4
|
+
export type EntryCheckerConfig = EntryRuntimeConfig & {
|
|
5
|
+
/** Passed through to `createProbes`: the app's mount path, e.g. `"/app/"`. */
|
|
6
|
+
readonly mountPath: string;
|
|
7
|
+
};
|
|
8
|
+
export type EntryCheckOptions = {
|
|
9
|
+
readonly returnPath?: string;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Assembles the real browser ports (probes, IndexedDB store, `Date.now`, `location.origin`) that
|
|
13
|
+
* `createEntryRecoveryChecker` (below), src/client/index.ts's `checkEntryRecovery`/`updateEntryManifest` (which
|
|
14
|
+
* share one instance across both calls) and the recovery page (src/page/main.ts, which calls
|
|
15
|
+
* `resolveEntryRecovery` directly rather than `runEntryRecovery`) all need. `saveStored` is included alongside the
|
|
16
|
+
* `EntryRuntimePorts` fields so the same object also satisfies `EntryManifestUpdatePorts` (src/update.ts) without a
|
|
17
|
+
* second IndexedDB store being opened for the write path.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createEntryRuntimePorts(config: EntryCheckerConfig): EntryRuntimePorts & {
|
|
20
|
+
saveStored(manifest: EntryManifest): Promise<void>;
|
|
21
|
+
};
|
|
22
|
+
/** Builds `checkEntryRecovery`, wiring real browser ports around the pure `runEntryRecovery`. */
|
|
23
|
+
export declare function createEntryRecoveryChecker(config: EntryCheckerConfig): (options?: EntryCheckOptions) => Promise<EntryRecoveryResult>;
|
|
24
|
+
export { createIndexedDbStore } from "./indexeddb.js";
|
|
25
|
+
export type { EntryIndexedDbStore, CreateIndexedDbStoreOptions } from "./indexeddb.js";
|
|
26
|
+
export { createProbes } from "./probes.js";
|
|
27
|
+
export type { CreateProbesOptions } from "./probes.js";
|
|
28
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/browser/index.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,KAAK,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAC3E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAIjD,MAAM,MAAM,kBAAkB,GAAG,kBAAkB,GAAG;IACpD,8EAA8E;IAC9E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjE;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,kBAAkB,GACzB,iBAAiB,GAAG;IAAE,UAAU,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAY5E;AAED,iGAAiG;AACjG,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,kBAAkB,GACzB,CAAC,OAAO,CAAC,EAAE,iBAAiB,KAAK,OAAO,CAAC,mBAAmB,CAAC,CAG/D;AAED,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AACtD,YAAY,EAAE,mBAAmB,EAAE,2BAA2B,EAAE,MAAM,gBAAgB,CAAC;AACvF,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,YAAY,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Assembles the browser ports (probes, IndexedDB store, `Date.now`, `location.origin`) into the page-side
|
|
2
|
+
// `checkEntryRecovery` function described in spec/pwa-entry-resilience.md's "页面侧 API". The Vite plugin supplies
|
|
3
|
+
// `config` from its virtual module.
|
|
4
|
+
//
|
|
5
|
+
// ADR-0033 (2026-09-23) removed the discovery-source fetch from the read path (`checkEntryRecovery` now only reads
|
|
6
|
+
// what a prior `updateEntryManifest` call stored — see src/resolve.ts and src/update.ts), so `createEntryRuntimePorts`
|
|
7
|
+
// no longer builds a `fetchDiscovery` port, and src/browser/discovery.ts is deleted along with it (EM3,
|
|
8
|
+
// tasks/pwa-entry-resilience/plan.md).
|
|
9
|
+
import { runEntryRecovery } from "../check.js";
|
|
10
|
+
import { createIndexedDbStore } from "./indexeddb.js";
|
|
11
|
+
import { createProbes } from "./probes.js";
|
|
12
|
+
/**
|
|
13
|
+
* Assembles the real browser ports (probes, IndexedDB store, `Date.now`, `location.origin`) that
|
|
14
|
+
* `createEntryRecoveryChecker` (below), src/client/index.ts's `checkEntryRecovery`/`updateEntryManifest` (which
|
|
15
|
+
* share one instance across both calls) and the recovery page (src/page/main.ts, which calls
|
|
16
|
+
* `resolveEntryRecovery` directly rather than `runEntryRecovery`) all need. `saveStored` is included alongside the
|
|
17
|
+
* `EntryRuntimePorts` fields so the same object also satisfies `EntryManifestUpdatePorts` (src/update.ts) without a
|
|
18
|
+
* second IndexedDB store being opened for the write path.
|
|
19
|
+
*/
|
|
20
|
+
export function createEntryRuntimePorts(config) {
|
|
21
|
+
const probes = createProbes({ mountPath: config.mountPath });
|
|
22
|
+
const store = createIndexedDbStore({ appId: config.appId, environment: config.environment });
|
|
23
|
+
return {
|
|
24
|
+
now: () => Date.now(),
|
|
25
|
+
currentOrigin: () => location.origin,
|
|
26
|
+
loadStored: store.loadStored,
|
|
27
|
+
saveStored: store.saveStored,
|
|
28
|
+
probePrimary: probes.probePrimary,
|
|
29
|
+
probeAlternate: probes.probeAlternate,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** Builds `checkEntryRecovery`, wiring real browser ports around the pure `runEntryRecovery`. */
|
|
33
|
+
export function createEntryRecoveryChecker(config) {
|
|
34
|
+
const ports = createEntryRuntimePorts(config);
|
|
35
|
+
return (options = {}) => runEntryRecovery(config, ports, options);
|
|
36
|
+
}
|
|
37
|
+
export { createIndexedDbStore } from "./indexeddb.js";
|
|
38
|
+
export { createProbes } from "./probes.js";
|
|
39
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/browser/index.ts"],"names":[],"mappings":"AAAA,0GAA0G;AAC1G,gHAAgH;AAChH,oCAAoC;AACpC,EAAE;AACF,mHAAmH;AACnH,uHAAuH;AACvH,wGAAwG;AACxG,uCAAuC;AACvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAI/C,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAS3C;;;;;;;GAOG;AACH,MAAM,UAAU,uBAAuB,CACrC,MAA0B;IAE1B,MAAM,MAAM,GAAG,YAAY,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC;IAC7D,MAAM,KAAK,GAAG,oBAAoB,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,WAAW,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;IAE7F,OAAO;QACL,GAAG,EAAE,GAAW,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE;QAC7B,aAAa,EAAE,GAAW,EAAE,CAAC,QAAQ,CAAC,MAAM;QAC5C,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,YAAY,EAAE,MAAM,CAAC,YAAY;QACjC,cAAc,EAAE,MAAM,CAAC,cAAc;KACtC,CAAC;AACJ,CAAC;AAED,iGAAiG;AACjG,MAAM,UAAU,0BAA0B,CACxC,MAA0B;IAE1B,MAAM,KAAK,GAAG,uBAAuB,CAAC,MAAM,CAAC,CAAC;IAC9C,OAAO,CAAC,UAA6B,EAAE,EAAgC,EAAE,CAAC,gBAAgB,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;AACrH,CAAC;AAED,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAEtD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { EntryManifest } from "../types.js";
|
|
2
|
+
export type CreateIndexedDbStoreOptions = {
|
|
3
|
+
readonly appId: string;
|
|
4
|
+
readonly environment: string;
|
|
5
|
+
};
|
|
6
|
+
export type EntryIndexedDbStore = {
|
|
7
|
+
loadStored(): Promise<EntryManifest | null>;
|
|
8
|
+
saveStored(manifest: EntryManifest): Promise<void>;
|
|
9
|
+
};
|
|
10
|
+
/** Builds the `loadStored`/`saveStored` ports backed by `pwa-entry:<appId>:<environment>`. */
|
|
11
|
+
export declare function createIndexedDbStore({ appId, environment }: CreateIndexedDbStoreOptions): EntryIndexedDbStore;
|
|
12
|
+
//# sourceMappingURL=indexeddb.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"indexeddb.d.ts","sourceRoot":"","sources":["../../src/browser/indexeddb.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,MAAM,2BAA2B,GAAG;IACxC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,UAAU,IAAI,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC;IAC5C,UAAU,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpD,CAAC;AAgBF,8FAA8F;AAC9F,wBAAgB,oBAAoB,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,EAAE,2BAA2B,GAAG,mBAAmB,CA6C7G"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const STORE_NAME = "manifest";
|
|
2
|
+
const RECORD_KEY = "current";
|
|
3
|
+
function openDatabase(databaseName) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const request = indexedDB.open(databaseName, 1);
|
|
6
|
+
request.onupgradeneeded = () => {
|
|
7
|
+
request.result.createObjectStore(STORE_NAME);
|
|
8
|
+
};
|
|
9
|
+
request.onsuccess = () => resolve(request.result);
|
|
10
|
+
request.onerror = () => reject(request.error);
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
/** Builds the `loadStored`/`saveStored` ports backed by `pwa-entry:<appId>:<environment>`. */
|
|
14
|
+
export function createIndexedDbStore({ appId, environment }) {
|
|
15
|
+
const databaseName = `pwa-entry:${encodeURIComponent(appId)}:${encodeURIComponent(environment)}`;
|
|
16
|
+
return {
|
|
17
|
+
// Open failure (including a `VersionError` when a higher-versioned database already exists on disk),
|
|
18
|
+
// transaction/request errors and transaction aborts all reject rather than resolving `null` — only a genuinely
|
|
19
|
+
// missing record does that. Callers already treat a rejected `loadStored` as `entry.storage-unavailable`;
|
|
20
|
+
// silently returning `null` here would mask a real failure as "nothing stored".
|
|
21
|
+
async loadStored() {
|
|
22
|
+
const db = await openDatabase(databaseName);
|
|
23
|
+
try {
|
|
24
|
+
return await new Promise((resolve, reject) => {
|
|
25
|
+
let transaction;
|
|
26
|
+
try {
|
|
27
|
+
transaction = db.transaction(STORE_NAME, "readonly");
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
reject(error);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
transaction.onabort = () => reject(transaction.error ?? new Error("entry: readonly transaction aborted"));
|
|
34
|
+
const request = transaction.objectStore(STORE_NAME).get(RECORD_KEY);
|
|
35
|
+
request.onsuccess = () => resolve(request.result ?? null);
|
|
36
|
+
request.onerror = () => reject(request.error ?? new Error("entry: get request failed"));
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
db.close();
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
async saveStored(manifest) {
|
|
44
|
+
const db = await openDatabase(databaseName);
|
|
45
|
+
try {
|
|
46
|
+
await new Promise((resolve, reject) => {
|
|
47
|
+
const transaction = db.transaction(STORE_NAME, "readwrite");
|
|
48
|
+
transaction.objectStore(STORE_NAME).put(manifest, RECORD_KEY);
|
|
49
|
+
transaction.oncomplete = () => resolve();
|
|
50
|
+
transaction.onerror = () => reject(transaction.error);
|
|
51
|
+
// A quota failure can end the transaction with `abort` and no settled request; without this the write hangs.
|
|
52
|
+
transaction.onabort = () => reject(transaction.error);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
db.close();
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=indexeddb.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"indexeddb.js","sourceRoot":"","sources":["../../src/browser/indexeddb.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { EntryProbes } from "../decide.js";
|
|
2
|
+
export type CreateProbesOptions = {
|
|
3
|
+
readonly mountPath: string;
|
|
4
|
+
readonly fetchImpl?: typeof fetch;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Builds the `EntryProbes` port. The primary probe hits an uncached, non-precached path under the
|
|
8
|
+
* mount so a network failure cannot be masked by the platform worker's cache; any settled response
|
|
9
|
+
* (including a 404) counts as reachable. The alternate probe is a `no-cors` cross-origin request:
|
|
10
|
+
* the response is opaque and never read, so a settled promise is the only signal available.
|
|
11
|
+
*/
|
|
12
|
+
export declare function createProbes({ mountPath, fetchImpl }: CreateProbesOptions): EntryProbes;
|
|
13
|
+
//# sourceMappingURL=probes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"probes.d.ts","sourceRoot":"","sources":["../../src/browser/probes.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CACnC,CAAC;AAIF;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,EAAE,SAAS,EAAE,SAAiB,EAAE,EAAE,mBAAmB,GAAG,WAAW,CAyB/F"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const PROBE_TIMEOUT_MS = 5_000;
|
|
2
|
+
/**
|
|
3
|
+
* Builds the `EntryProbes` port. The primary probe hits an uncached, non-precached path under the
|
|
4
|
+
* mount so a network failure cannot be masked by the platform worker's cache; any settled response
|
|
5
|
+
* (including a 404) counts as reachable. The alternate probe is a `no-cors` cross-origin request:
|
|
6
|
+
* the response is opaque and never read, so a settled promise is the only signal available.
|
|
7
|
+
*/
|
|
8
|
+
export function createProbes({ mountPath, fetchImpl = fetch }) {
|
|
9
|
+
return {
|
|
10
|
+
async probePrimary() {
|
|
11
|
+
const url = `${mountPath}__pwa-entry-probe?${Math.random()}`;
|
|
12
|
+
try {
|
|
13
|
+
await fetchImpl(url, { cache: "no-store", credentials: "omit", signal: AbortSignal.timeout(PROBE_TIMEOUT_MS) });
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
async probeAlternate(origin, startPath) {
|
|
21
|
+
try {
|
|
22
|
+
await fetchImpl(`${origin}${startPath}`, {
|
|
23
|
+
mode: "no-cors",
|
|
24
|
+
credentials: "omit",
|
|
25
|
+
cache: "no-store",
|
|
26
|
+
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
|
|
27
|
+
});
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=probes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"probes.js","sourceRoot":"","sources":["../../src/browser/probes.ts"],"names":[],"mappings":"AASA,MAAM,gBAAgB,GAAG,KAAK,CAAC;AAE/B;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,EAAE,SAAS,EAAE,SAAS,GAAG,KAAK,EAAuB;IAChF,OAAO;QACL,KAAK,CAAC,YAAY;YAChB,MAAM,GAAG,GAAG,GAAG,SAAS,qBAAqB,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;YAC7D,IAAI,CAAC;gBACH,MAAM,SAAS,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,gBAAgB,CAAC,EAAE,CAAC,CAAC;gBAChH,OAAO,IAAI,CAAC;YACd,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,KAAK,CAAC;YACf,CAAC;QACH,CAAC;QACD,KAAK,CAAC,cAAc,CAAC,MAAc,EAAE,SAAiB;YACpD,IAAI,CAAC;gBACH,MAAM,SAAS,CAAC,GAAG,MAAM,GAAG,SAAS,EAAE,EAAE;oBACvC,IAAI,EAAE,SAAS;oBACf,WAAW,EAAE,MAAM;oBACnB,KAAK,EAAE,UAAU;oBACjB,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,gBAAgB,CAAC;iBAC9C,CAAC,CAAC;gBACH,OAAO,IAAI,CAAC;YACd,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,KAAK,CAAC;YACf,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC"}
|
package/dist/check.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { EntryDiagnostic } from "./diagnostics.js";
|
|
2
|
+
import type { EntryRuntimeConfig, EntryRuntimePorts } from "./resolve.js";
|
|
3
|
+
export type { EntryRuntimeConfig, EntryRuntimePorts } from "./resolve.js";
|
|
4
|
+
export type EntryRecoveryResult = {
|
|
5
|
+
readonly kind: "none";
|
|
6
|
+
readonly diagnostics: readonly EntryDiagnostic[];
|
|
7
|
+
} | {
|
|
8
|
+
readonly kind: "available";
|
|
9
|
+
readonly status: "migrating" | "incident" | "unconfirmed-outage";
|
|
10
|
+
readonly reason: {
|
|
11
|
+
readonly code: string;
|
|
12
|
+
readonly message: string | null;
|
|
13
|
+
};
|
|
14
|
+
readonly expiresAt: string;
|
|
15
|
+
readonly recoveryPageUrl: string;
|
|
16
|
+
readonly diagnostics: readonly EntryDiagnostic[];
|
|
17
|
+
};
|
|
18
|
+
export declare function runEntryRecovery(config: EntryRuntimeConfig, ports: EntryRuntimePorts, options?: {
|
|
19
|
+
readonly returnPath?: string;
|
|
20
|
+
}): Promise<EntryRecoveryResult>;
|
|
21
|
+
//# sourceMappingURL=check.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../src/check.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAGxD,OAAO,KAAK,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAE1E,YAAY,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAE1E,MAAM,MAAM,mBAAmB,GAC3B;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,WAAW,EAAE,SAAS,eAAe,EAAE,CAAA;CAAE,GAC3E;IACE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,UAAU,GAAG,oBAAoB,CAAC;IACjE,QAAQ,CAAC,MAAM,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAC5E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,WAAW,EAAE,SAAS,eAAe,EAAE,CAAC;CAClD,CAAC;AA2BN,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,kBAAkB,EAC1B,KAAK,EAAE,iBAAiB,EACxB,OAAO,GAAE;IAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAO,GAC7C,OAAO,CAAC,mBAAmB,CAAC,CAqB9B"}
|
package/dist/check.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Orchestration and the page-side public API: spec/pwa-entry-resilience.md's "页面侧 API" and
|
|
2
|
+
// "存储" sections. `runEntryRecovery` never throws — every port failure becomes a diagnostic (or, for
|
|
3
|
+
// `saveStored`, a diagnostic that does not change the result) — and the result never carries an
|
|
4
|
+
// alternate-entry address: only a link to the platform recovery page, which repeats the whole
|
|
5
|
+
// selection and decision on its own (see src/resolve.ts, which this function is built on).
|
|
6
|
+
import { diagnostic } from "./diagnostics.js";
|
|
7
|
+
import { normalizeReturnPath } from "./return-path.js";
|
|
8
|
+
import { resolveEntryRecovery } from "./resolve.js";
|
|
9
|
+
/** Runs `fn`, returning `undefined` if it throws. */
|
|
10
|
+
function tryCall(fn) {
|
|
11
|
+
try {
|
|
12
|
+
return fn();
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function buildRecoveryPageUrl(config, currentOrigin, returnPath, diagnostics) {
|
|
19
|
+
if (returnPath === undefined)
|
|
20
|
+
return config.recoveryPagePath;
|
|
21
|
+
const normalized = normalizeReturnPath(returnPath, { origin: currentOrigin, scope: config.scope });
|
|
22
|
+
if (normalized === null) {
|
|
23
|
+
diagnostics.push(diagnostic("entry.return-path-dropped", ""));
|
|
24
|
+
return config.recoveryPagePath;
|
|
25
|
+
}
|
|
26
|
+
return `${config.recoveryPagePath}?return=${encodeURIComponent(normalized)}`;
|
|
27
|
+
}
|
|
28
|
+
export async function runEntryRecovery(config, ports, options = {}) {
|
|
29
|
+
const resolution = await resolveEntryRecovery(config, ports);
|
|
30
|
+
const diagnostics = [...resolution.diagnostics];
|
|
31
|
+
if (resolution.kind === "none")
|
|
32
|
+
return { kind: "none", diagnostics };
|
|
33
|
+
// `resolveEntryRecovery` already required `ports.currentOrigin()` to succeed once to get this far; it is called
|
|
34
|
+
// again here only because building the recovery page's URL is this function's own concern, not
|
|
35
|
+
// `EntryResolution`'s. A second failure (there is no reason to expect one from a port that just succeeded) still
|
|
36
|
+
// fails closed on the return path rather than throwing: the recovery link is built without it.
|
|
37
|
+
const currentOrigin = tryCall(() => ports.currentOrigin()) ?? "";
|
|
38
|
+
const recoveryPageUrl = buildRecoveryPageUrl(config, currentOrigin, options.returnPath, diagnostics);
|
|
39
|
+
return {
|
|
40
|
+
kind: "available",
|
|
41
|
+
status: resolution.status,
|
|
42
|
+
reason: { code: resolution.manifest.reason.code, message: resolution.manifest.reason.message ?? null },
|
|
43
|
+
expiresAt: resolution.manifest.expiresAt,
|
|
44
|
+
recoveryPageUrl,
|
|
45
|
+
diagnostics,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=check.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check.js","sourceRoot":"","sources":["../src/check.ts"],"names":[],"mappings":"AAAA,2FAA2F;AAC3F,oGAAoG;AACpG,gGAAgG;AAChG,8FAA8F;AAC9F,2FAA2F;AAC3F,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAgBpD,qDAAqD;AACrD,SAAS,OAAO,CAAI,EAAW;IAC7B,IAAI,CAAC;QACH,OAAO,EAAE,EAAE,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,SAAS,oBAAoB,CAC3B,MAA0B,EAC1B,aAAqB,EACrB,UAA8B,EAC9B,WAA8B;IAE9B,IAAI,UAAU,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC,gBAAgB,CAAC;IAE7D,MAAM,UAAU,GAAG,mBAAmB,CAAC,UAAU,EAAE,EAAE,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;IACnG,IAAI,UAAU,KAAK,IAAI,EAAE,CAAC;QACxB,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC,2BAA2B,EAAE,EAAE,CAAC,CAAC,CAAC;QAC9D,OAAO,MAAM,CAAC,gBAAgB,CAAC;IACjC,CAAC;IACD,OAAO,GAAG,MAAM,CAAC,gBAAgB,WAAW,kBAAkB,CAAC,UAAU,CAAC,EAAE,CAAC;AAC/E,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,MAA0B,EAC1B,KAAwB,EACxB,UAA4C,EAAE;IAE9C,MAAM,UAAU,GAAG,MAAM,oBAAoB,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAC7D,MAAM,WAAW,GAAG,CAAC,GAAG,UAAU,CAAC,WAAW,CAAC,CAAC;IAEhD,IAAI,UAAU,CAAC,IAAI,KAAK,MAAM;QAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;IAErE,gHAAgH;IAChH,+FAA+F;IAC/F,iHAAiH;IACjH,+FAA+F;IAC/F,MAAM,aAAa,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,aAAa,EAAE,CAAC,IAAI,EAAE,CAAC;IACjE,MAAM,eAAe,GAAG,oBAAoB,CAAC,MAAM,EAAE,aAAa,EAAE,OAAO,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;IAErG,OAAO;QACL,IAAI,EAAE,WAAW;QACjB,MAAM,EAAE,UAAU,CAAC,MAAM;QACzB,MAAM,EAAE,EAAE,IAAI,EAAE,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,EAAE,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,IAAI,IAAI,EAAE;QACtG,SAAS,EAAE,UAAU,CAAC,QAAQ,CAAC,SAAS;QACxC,eAAe;QACf,WAAW;KACZ,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { EntryRecoveryResult } from "../check.js";
|
|
2
|
+
import type { EntryManifestUpdateResult as EntryUpdateResult } from "../update.js";
|
|
3
|
+
import type { EntryCheckOptions } from "../browser/index.js";
|
|
4
|
+
export type { EntryUpdateResult };
|
|
5
|
+
export type PwaTheme = "light" | "dark" | "system";
|
|
6
|
+
/** Runs the page-side entry-recovery check. Never throws — see `runEntryRecovery`'s docstring. */
|
|
7
|
+
export declare function checkEntryRecovery(options?: EntryCheckOptions): Promise<EntryRecoveryResult>;
|
|
8
|
+
/** Accepts a manifest the application obtained and decrypted itself. Never throws — see
|
|
9
|
+
* `runEntryManifestUpdate`'s docstring. */
|
|
10
|
+
export declare function updateEntryManifest(data: unknown): Promise<EntryUpdateResult>;
|
|
11
|
+
/**
|
|
12
|
+
* Sets the app's theme preference, read by the recovery page (src/page/main.ts) before its first render — see
|
|
13
|
+
* spec/pwa-entry-resilience.md's "跟随宿主应用的主题设置". Writes `"light"`/`"dark"` to the storage key
|
|
14
|
+
* `themeStorageKey` builds; `"system"` removes the key instead, so the recovery page falls back to the system
|
|
15
|
+
* preference. Never throws: `localStorage` access is wrapped, and a failure (privacy mode, disabled storage, quota)
|
|
16
|
+
* is treated the same as the preference being absent.
|
|
17
|
+
*/
|
|
18
|
+
export declare function setPwaTheme(theme: PwaTheme): void;
|
|
19
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAEvD,OAAO,KAAK,EAAE,yBAAyB,IAAI,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEnF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAG7D,YAAY,EAAE,iBAAiB,EAAE,CAAC;AAElC,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;AAYnD,kGAAkG;AAClG,wBAAgB,kBAAkB,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAE5F;AAED;4CAC4C;AAC5C,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAE7E;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,CAQjD"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Page-side entry for apps: spec/pwa-entry-resilience.md's revised "页面侧 API". Reads the plugin-provided virtual
|
|
2
|
+
// module once and shares one lazily-created set of browser ports (IndexedDB, `Date.now`, probes — see
|
|
3
|
+
// src/browser/index.ts's `createEntryRuntimePorts`) between both page-side functions: `checkEntryRecovery`
|
|
4
|
+
// (src/check.ts's `runEntryRecovery`) and `updateEntryManifest` (src/update.ts's `runEntryManifestUpdate`). Neither
|
|
5
|
+
// wrapper builds its own ports — a second IndexedDB store or probe set per call would be wasted work for no benefit,
|
|
6
|
+
// since both read/write the same `pwa-entry:<appId>:<environment>` database.
|
|
7
|
+
//
|
|
8
|
+
// Per ADR-0018's import boundary, this file's only non-relative import is the virtual module; everything else it
|
|
9
|
+
// needs is a relative import into the rest of this package (see src/virtual.d.ts for the module's declared type).
|
|
10
|
+
import config from "virtual:pwa-entry-config";
|
|
11
|
+
import { runEntryRecovery } from "../check.js";
|
|
12
|
+
import { runEntryManifestUpdate } from "../update.js";
|
|
13
|
+
import { createEntryRuntimePorts } from "../browser/index.js";
|
|
14
|
+
import { themeStorageKey } from "../internal/theme-key.js";
|
|
15
|
+
let ports;
|
|
16
|
+
/** Builds the shared browser ports on first use and returns the same instance on every later call. */
|
|
17
|
+
function sharedPorts() {
|
|
18
|
+
ports ??= createEntryRuntimePorts(config);
|
|
19
|
+
return ports;
|
|
20
|
+
}
|
|
21
|
+
/** Runs the page-side entry-recovery check. Never throws — see `runEntryRecovery`'s docstring. */
|
|
22
|
+
export function checkEntryRecovery(options) {
|
|
23
|
+
return runEntryRecovery(config, sharedPorts(), options);
|
|
24
|
+
}
|
|
25
|
+
/** Accepts a manifest the application obtained and decrypted itself. Never throws — see
|
|
26
|
+
* `runEntryManifestUpdate`'s docstring. */
|
|
27
|
+
export function updateEntryManifest(data) {
|
|
28
|
+
return runEntryManifestUpdate(data, config, sharedPorts());
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Sets the app's theme preference, read by the recovery page (src/page/main.ts) before its first render — see
|
|
32
|
+
* spec/pwa-entry-resilience.md's "跟随宿主应用的主题设置". Writes `"light"`/`"dark"` to the storage key
|
|
33
|
+
* `themeStorageKey` builds; `"system"` removes the key instead, so the recovery page falls back to the system
|
|
34
|
+
* preference. Never throws: `localStorage` access is wrapped, and a failure (privacy mode, disabled storage, quota)
|
|
35
|
+
* is treated the same as the preference being absent.
|
|
36
|
+
*/
|
|
37
|
+
export function setPwaTheme(theme) {
|
|
38
|
+
try {
|
|
39
|
+
const key = themeStorageKey(config.appId, config.environment);
|
|
40
|
+
if (theme === "system")
|
|
41
|
+
localStorage.removeItem(key);
|
|
42
|
+
else
|
|
43
|
+
localStorage.setItem(key, theme);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Storage unavailable — silently no-op, per the docstring above.
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,gHAAgH;AAChH,sGAAsG;AACtG,2GAA2G;AAC3G,oHAAoH;AACpH,qHAAqH;AACrH,6EAA6E;AAC7E,EAAE;AACF,iHAAiH;AACjH,kHAAkH;AAClH,OAAO,MAAM,MAAM,0BAA0B,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE/C,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAEtD,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAE9D,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAQ3D,IAAI,KAA6B,CAAC;AAElC,sGAAsG;AACtG,SAAS,WAAW;IAClB,KAAK,KAAK,uBAAuB,CAAC,MAAM,CAAC,CAAC;IAC1C,OAAO,KAAK,CAAC;AACf,CAAC;AAED,kGAAkG;AAClG,MAAM,UAAU,kBAAkB,CAAC,OAA2B;IAC5D,OAAO,gBAAgB,CAAC,MAAM,EAAE,WAAW,EAAE,EAAE,OAAO,CAAC,CAAC;AAC1D,CAAC;AAED;4CAC4C;AAC5C,MAAM,UAAU,mBAAmB,CAAC,IAAa;IAC/C,OAAO,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,CAAC;AAC7D,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,KAAe;IACzC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,eAAe,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC;QAC9D,IAAI,KAAK,KAAK,QAAQ;YAAE,YAAY,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;;YAChD,YAAY,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACxC,CAAC;IAAC,MAAM,CAAC;QACP,iEAAiE;IACnE,CAAC;AACH,CAAC"}
|
package/dist/decide.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { EntryDiagnostic } from "./diagnostics.js";
|
|
2
|
+
import type { EntryManifest, EntryManifestEntry } from "./types.js";
|
|
3
|
+
export type EntryProbes = {
|
|
4
|
+
probePrimary(): Promise<boolean>;
|
|
5
|
+
probeAlternate(origin: string, startPath: string): Promise<boolean>;
|
|
6
|
+
};
|
|
7
|
+
export type EntryDecision = {
|
|
8
|
+
readonly kind: "none";
|
|
9
|
+
readonly diagnostics: readonly EntryDiagnostic[];
|
|
10
|
+
} | {
|
|
11
|
+
readonly kind: "available";
|
|
12
|
+
readonly status: "migrating" | "incident" | "unconfirmed-outage";
|
|
13
|
+
readonly manifest: EntryManifest;
|
|
14
|
+
/** Only the entries the spec allows showing: every entry for `migrating`/`incident` (not probed), or only the
|
|
15
|
+
* probe-confirmed-reachable ones for `unconfirmed-outage` (see spec's "只展示探测确认可达的入口"). */
|
|
16
|
+
readonly entries: readonly EntryManifestEntry[];
|
|
17
|
+
};
|
|
18
|
+
export declare function decideRecovery(manifest: EntryManifest, probes: EntryProbes): Promise<EntryDecision>;
|
|
19
|
+
//# sourceMappingURL=decide.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decide.d.ts","sourceRoot":"","sources":["../src/decide.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEpE,MAAM,MAAM,WAAW,GAAG;IACxB,YAAY,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IACjC,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACrE,CAAC;AAEF,MAAM,MAAM,aAAa,GACrB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,WAAW,EAAE,SAAS,eAAe,EAAE,CAAA;CAAE,GAC3E;IACE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,UAAU,GAAG,oBAAoB,CAAC;IACjE,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;IACjC;+FAC2F;IAC3F,QAAQ,CAAC,OAAO,EAAE,SAAS,kBAAkB,EAAE,CAAC;CACjD,CAAC;AAWN,wBAAsB,cAAc,CAAC,QAAQ,EAAE,aAAa,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,aAAa,CAAC,CAuBzG"}
|
package/dist/decide.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Display decision for a single already-selected manifest, per spec/pwa-entry-resilience.md's
|
|
2
|
+
// "状态与展示规则". Pure orchestration over injected probes: no clock, no storage, no network of its
|
|
3
|
+
// own, so every branch (empty entries, migrating/incident short-circuiting, normal's probe-then-probe
|
|
4
|
+
// sequencing) is directly testable.
|
|
5
|
+
import { diagnostic } from "./diagnostics.js";
|
|
6
|
+
/** A probe that throws is treated as unreachable, per spec: "探测函数抛出时视为不可达". */
|
|
7
|
+
async function safeProbe(probe) {
|
|
8
|
+
try {
|
|
9
|
+
return await probe();
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export async function decideRecovery(manifest, probes) {
|
|
16
|
+
if (manifest.entries.length === 0) {
|
|
17
|
+
return { kind: "none", diagnostics: [diagnostic("entry.no-entries", "")] };
|
|
18
|
+
}
|
|
19
|
+
if (manifest.status === "migrating" || manifest.status === "incident") {
|
|
20
|
+
return { kind: "available", status: manifest.status, manifest, entries: manifest.entries };
|
|
21
|
+
}
|
|
22
|
+
// status === "normal": show nothing unless the primary is unreachable and some alternate is reachable. Every
|
|
23
|
+
// entry is probed — not only up to the first reachable one — so the page can show every entry that is actually
|
|
24
|
+
// confirmed reachable, per spec's "只展示探测确认可达的入口" (independent review finding, 2026-09-17).
|
|
25
|
+
const primaryReachable = await safeProbe(() => probes.probePrimary());
|
|
26
|
+
if (primaryReachable)
|
|
27
|
+
return { kind: "none", diagnostics: [] };
|
|
28
|
+
const reachable = [];
|
|
29
|
+
for (const entry of manifest.entries) {
|
|
30
|
+
const alternateReachable = await safeProbe(() => probes.probeAlternate(entry.origin, entry.startPath));
|
|
31
|
+
if (alternateReachable)
|
|
32
|
+
reachable.push(entry);
|
|
33
|
+
}
|
|
34
|
+
if (reachable.length === 0)
|
|
35
|
+
return { kind: "none", diagnostics: [] };
|
|
36
|
+
return { kind: "available", status: "unconfirmed-outage", manifest, entries: reachable };
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=decide.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decide.js","sourceRoot":"","sources":["../src/decide.ts"],"names":[],"mappings":"AAAA,8FAA8F;AAC9F,8FAA8F;AAC9F,sGAAsG;AACtG,oCAAoC;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAoB9C,+EAA+E;AAC/E,KAAK,UAAU,SAAS,CAAC,KAA6B;IACpD,IAAI,CAAC;QACH,OAAO,MAAM,KAAK,EAAE,CAAC;IACvB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,QAAuB,EAAE,MAAmB;IAC/E,IAAI,QAAQ,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAClC,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,UAAU,CAAC,kBAAkB,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC;IAC7E,CAAC;IAED,IAAI,QAAQ,CAAC,MAAM,KAAK,WAAW,IAAI,QAAQ,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;QACtE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,CAAC;IAC7F,CAAC;IAED,6GAA6G;IAC7G,+GAA+G;IAC/G,2FAA2F;IAC3F,MAAM,gBAAgB,GAAG,MAAM,SAAS,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC,CAAC;IACtE,IAAI,gBAAgB;QAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC;IAE/D,MAAM,SAAS,GAAyB,EAAE,CAAC;IAC3C,KAAK,MAAM,KAAK,IAAI,QAAQ,CAAC,OAAO,EAAE,CAAC;QACrC,MAAM,kBAAkB,GAAG,MAAM,SAAS,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;QACvG,IAAI,kBAAkB;YAAE,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAChD,CAAC;IAED,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC;IACrE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,oBAAoB,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;AAC3F,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const ENTRY_DIAGNOSTIC_CODES: readonly ["entry.manifest-invalid-shape", "entry.app-id-mismatch", "entry.environment-mismatch", "entry.sequence-invalid", "entry.expires-at-invalid", "entry.expired", "entry.validity-period-too-long", "entry.status-invalid", "entry.reason-invalid-shape", "entry.reason-code-invalid", "entry.reason-message-invalid", "entry.entries-too-many", "entry.entry-invalid-shape", "entry.entry-origin-invalid", "entry.entry-start-path-invalid", "entry.sequence-not-greater", "entry.no-entries", "entry.storage-unavailable", "entry.storage-write-failed", "entry.runtime-unavailable", "entry.return-path-dropped", "entry.max-validity-days-invalid", "entry.css-invalid", "entry.option-removed", "entry.locale-invalid", "entry.message-invalid", "entry.base-mismatch", "entry.platform-plugin-missing", "entry.platform-plan-unavailable", "entry.recovery-page-not-precached"];
|
|
2
|
+
export type EntryDiagnosticCode = (typeof ENTRY_DIAGNOSTIC_CODES)[number];
|
|
3
|
+
/** JSON Pointer (RFC 6901) into the value being checked; `""` addresses the root. */
|
|
4
|
+
export type EntryDiagnosticPath = "" | `/${string}`;
|
|
5
|
+
/** Machine-readable finding. Carries no message and must never be built from an input value. */
|
|
6
|
+
export type EntryDiagnostic = {
|
|
7
|
+
readonly code: EntryDiagnosticCode;
|
|
8
|
+
readonly path: EntryDiagnosticPath;
|
|
9
|
+
};
|
|
10
|
+
export declare function diagnostic(code: EntryDiagnosticCode, path: EntryDiagnosticPath): EntryDiagnostic;
|
|
11
|
+
//# sourceMappingURL=diagnostics.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diagnostics.d.ts","sourceRoot":"","sources":["../src/diagnostics.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,sBAAsB,61BAqCzB,CAAC;AAEX,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE1E,qFAAqF;AACrF,MAAM,MAAM,mBAAmB,GAAG,EAAE,GAAG,IAAI,MAAM,EAAE,CAAC;AAEpD,gGAAgG;AAChG,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IACnC,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;CACpC,CAAC;AAEF,wBAAgB,UAAU,CAAC,IAAI,EAAE,mBAAmB,EAAE,IAAI,EAAE,mBAAmB,GAAG,eAAe,CAEhG"}
|