@microbt/pwa 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,59 @@
1
- # Temporary Holding Version
1
+ # @microbt/pwa
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 帮助中心(cinmoore-support-center 0.15.16)的 Service Worker 运行时与页面端注册,原样抽成的 npm 包。行为契约与原实现逐字一致(缓存名、消息类型、注册地址格式、计数名、退避参数)。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ pnpm add @microbt/pwa
9
+ ```
10
+
11
+ 需要 peer 依赖 `@microbt/environment`(ARMS 上报)。构建侧仍由接入方自备 workbox 的 `InjectManifest` 插件(配置不用改,见下)。
12
+
13
+ 三个入口:`.`(页面端注册)、`./protocol`(诊断协议,纯常量与纯函数,依赖链不含 `@microbt/environment`)、`./sw`(SW 端运行时)。
14
+
15
+ ## 接入
16
+
17
+ **1. SW 侧**:`src/sw.ts` 三行写法(`InjectManifest` 仍由接入方配置,注入点保持在接入方产物中,包内不含 `__WB_MANIFEST`):
18
+
19
+ ```ts
20
+ import { setupServiceWorker } from '@microbt/pwa/sw';
21
+
22
+ declare const RELEASE: string;
23
+ console.log(self.__WB_MANIFEST); // InjectManifest 注入点,保留原样
24
+ setupServiceWorker({ release: typeof RELEASE === 'undefined' ? '' : RELEASE });
25
+ ```
26
+
27
+ **2. 页面侧**(如 `bootstrap.ts`,原 `schedulePwaRegistration()` 无参调用改为传参):
28
+
29
+ ```ts
30
+ import { schedulePwaRegistration } from '@microbt/pwa';
31
+
32
+ schedulePwaRegistration({ version: RELEASE, log });
33
+ ```
34
+
35
+ **3. 协议符号**(诊断协议 `CHUNK_TRACE_MESSAGE`、`sanitizeChunkUrl`、`createChunkDiagnosticId`、`type ChunkRequestTrace`)从子入口导入:
36
+
37
+ ```ts
38
+ import { CHUNK_TRACE_MESSAGE } from '@microbt/pwa/protocol';
39
+ ```
40
+
41
+ ## 行为契约
42
+
43
+ 以下字符串/参数与帮助中心 0.15.16 一字不差:
44
+
45
+ - 缓存名前缀 `cache-pages-`、`cache-static-assets-`、`cache-js-css-`;激活时 `caches.delete('pages')`
46
+ - 消息类型 `SW_ERROR`、`SW_UNHANDLED_REJECTION`、`SW_CHUNK_REQUEST_TRACE`
47
+ - 注册地址 `/sw.js?baseUrl=<CDN 版本目录>&v=<构建版本>`(`v` 随构建版本变化是离线 SW 更新通道)
48
+ - 计数名 `PWA-*` 全系;行为名 `pwa-register-retry`(退避 30s → 2min → 8min,共 3 次)
49
+ - 导航请求 networkTimeoutSeconds 3;`requestIdleCallback` 超时 3000ms
50
+ - `schedulePwaRegistration` 单例语义:同一页面第二次调用不重复调度
51
+
52
+ ## 开发
53
+
54
+ ```bash
55
+ pnpm install
56
+ pnpm test # vitest run
57
+ pnpm build # father build(dist/esm)
58
+ pnpm lint # eslint
59
+ ```
@@ -0,0 +1,2 @@
1
+ export { schedulePwaRegistration } from './shared/pwa';
2
+ export { SW_REJECTION_METRICS } from './sw-error-filter';
@@ -0,0 +1,7 @@
1
+ // src/index.ts
2
+ import { schedulePwaRegistration } from "./shared/pwa";
3
+ import { SW_REJECTION_METRICS } from "./sw-error-filter";
4
+ export {
5
+ SW_REJECTION_METRICS,
6
+ schedulePwaRegistration
7
+ };
@@ -0,0 +1,2 @@
1
+ export { CHUNK_TRACE_MESSAGE, createChunkDiagnosticId, sanitizeChunkUrl, } from './shared/chunk-diagnostics';
2
+ export type { ChunkRequestTrace } from './shared/chunk-diagnostics';
@@ -0,0 +1,11 @@
1
+ // src/protocol.ts
2
+ import {
3
+ CHUNK_TRACE_MESSAGE,
4
+ createChunkDiagnosticId,
5
+ sanitizeChunkUrl
6
+ } from "./shared/chunk-diagnostics";
7
+ export {
8
+ CHUNK_TRACE_MESSAGE,
9
+ createChunkDiagnosticId,
10
+ sanitizeChunkUrl
11
+ };
@@ -0,0 +1,26 @@
1
+ /** 页面与 SW 共用的有限、脱敏诊断协议;不携带请求头、正文或查询参数。 */
2
+ export interface ChunkRequestTrace {
3
+ requestId: string;
4
+ at: number;
5
+ completedAt?: number;
6
+ resource: string;
7
+ phase: 'response' | 'complete';
8
+ destination: string;
9
+ mode: string;
10
+ credentials: string;
11
+ cache: 'hit' | 'miss' | 'unknown';
12
+ network: 'pending' | 'response' | 'error';
13
+ networkStatus?: number;
14
+ networkType?: string;
15
+ networkMime?: string;
16
+ responseStatus?: number;
17
+ responseType?: string;
18
+ responseMime?: string;
19
+ servedFrom?: 'cache' | 'network' | 'none';
20
+ errorName?: string;
21
+ swRelease: string;
22
+ }
23
+ export declare const CHUNK_TRACE_MESSAGE = "SW_CHUNK_REQUEST_TRACE";
24
+ /** URL 凭证、query、hash 均不出本地;未知协议不记录。 */
25
+ export declare function sanitizeChunkUrl(value: string, base?: string): string;
26
+ export declare function createChunkDiagnosticId(): string;
@@ -0,0 +1,18 @@
1
+ // src/shared/chunk-diagnostics.ts
2
+ var CHUNK_TRACE_MESSAGE = "SW_CHUNK_REQUEST_TRACE";
3
+ function sanitizeChunkUrl(value, base) {
4
+ try {
5
+ const url = new URL(value, base);
6
+ return /^https?:$/.test(url.protocol) ? `${url.origin}${url.pathname}`.slice(0, 512) : "";
7
+ } catch {
8
+ return "";
9
+ }
10
+ }
11
+ function createChunkDiagnosticId() {
12
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
13
+ }
14
+ export {
15
+ CHUNK_TRACE_MESSAGE,
16
+ createChunkDiagnosticId,
17
+ sanitizeChunkUrl
18
+ };
@@ -0,0 +1,4 @@
1
+ export type RegistrationStage = 'load-workbox' | 'resolve-url' | 'register-sw';
2
+ export declare function registrationErrorMessage(reason: unknown): string;
3
+ export declare function createRegistrationError(reason: unknown, stage: RegistrationStage): Error;
4
+ export declare function countRegistrationFailure(stage: RegistrationStage): void;
@@ -0,0 +1,38 @@
1
+ // src/shared/pwa-registration-telemetry.ts
2
+ import { arms } from "@microbt/environment";
3
+ var STAGE_COUNTERS = {
4
+ "load-workbox": "PWA-REGISTER-FAILED-LOAD-WORKBOX",
5
+ "resolve-url": "PWA-REGISTER-FAILED-RESOLVE-URL",
6
+ "register-sw": "PWA-REGISTER-FAILED-REGISTER-SW"
7
+ };
8
+ function registrationErrorMessage(reason) {
9
+ if (reason instanceof Error && reason.message)
10
+ return reason.message;
11
+ if (typeof reason === "string" && reason.trim())
12
+ return reason;
13
+ if (reason && typeof reason === "object" && "message" in reason) {
14
+ try {
15
+ if (typeof reason.message === "string" && reason.message.trim()) {
16
+ return reason.message;
17
+ }
18
+ } catch {
19
+ }
20
+ }
21
+ return "Unknown registration failure";
22
+ }
23
+ function createRegistrationError(reason, stage) {
24
+ const error = new Error(`PWA Register Error [${stage}]: ${registrationErrorMessage(reason)}`, {
25
+ cause: reason
26
+ });
27
+ error.name = "ServiceWorkerRegistrationError";
28
+ return error;
29
+ }
30
+ function countRegistrationFailure(stage) {
31
+ arms.sum("PWA-REGISTER-FAILED");
32
+ arms.sum(STAGE_COUNTERS[stage]);
33
+ }
34
+ export {
35
+ countRegistrationFailure,
36
+ createRegistrationError,
37
+ registrationErrorMessage
38
+ };
@@ -0,0 +1,26 @@
1
+ import type { PwaLog } from './pwa';
2
+ /**
3
+ * SW 注册失败的重试调度与上报去重。
4
+ * - 退避重试:仅提供恢复机会,实际恢复以成功注册为准
5
+ * - 会话内去重上报:网关故障期间每个 PV 刷新都会注册失败,不去重会按
6
+ * PV 刷出大量重复 error(实测 8 天 8865 条 / 5656 用户)
7
+ */
8
+ export declare class SwRegistrationRetry {
9
+ private readonly isRegistrationCompleted;
10
+ private readonly retryRegistration;
11
+ private readonly log;
12
+ private retryCount;
13
+ private retryHandle;
14
+ private waitingForReadiness;
15
+ private stopped;
16
+ private firstFailureAt;
17
+ constructor(isRegistrationCompleted: () => boolean, retryRegistration: () => void, log: PwaLog);
18
+ reportErrorOnce(error: Error): void;
19
+ scheduleRetry(): void;
20
+ private runWhenReady;
21
+ private handleReadiness;
22
+ private detachReadiness;
23
+ private reportOutcome;
24
+ stop(): void;
25
+ onSuccess(): void;
26
+ }
@@ -0,0 +1,133 @@
1
+ // src/shared/pwa-retry.ts
2
+ import { arms } from "@microbt/environment";
3
+ import { sanitizeChunkUrl } from "./chunk-diagnostics";
4
+ var RETRY_DELAYS_MS = [3e4, 12e4, 48e4];
5
+ var BEHAVIOR_NAME = "pwa-register-retry";
6
+ function reportRetryBehavior(detail) {
7
+ var _a;
8
+ const logger = window.__bl;
9
+ try {
10
+ if (typeof (logger == null ? void 0 : logger.addBehavior) === "function" && typeof logger.reportBehavior === "function") {
11
+ logger.addBehavior({
12
+ data: {
13
+ name: BEHAVIOR_NAME,
14
+ message: JSON.stringify(detail)
15
+ },
16
+ // URL 凭证、query、hash 不出本地:page 只带 origin+pathname。
17
+ page: sanitizeChunkUrl(((_a = window.location) == null ? void 0 : _a.href) ?? "")
18
+ });
19
+ logger.reportBehavior();
20
+ }
21
+ } catch {
22
+ }
23
+ }
24
+ var SwRegistrationRetry = class {
25
+ constructor(isRegistrationCompleted, retryRegistration, log) {
26
+ this.isRegistrationCompleted = isRegistrationCompleted;
27
+ this.retryRegistration = retryRegistration;
28
+ this.log = log;
29
+ this.retryCount = 0;
30
+ this.retryHandle = null;
31
+ this.waitingForReadiness = false;
32
+ this.stopped = false;
33
+ this.firstFailureAt = null;
34
+ this.runWhenReady = () => {
35
+ if (this.stopped || this.isRegistrationCompleted()) {
36
+ this.stop();
37
+ return;
38
+ }
39
+ if (document.visibilityState !== "visible" || !navigator.onLine) {
40
+ if (!this.waitingForReadiness) {
41
+ this.waitingForReadiness = true;
42
+ window.addEventListener("online", this.handleReadiness);
43
+ document.addEventListener("visibilitychange", this.handleReadiness);
44
+ }
45
+ return;
46
+ }
47
+ this.detachReadiness();
48
+ this.retryCount += 1;
49
+ this.log(`Service Worker registration retry #${this.retryCount}`);
50
+ this.retryRegistration();
51
+ };
52
+ this.handleReadiness = () => {
53
+ if (this.waitingForReadiness)
54
+ this.runWhenReady();
55
+ };
56
+ }
57
+ reportErrorOnce(error) {
58
+ const key = `pwa-register-error:${error.message}`;
59
+ try {
60
+ if (window.sessionStorage.getItem(key) !== null) {
61
+ return;
62
+ }
63
+ window.sessionStorage.setItem(key, "1");
64
+ } catch {
65
+ }
66
+ arms.error(error);
67
+ }
68
+ scheduleRetry() {
69
+ if (this.stopped || this.isRegistrationCompleted()) {
70
+ this.stop();
71
+ return;
72
+ }
73
+ this.firstFailureAt ??= Date.now();
74
+ if (this.retryCount >= RETRY_DELAYS_MS.length) {
75
+ this.stop();
76
+ this.reportOutcome("exhausted", "PWA-REGISTER-RETRY-EXHAUSTED");
77
+ return;
78
+ }
79
+ if (this.retryHandle !== null || this.waitingForReadiness) {
80
+ return;
81
+ }
82
+ const delay = RETRY_DELAYS_MS[this.retryCount];
83
+ this.retryHandle = window.setTimeout(() => {
84
+ this.retryHandle = null;
85
+ this.runWhenReady();
86
+ }, delay);
87
+ }
88
+ detachReadiness() {
89
+ if (!this.waitingForReadiness)
90
+ return;
91
+ this.waitingForReadiness = false;
92
+ window.removeEventListener("online", this.handleReadiness);
93
+ document.removeEventListener("visibilitychange", this.handleReadiness);
94
+ }
95
+ reportOutcome(outcome, counter) {
96
+ const detail = {
97
+ outcome,
98
+ attempts: this.retryCount + 1,
99
+ elapsedMs: Date.now() - (this.firstFailureAt ?? Date.now())
100
+ };
101
+ for (const report of [
102
+ () => arms.sum(counter),
103
+ () => reportRetryBehavior(detail),
104
+ () => this.log(
105
+ `Service Worker retry ${outcome}: attempts=${detail.attempts}, elapsedMs=${detail.elapsedMs}`
106
+ )
107
+ ]) {
108
+ try {
109
+ report();
110
+ } catch {
111
+ }
112
+ }
113
+ }
114
+ stop() {
115
+ this.stopped = true;
116
+ if (this.retryHandle !== null) {
117
+ window.clearTimeout(this.retryHandle);
118
+ this.retryHandle = null;
119
+ }
120
+ this.detachReadiness();
121
+ }
122
+ onSuccess() {
123
+ if (this.stopped)
124
+ return;
125
+ this.stop();
126
+ if (this.retryCount > 0) {
127
+ this.reportOutcome("recovered", "PWA-REGISTER-RETRY-SUCCESS");
128
+ }
129
+ }
130
+ };
131
+ export {
132
+ SwRegistrationRetry
133
+ };
@@ -0,0 +1,5 @@
1
+ export type PwaLog = (message: string, ...args: unknown[]) => void;
2
+ export declare const schedulePwaRegistration: (options: {
3
+ version: string;
4
+ log: PwaLog;
5
+ }) => void;
@@ -0,0 +1,231 @@
1
+ // src/shared/pwa.ts
2
+ import { arms } from "@microbt/environment";
3
+ import {
4
+ countRegistrationFailure,
5
+ createRegistrationError,
6
+ registrationErrorMessage
7
+ } from "./pwa-registration-telemetry";
8
+ import { SwRegistrationRetry } from "./pwa-retry";
9
+ import { removeInvalidRegistrations, resolveSwRegisterUrl } from "./sw-registration";
10
+ var PwaRegistrationScheduler = class {
11
+ constructor(options) {
12
+ this.options = options;
13
+ this.scheduleStarted = false;
14
+ this.firstPaintQueued = false;
15
+ this.registrationCompleted = false;
16
+ this.registrationInFlight = null;
17
+ this.idleHandle = null;
18
+ this.timeoutHandle = null;
19
+ this.visibilityListenerAttached = false;
20
+ this.onlineListenerAttached = false;
21
+ this.retry = new SwRegistrationRetry(
22
+ () => this.registrationCompleted,
23
+ () => {
24
+ void this.registerServiceWorker();
25
+ },
26
+ (message, ...args) => this.options.log(message, ...args)
27
+ );
28
+ this.clearPendingRegistrationTask = () => {
29
+ const idleWindow = this.getIdleWindow();
30
+ if (this.idleHandle !== null && idleWindow.cancelIdleCallback) {
31
+ idleWindow.cancelIdleCallback(this.idleHandle);
32
+ this.idleHandle = null;
33
+ }
34
+ if (this.timeoutHandle !== null) {
35
+ window.clearTimeout(this.timeoutHandle);
36
+ this.timeoutHandle = null;
37
+ }
38
+ };
39
+ this.detachReadinessListeners = () => {
40
+ if (this.visibilityListenerAttached) {
41
+ document.removeEventListener("visibilitychange", this.handleReadinessChange);
42
+ this.visibilityListenerAttached = false;
43
+ }
44
+ if (this.onlineListenerAttached) {
45
+ window.removeEventListener("online", this.handleReadinessChange);
46
+ this.onlineListenerAttached = false;
47
+ }
48
+ };
49
+ this.attachReadinessListeners = () => {
50
+ if (!this.visibilityListenerAttached) {
51
+ document.addEventListener("visibilitychange", this.handleReadinessChange);
52
+ this.visibilityListenerAttached = true;
53
+ }
54
+ if (!this.onlineListenerAttached) {
55
+ window.addEventListener("online", this.handleReadinessChange);
56
+ this.onlineListenerAttached = true;
57
+ }
58
+ };
59
+ this.queueRegistrationWhenIdle = () => {
60
+ if (this.registrationCompleted || this.registrationInFlight) {
61
+ this.detachReadinessListeners();
62
+ return;
63
+ }
64
+ if (document.visibilityState !== "visible" || !navigator.onLine) {
65
+ this.waitUntilPageReady();
66
+ return;
67
+ }
68
+ if (this.idleHandle !== null || this.timeoutHandle !== null) {
69
+ return;
70
+ }
71
+ const runRegistration = () => {
72
+ this.idleHandle = null;
73
+ this.timeoutHandle = null;
74
+ if (document.visibilityState !== "visible" || !navigator.onLine) {
75
+ this.waitUntilPageReady();
76
+ return;
77
+ }
78
+ void this.registerServiceWorker();
79
+ };
80
+ const idleWindow = this.getIdleWindow();
81
+ if (idleWindow.requestIdleCallback) {
82
+ this.idleHandle = idleWindow.requestIdleCallback(runRegistration, {
83
+ timeout: 3e3
84
+ });
85
+ return;
86
+ }
87
+ this.timeoutHandle = window.setTimeout(runRegistration, 3e3);
88
+ };
89
+ this.scheduleAfterFirstPaint = () => {
90
+ if (this.firstPaintQueued || this.registrationCompleted || this.registrationInFlight) {
91
+ return;
92
+ }
93
+ this.firstPaintQueued = true;
94
+ window.requestAnimationFrame(() => {
95
+ window.requestAnimationFrame(() => {
96
+ this.firstPaintQueued = false;
97
+ this.queueRegistrationWhenIdle();
98
+ });
99
+ });
100
+ };
101
+ this.waitUntilPageReady = () => {
102
+ this.clearPendingRegistrationTask();
103
+ if (document.visibilityState === "visible" && navigator.onLine) {
104
+ this.detachReadinessListeners();
105
+ this.scheduleAfterFirstPaint();
106
+ return;
107
+ }
108
+ this.attachReadinessListeners();
109
+ };
110
+ this.handleReadinessChange = () => {
111
+ if (this.registrationCompleted || this.registrationInFlight) {
112
+ this.detachReadinessListeners();
113
+ return;
114
+ }
115
+ this.waitUntilPageReady();
116
+ };
117
+ this.registerServiceWorker = async () => {
118
+ if (this.registrationCompleted) {
119
+ return;
120
+ }
121
+ if (this.registrationInFlight) {
122
+ await this.registrationInFlight;
123
+ return;
124
+ }
125
+ this.registrationInFlight = (async () => {
126
+ var _a;
127
+ let swRegisterUrl = null;
128
+ let stage = "load-workbox";
129
+ try {
130
+ const { Workbox } = await import("workbox-window");
131
+ stage = "resolve-url";
132
+ swRegisterUrl = await resolveSwRegisterUrl({
133
+ version: this.options.version
134
+ });
135
+ stage = "register-sw";
136
+ const wb = new Workbox(swRegisterUrl);
137
+ const startTime = +/* @__PURE__ */ new Date();
138
+ wb.addEventListener("installed", () => {
139
+ const endTime = +/* @__PURE__ */ new Date();
140
+ this.options.log(
141
+ `Service Worker installed:${endTime}, time taken:${endTime - startTime}`
142
+ );
143
+ });
144
+ wb.addEventListener("activated", () => {
145
+ this.options.log(
146
+ `Service Worker activated:${+/* @__PURE__ */ new Date()}, online status: ${navigator.onLine}`
147
+ );
148
+ if (!navigator.onLine) {
149
+ arms.sum("PWA-ACTIVATED-OFFLINE");
150
+ }
151
+ });
152
+ wb.addEventListener("controlling", () => {
153
+ this.options.log(`Service Worker has taken control of the current client:${+/* @__PURE__ */ new Date()}`);
154
+ });
155
+ wb.addEventListener("waiting", () => {
156
+ this.options.log(`Service Worker is waiting:${+/* @__PURE__ */ new Date()}`);
157
+ });
158
+ const res = await wb.register();
159
+ this.options.log(
160
+ `Service Worker registration successful:${startTime}, state: ${(_a = res == null ? void 0 : res.active) == null ? void 0 : _a.state}, installing: ${res == null ? void 0 : res.installing}, host:${location.host}`
161
+ );
162
+ arms.sum("PWA-REGISTER-SUCCESS");
163
+ this.retry.onSuccess();
164
+ this.registrationCompleted = true;
165
+ } catch (e) {
166
+ if (this.shouldIgnoreError(e)) {
167
+ this.retry.stop();
168
+ this.options.log(
169
+ `Service Worker registration restricted (report ignored): ${registrationErrorMessage(
170
+ e
171
+ )}`
172
+ );
173
+ return;
174
+ }
175
+ const error = createRegistrationError(e, stage);
176
+ this.retry.reportErrorOnce(error);
177
+ countRegistrationFailure(stage);
178
+ this.retry.scheduleRetry();
179
+ this.options.log(`Service Worker registration failed: ${error.message}`, error);
180
+ } finally {
181
+ this.registrationInFlight = null;
182
+ this.clearPendingRegistrationTask();
183
+ this.detachReadinessListeners();
184
+ }
185
+ if (this.registrationCompleted && swRegisterUrl !== null) {
186
+ try {
187
+ await removeInvalidRegistrations(
188
+ swRegisterUrl,
189
+ (message, ...args) => this.options.log(message, ...args)
190
+ );
191
+ } catch (e) {
192
+ this.options.log(`清理历史 SW 注册失败(不影响本次注册): ${e.message}`);
193
+ }
194
+ }
195
+ })();
196
+ await this.registrationInFlight;
197
+ };
198
+ this.schedule = () => {
199
+ if (this.scheduleStarted) {
200
+ return;
201
+ }
202
+ if (typeof window === "undefined" || typeof document === "undefined" || // WebView / opaque origin 下 serviceWorker 属性存在但值为 null,
203
+ // 必须用真值检查而非 'serviceWorker' in navigator
204
+ !navigator.serviceWorker) {
205
+ return;
206
+ }
207
+ this.scheduleStarted = true;
208
+ this.waitUntilPageReady();
209
+ };
210
+ }
211
+ shouldIgnoreError(error) {
212
+ const errorMessages = [
213
+ "Job rejected for non app-bound domain"
214
+ // iOS PWA注册限制错误
215
+ // 'The operation is insecure', // 某些浏览器安全限制错误
216
+ // 'Failed to register a ServiceWorker', // 通用注册失败错误
217
+ ];
218
+ return errorMessages.some((msg) => registrationErrorMessage(error).includes(msg));
219
+ }
220
+ getIdleWindow() {
221
+ return window;
222
+ }
223
+ };
224
+ var pwaRegistrationScheduler = null;
225
+ var schedulePwaRegistration = (options) => {
226
+ pwaRegistrationScheduler ??= new PwaRegistrationScheduler(options);
227
+ pwaRegistrationScheduler.schedule();
228
+ };
229
+ export {
230
+ schedulePwaRegistration
231
+ };
@@ -0,0 +1,11 @@
1
+ import type { PwaLog } from './pwa';
2
+ /** 解析本次应注册的 SW 地址(在线/离线/同构建复用,见 sw-script-url.ts) */
3
+ export declare function resolveSwRegisterUrl(options: {
4
+ version: string;
5
+ }): Promise<string>;
6
+ /**
7
+ * 历史注册清理:注销与本次注册地址不同(规范化比较,消除绝对/相对差异)
8
+ * 的本站 /sw.js 注册。必须在 register 成功之后调用——若 register 失败
9
+ * 直接跳过,保证任何时候不把正在使用的注册注销掉。
10
+ */
11
+ export declare function removeInvalidRegistrations(keepScriptUrl: string, log: PwaLog): Promise<void>;
@@ -0,0 +1,35 @@
1
+ // src/shared/sw-registration.ts
2
+ import { isSameOriginSwScriptUrl, normalizeScriptUrl, resolveSwScriptUrl } from "./sw-script-url";
3
+ function getRegistrationScriptUrl(registration) {
4
+ var _a, _b, _c;
5
+ return ((_a = registration.active) == null ? void 0 : _a.scriptURL) ?? ((_b = registration.waiting) == null ? void 0 : _b.scriptURL) ?? ((_c = registration.installing) == null ? void 0 : _c.scriptURL) ?? "";
6
+ }
7
+ async function resolveSwRegisterUrl(options) {
8
+ const publicPath = window.publicPath;
9
+ const registrations = await navigator.serviceWorker.getRegistrations();
10
+ const existingScriptUrls = registrations.map((registration) => getRegistrationScriptUrl(registration)).filter((scriptUrl) => scriptUrl !== "");
11
+ return resolveSwScriptUrl({
12
+ publicPath: typeof publicPath === "string" ? publicPath : void 0,
13
+ existingScriptUrls,
14
+ version: options.version,
15
+ pageOrigin: window.location.origin
16
+ });
17
+ }
18
+ async function removeInvalidRegistrations(keepScriptUrl, log) {
19
+ const pageOrigin = window.location.origin;
20
+ const keepUrl = normalizeScriptUrl(keepScriptUrl, pageOrigin);
21
+ const registrations = await navigator.serviceWorker.getRegistrations();
22
+ const staleRegistrations = registrations.filter((registration) => {
23
+ const scriptUrl = getRegistrationScriptUrl(registration);
24
+ return scriptUrl !== "" && normalizeScriptUrl(scriptUrl, pageOrigin) !== keepUrl && isSameOriginSwScriptUrl(scriptUrl, pageOrigin);
25
+ });
26
+ if (staleRegistrations.length === 0) {
27
+ return;
28
+ }
29
+ await Promise.all(staleRegistrations.map((registration) => registration.unregister()));
30
+ log(`已注销 ${staleRegistrations.length} 个非当前地址的 SW 注册`);
31
+ }
32
+ export {
33
+ removeInvalidRegistrations,
34
+ resolveSwRegisterUrl
35
+ };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * SW 注册地址(scriptURL)的构造与解析。
3
+ *
4
+ * 注册地址形如 `/sw.js?baseUrl=<CDN 版本目录>&v=<构建版本>`:
5
+ * - baseUrl:服务端转发层的版本路由参数(nginx location = /sw.js 按
6
+ * $arg_baseUrl proxy_pass 到 CDN 版本目录取 sw.js),在线 HTML 由 render
7
+ * 服务注入 window.publicPath,离线包 HTML 无 publicPath,按设计兜底为
8
+ * 字面量 'undefined',由 Native 按 path 命中本地包内 sw.js。
9
+ * - v:构建版本(RELEASE)。这是离线场景 SW 更新的关键通道——实测
10
+ * Chrome 91 WebView 中,register() 携带新 scriptURL 的安装 fetch 会被
11
+ * ServiceWorkerClient 拦截并命中 Native 本地包,而 update()/自然软更新
12
+ * 的 fetch 不经过任何 Java 拦截、直落 nginx(baseUrl=undefined 时 500)。
13
+ * 因此 scriptURL 必须随构建版本变化:新包的页面以新 URL 注册,经安装
14
+ * 通道取得新 SW 并接管;同构建重复打开时注册同一 URL,不触发重装。
15
+ */
16
+ /** 按转发层认识的格式拼 SW 注册地址(含构建版本参数) */
17
+ export declare function buildSwRegisterUrl(baseUrl: string, version: string): string;
18
+ /** 解析注册地址中的构建版本参数;无 v 参数(历史注册)返回 null */
19
+ export declare function getSwScriptVersion(scriptUrl: string, pageOrigin: string): string | null;
20
+ /** 判定注册地址是否为本站 /sw.js(用于历史注册清理的范围判断) */
21
+ export declare function isSameOriginSwScriptUrl(scriptUrl: string, pageOrigin: string): boolean;
22
+ /** 用同一 origin 规范化注册地址,消除绝对/相对形式差异后比较 */
23
+ export declare function normalizeScriptUrl(url: string, pageOrigin: string): string;
24
+ /**
25
+ * 解析本次应注册的 SW 地址(纯函数,便于单测):
26
+ * 1. 在线入口(publicPath 为协议相对或绝对 http(s) URL):注册
27
+ * baseUrl=publicPath 的当前构建地址,保持 nginx 版本转发链路。
28
+ * 2. 离线入口:若已有同构建版本(v 一致)的本站 /sw.js 注册——无论其
29
+ * baseUrl 是 CDN 基址还是 undefined 兜底——原样复用其 scriptURL,
30
+ * 在线/离线交替打开时不重装、不产生 registration 抖动。
31
+ * 3. 其余情况(仅旧构建/无 v 历史注册,或无注册):注册
32
+ * baseUrl=undefined 兜底地址 + 当前构建 v。scriptURL 与历史注册不同,
33
+ * 触发安装 fetch(被 ServiceWorkerClient 拦截、命中 Native 当前包的
34
+ * sw.js)完成离线 SW 更新;不回用旧构建的 CDN 基址,避免安装 fetch
35
+ * 未被拦截时从 CDN 取回旧版本脚本。
36
+ */
37
+ export declare function resolveSwScriptUrl({ publicPath, existingScriptUrls, version, pageOrigin, }: {
38
+ publicPath: string | undefined;
39
+ existingScriptUrls: string[];
40
+ version: string;
41
+ pageOrigin: string;
42
+ }): string;