@microbt/pwa 0.0.0-stage → 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/README.md +87 -2
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/protocol.d.ts +2 -0
- package/dist/esm/protocol.js +11 -0
- package/dist/esm/recovery.d.ts +3 -0
- package/dist/esm/recovery.js +11 -0
- package/dist/esm/shared/chunk-diagnostic-outbox.d.ts +13 -0
- package/dist/esm/shared/chunk-diagnostic-outbox.js +137 -0
- package/dist/esm/shared/chunk-diagnostic-payload.d.ts +17 -0
- package/dist/esm/shared/chunk-diagnostic-payload.js +162 -0
- package/dist/esm/shared/chunk-diagnostic-reporter.d.ts +9 -0
- package/dist/esm/shared/chunk-diagnostic-reporter.js +114 -0
- package/dist/esm/shared/chunk-diagnostics.d.ts +26 -0
- package/dist/esm/shared/chunk-diagnostics.js +18 -0
- package/dist/esm/shared/chunk-error-recovery.d.ts +51 -0
- package/dist/esm/shared/chunk-error-recovery.js +225 -0
- package/dist/esm/shared/pwa-registration-telemetry.d.ts +4 -0
- package/dist/esm/shared/pwa-registration-telemetry.js +38 -0
- package/dist/esm/shared/pwa-retry.d.ts +26 -0
- package/dist/esm/shared/pwa-retry.js +133 -0
- package/dist/esm/shared/pwa.d.ts +5 -0
- package/dist/esm/shared/pwa.js +231 -0
- package/dist/esm/shared/sw-registration.d.ts +11 -0
- package/dist/esm/shared/sw-registration.js +35 -0
- package/dist/esm/shared/sw-script-url.d.ts +42 -0
- package/dist/esm/shared/sw-script-url.js +49 -0
- package/dist/esm/sw-cache-validation.d.ts +51 -0
- package/dist/esm/sw-cache-validation.js +111 -0
- package/dist/esm/sw-chunk-diagnostics.d.ts +19 -0
- package/dist/esm/sw-chunk-diagnostics.js +124 -0
- package/dist/esm/sw-error-filter.d.ts +58 -0
- package/dist/esm/sw-error-filter.js +87 -0
- package/dist/esm/sw-polyfills.d.ts +1 -0
- package/dist/esm/sw-polyfills.js +14 -0
- package/dist/esm/sw-request-filter.d.ts +13 -0
- package/dist/esm/sw-request-filter.js +7 -0
- package/dist/esm/sw.d.ts +4 -0
- package/dist/esm/sw.js +194 -0
- package/package.json +90 -4
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChunkLoadError 应用层兜底(白屏的最后一道防线)。
|
|
3
|
+
*
|
|
4
|
+
* SW 侧已做缓存准入校验与生命周期对齐,但 chunk 加载失败仍可能发生
|
|
5
|
+
* (弱网、CDN 旧版本清理、HTML 与 chunk 版本错配)。这里监听三类信号:
|
|
6
|
+
* 1. 本应用 script/stylesheet 加载失败(error 不冒泡,必须捕获阶段监听);
|
|
7
|
+
* 2. 运行时错误消息命中 ChunkLoadError 特征;
|
|
8
|
+
* 3. 动态 import 的 unhandledrejection(webpack/vite 的 chunk 失败形态)。
|
|
9
|
+
* 防死循环:时间窗内只允许自动刷新一次;离线时刷新无意义,交给
|
|
10
|
+
* SW/Native 缓存兜底,不刷新。
|
|
11
|
+
*
|
|
12
|
+
* 遥测通过 bootstrap 注入同步回调,不再为了记日志动态下载 chunk。
|
|
13
|
+
* 刷新前发送仍可能丢失;sessionStorage 留下脱敏记录,下一次 bootstrap 补报重新进入。
|
|
14
|
+
* ROUTE-READY 只证明刷新后的路由 Suspense 边界已提交,不保证所有业务请求成功。
|
|
15
|
+
* 入口 JS 自身无法运行时仍无法观测,不能把没有结果记录当作恢复成功。
|
|
16
|
+
*
|
|
17
|
+
* 完成信号的语义分层(console-master 双层探针,与 support-center 单探针不同):
|
|
18
|
+
* - ROUTE-READY:本地静态路由页面(/login、/settings 等)的叶子组件已提交;
|
|
19
|
+
* - MICRO-APP-READY:qiankun 子应用 entry 加载+执行+挂载完成(afterMount),
|
|
20
|
+
* 不保证子应用内部异步资源全部成功;
|
|
21
|
+
* - ROUTE-REDIRECTED:恢复目标已变更/失效(典型:恢复中跳登录页、切换到
|
|
22
|
+
* 其他页面、超期),不结算为原页面恢复成功。路由监听在首次离开待恢复
|
|
23
|
+
* 路径时立即结算并清空记录,之后返回原路径不恢复——qiankun 切页不取消
|
|
24
|
+
* 未完成挂载,迟到 afterMount 到达时 previous 已空,不会误归因。
|
|
25
|
+
*
|
|
26
|
+
* 注意:依赖链不得含加载期读 window/document 的模块。
|
|
27
|
+
*/
|
|
28
|
+
type RecoverySource = 'resource' | 'runtime' | 'rejection';
|
|
29
|
+
type RecoveryOutcome = 'RELOAD' | 'SKIP-OFFLINE' | 'SKIP-GUARD' | 'SKIP-STORAGE' | 'REENTERED' | 'ROUTE-READY' | 'MICRO-APP-READY' | 'ROUTE-REDIRECTED' | 'RECURRED';
|
|
30
|
+
export type RecoveryCompletionKind = 'page' | 'micro-app';
|
|
31
|
+
interface RecoveryAttempt {
|
|
32
|
+
attemptId: string;
|
|
33
|
+
at: number;
|
|
34
|
+
page: string;
|
|
35
|
+
resource: string;
|
|
36
|
+
source: RecoverySource;
|
|
37
|
+
}
|
|
38
|
+
export interface ChunkRecoveryEvent extends RecoveryAttempt {
|
|
39
|
+
outcome: RecoveryOutcome;
|
|
40
|
+
occurredAt: number;
|
|
41
|
+
observedResource?: string;
|
|
42
|
+
observedSource?: RecoverySource;
|
|
43
|
+
}
|
|
44
|
+
/** 由完成探针调用:本地叶子路由传 'page',qiankun afterMount 传 'micro-app'。 */
|
|
45
|
+
export declare function markChunkRecoveryRouteReady(kind?: RecoveryCompletionKind): void;
|
|
46
|
+
export declare function isChunkErrorMessage(message: string): boolean;
|
|
47
|
+
export declare function isChunkErrorReason(reason: unknown): boolean;
|
|
48
|
+
/** 防死循环闸门:时间窗内只允许自动刷新一次;通过则记录本次时间 */
|
|
49
|
+
export declare function shouldReloadForChunkError(storage: Pick<Storage, 'getItem' | 'setItem'>, now: number): boolean;
|
|
50
|
+
export declare function setupChunkErrorRecovery(report?: (event: ChunkRecoveryEvent) => void): (() => void) | undefined;
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// src/shared/chunk-error-recovery.ts
|
|
2
|
+
import { createChunkDiagnosticId, sanitizeChunkUrl } from "./chunk-diagnostics";
|
|
3
|
+
var RELOAD_GUARD_KEY = "chunk-error-last-reload-at";
|
|
4
|
+
var RELOAD_GUARD_MS = 60 * 1e3;
|
|
5
|
+
var PENDING_KEY = "chunk-error-pending-recovery";
|
|
6
|
+
var PENDING_TTL_MS = 5 * 60 * 1e3;
|
|
7
|
+
var reportRouteReady = () => {
|
|
8
|
+
};
|
|
9
|
+
function markChunkRecoveryRouteReady(kind = "page") {
|
|
10
|
+
reportRouteReady(kind);
|
|
11
|
+
}
|
|
12
|
+
var CHUNK_ERROR_PATTERN = /ChunkLoadError|CSS_CHUNK_LOAD_FAILED|Loading (?:CSS )?chunk \S+ failed|error loading dynamically imported module/i;
|
|
13
|
+
function isChunkErrorMessage(message) {
|
|
14
|
+
return CHUNK_ERROR_PATTERN.test(message);
|
|
15
|
+
}
|
|
16
|
+
function isChunkErrorReason(reason) {
|
|
17
|
+
if (reason && typeof reason === "object") {
|
|
18
|
+
const { message, code, name } = reason;
|
|
19
|
+
return code === "CSS_CHUNK_LOAD_FAILED" || name === "ChunkLoadError" || typeof message === "string" && isChunkErrorMessage(message);
|
|
20
|
+
}
|
|
21
|
+
return typeof reason === "string" && isChunkErrorMessage(reason);
|
|
22
|
+
}
|
|
23
|
+
function safeUrl(value) {
|
|
24
|
+
return sanitizeChunkUrl(value, window.location.href);
|
|
25
|
+
}
|
|
26
|
+
function readPending(raw, now) {
|
|
27
|
+
const pending = JSON.parse(raw);
|
|
28
|
+
if (!pending || typeof pending !== "object" || Array.isArray(pending))
|
|
29
|
+
return;
|
|
30
|
+
const value = pending;
|
|
31
|
+
if (typeof value.at !== "number" || !Number.isFinite(value.at) || value.at <= 0 || now - value.at < 0 || now - value.at >= PENDING_TTL_MS || typeof value.page !== "string" || typeof value.resource !== "string" || value.source !== "resource" && value.source !== "runtime" && value.source !== "rejection" || value.attemptId !== void 0 && (typeof value.attemptId !== "string" || !/^[a-zA-Z0-9_-]{1,80}$/.test(value.attemptId)))
|
|
32
|
+
return;
|
|
33
|
+
const page = safeUrl(value.page);
|
|
34
|
+
if (!page || page !== safeUrl(window.location.href))
|
|
35
|
+
return;
|
|
36
|
+
return {
|
|
37
|
+
attemptId: value.attemptId ?? createChunkDiagnosticId(),
|
|
38
|
+
at: value.at,
|
|
39
|
+
page,
|
|
40
|
+
resource: value.resource ? safeUrl(value.resource) : "",
|
|
41
|
+
source: value.source
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function reasonResource(reason) {
|
|
45
|
+
var _a;
|
|
46
|
+
const error = reason;
|
|
47
|
+
if (error && typeof error.request === "string")
|
|
48
|
+
return safeUrl(error.request);
|
|
49
|
+
const message = typeof (error == null ? void 0 : error.message) === "string" ? error.message : typeof reason === "string" ? reason : "";
|
|
50
|
+
const url = (_a = message.match(/https?:\/\/[^\s)]+/)) == null ? void 0 : _a[0];
|
|
51
|
+
return url ? safeUrl(url) : "";
|
|
52
|
+
}
|
|
53
|
+
function shouldReloadForChunkError(storage, now) {
|
|
54
|
+
const last = Number(storage.getItem(RELOAD_GUARD_KEY) ?? 0);
|
|
55
|
+
if (Number.isFinite(last) && last > 0 && now - last < RELOAD_GUARD_MS) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
storage.setItem(RELOAD_GUARD_KEY, String(now));
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
function setupChunkErrorRecovery(report = () => {
|
|
62
|
+
}) {
|
|
63
|
+
if (typeof window === "undefined") {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const emit = (outcome, attempt, observed) => {
|
|
67
|
+
try {
|
|
68
|
+
report({ ...attempt, ...observed, outcome, occurredAt: Date.now() });
|
|
69
|
+
} catch {
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
let previous;
|
|
73
|
+
try {
|
|
74
|
+
const raw = window.sessionStorage.getItem(PENDING_KEY);
|
|
75
|
+
if (raw) {
|
|
76
|
+
window.sessionStorage.removeItem(PENDING_KEY);
|
|
77
|
+
const navigation = performance.getEntriesByType("navigation")[0];
|
|
78
|
+
if ((navigation == null ? void 0 : navigation.type) === "reload") {
|
|
79
|
+
previous = readPending(raw, Date.now());
|
|
80
|
+
if (previous)
|
|
81
|
+
emit("REENTERED", previous);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
}
|
|
86
|
+
let current = previous;
|
|
87
|
+
let currentExpiresAt = current ? current.at + RELOAD_GUARD_MS : 0;
|
|
88
|
+
if (current) {
|
|
89
|
+
try {
|
|
90
|
+
const lastReloadAt = Number(window.sessionStorage.getItem(RELOAD_GUARD_KEY));
|
|
91
|
+
if (Number.isFinite(lastReloadAt) && lastReloadAt > 0) {
|
|
92
|
+
currentExpiresAt = Math.max(currentExpiresAt, lastReloadAt + RELOAD_GUARD_MS);
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const settle = (kind) => {
|
|
98
|
+
if (!previous) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const settled = previous;
|
|
102
|
+
previous = void 0;
|
|
103
|
+
if (kind === "redirected") {
|
|
104
|
+
emit("ROUTE-REDIRECTED", settled);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const age = Date.now() - settled.at;
|
|
108
|
+
if (age >= 0 && age < PENDING_TTL_MS && safeUrl(window.location.href) === settled.page) {
|
|
109
|
+
emit(kind === "micro-app" ? "MICRO-APP-READY" : "ROUTE-READY", settled);
|
|
110
|
+
} else {
|
|
111
|
+
emit("ROUTE-REDIRECTED", settled);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
reportRouteReady = (kind) => settle(kind);
|
|
115
|
+
let active = true;
|
|
116
|
+
const handleLocationChange = () => {
|
|
117
|
+
if (!active)
|
|
118
|
+
return;
|
|
119
|
+
if (previous && safeUrl(window.location.href) !== previous.page) {
|
|
120
|
+
settle("redirected");
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
const originalPushState = typeof history !== "undefined" ? history.pushState : void 0;
|
|
124
|
+
const originalReplaceState = typeof history !== "undefined" ? history.replaceState : void 0;
|
|
125
|
+
let pushStateWrapper;
|
|
126
|
+
let replaceStateWrapper;
|
|
127
|
+
if (originalPushState && originalReplaceState) {
|
|
128
|
+
pushStateWrapper = function(...args) {
|
|
129
|
+
const result = originalPushState.apply(this, args);
|
|
130
|
+
handleLocationChange();
|
|
131
|
+
return result;
|
|
132
|
+
};
|
|
133
|
+
replaceStateWrapper = function(...args) {
|
|
134
|
+
const result = originalReplaceState.apply(this, args);
|
|
135
|
+
handleLocationChange();
|
|
136
|
+
return result;
|
|
137
|
+
};
|
|
138
|
+
history.pushState = pushStateWrapper;
|
|
139
|
+
history.replaceState = replaceStateWrapper;
|
|
140
|
+
window.addEventListener("popstate", handleLocationChange);
|
|
141
|
+
}
|
|
142
|
+
const recover = (source, resource = "") => {
|
|
143
|
+
const now = Date.now();
|
|
144
|
+
const observed = { observedSource: source, observedResource: resource };
|
|
145
|
+
if (previous) {
|
|
146
|
+
current = previous;
|
|
147
|
+
emit("RECURRED", previous, observed);
|
|
148
|
+
previous = void 0;
|
|
149
|
+
} else if (!current || now >= currentExpiresAt) {
|
|
150
|
+
current = {
|
|
151
|
+
attemptId: createChunkDiagnosticId(),
|
|
152
|
+
at: now,
|
|
153
|
+
page: safeUrl(window.location.href),
|
|
154
|
+
source,
|
|
155
|
+
resource
|
|
156
|
+
};
|
|
157
|
+
currentExpiresAt = now + RELOAD_GUARD_MS;
|
|
158
|
+
}
|
|
159
|
+
const attempt = current;
|
|
160
|
+
const observation = attempt.source !== source || attempt.resource !== resource ? observed : void 0;
|
|
161
|
+
if (!navigator.onLine) {
|
|
162
|
+
emit("SKIP-OFFLINE", attempt, observation);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
if (!shouldReloadForChunkError(window.sessionStorage, now)) {
|
|
167
|
+
emit("SKIP-GUARD", attempt, observation);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
currentExpiresAt = now + RELOAD_GUARD_MS;
|
|
171
|
+
window.sessionStorage.setItem(PENDING_KEY, JSON.stringify(attempt));
|
|
172
|
+
} catch {
|
|
173
|
+
emit("SKIP-STORAGE", attempt, observation);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
emit("RELOAD", attempt, observation);
|
|
177
|
+
window.location.reload();
|
|
178
|
+
};
|
|
179
|
+
const onError = (event) => {
|
|
180
|
+
const target = event.target;
|
|
181
|
+
if (target instanceof HTMLScriptElement || target instanceof HTMLLinkElement && target.rel === "stylesheet") {
|
|
182
|
+
const url = target instanceof HTMLScriptElement ? target.src : target.href;
|
|
183
|
+
const assetBase = new URL(window.publicPath || "/", window.location.href);
|
|
184
|
+
const asset = new URL(url, window.location.href);
|
|
185
|
+
if (asset.origin === assetBase.origin && asset.pathname.startsWith(assetBase.pathname) && /\.(?:js|css)$/i.test(asset.pathname)) {
|
|
186
|
+
recover("resource", safeUrl(url));
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (typeof event.message === "string" && isChunkErrorMessage(event.message)) {
|
|
191
|
+
recover("runtime", reasonResource(event));
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
const onRejection = (event) => {
|
|
195
|
+
const reason = event.reason;
|
|
196
|
+
if (isChunkErrorReason(reason)) {
|
|
197
|
+
recover("rejection", reasonResource(reason));
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
window.addEventListener("error", onError, true);
|
|
201
|
+
window.addEventListener("unhandledrejection", onRejection);
|
|
202
|
+
return () => {
|
|
203
|
+
active = false;
|
|
204
|
+
window.removeEventListener("error", onError, true);
|
|
205
|
+
window.removeEventListener("unhandledrejection", onRejection);
|
|
206
|
+
if (originalPushState && originalReplaceState) {
|
|
207
|
+
window.removeEventListener("popstate", handleLocationChange);
|
|
208
|
+
if (pushStateWrapper && history.pushState === pushStateWrapper) {
|
|
209
|
+
history.pushState = originalPushState;
|
|
210
|
+
}
|
|
211
|
+
if (replaceStateWrapper && history.replaceState === replaceStateWrapper) {
|
|
212
|
+
history.replaceState = originalReplaceState;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
reportRouteReady = () => {
|
|
216
|
+
};
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
export {
|
|
220
|
+
isChunkErrorMessage,
|
|
221
|
+
isChunkErrorReason,
|
|
222
|
+
markChunkRecoveryRouteReady,
|
|
223
|
+
setupChunkErrorRecovery,
|
|
224
|
+
shouldReloadForChunkError
|
|
225
|
+
};
|
|
@@ -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
|
+
};
|