@microbt/pwa 0.0.1 → 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 +30 -1
- 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-error-recovery.d.ts +51 -0
- package/dist/esm/shared/chunk-error-recovery.js +225 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ pnpm add @microbt/pwa
|
|
|
10
10
|
|
|
11
11
|
需要 peer 依赖 `@microbt/environment`(ARMS 上报)。构建侧仍由接入方自备 workbox 的 `InjectManifest` 插件(配置不用改,见下)。
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
四个入口:`.`(页面端注册)、`./protocol`(诊断协议,纯常量与纯函数,依赖链不含 `@microbt/environment`)、`./sw`(SW 端运行时)、`./recovery`(chunk 恢复与诊断上报,见下节)。
|
|
14
14
|
|
|
15
15
|
## 接入
|
|
16
16
|
|
|
@@ -38,6 +38,35 @@ schedulePwaRegistration({ version: RELEASE, log });
|
|
|
38
38
|
import { CHUNK_TRACE_MESSAGE } from '@microbt/pwa/protocol';
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
## chunk 恢复与诊断上报(`./recovery`)
|
|
42
|
+
|
|
43
|
+
在 `bootstrap` 里尽早调用 `setupChunkErrorRecovery`,把恢复事件交给 `createChunkDiagnosticReporter`;ARMS 计数(`arms.sum`)由接入方回调自己做:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { createChunkDiagnosticReporter, setupChunkErrorRecovery } from '@microbt/pwa/recovery';
|
|
47
|
+
|
|
48
|
+
const reporter = createChunkDiagnosticReporter({
|
|
49
|
+
release: RELEASE,
|
|
50
|
+
appVersion: () => appVersion().version,
|
|
51
|
+
});
|
|
52
|
+
setupChunkErrorRecovery((event) => {
|
|
53
|
+
arms.sum(`CHUNK-RECOVERY-${event.outcome}`); // 计数留在接入方
|
|
54
|
+
reporter.report(event);
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
完成信号探针由接入方写:Umi 叶子路由提交时调 `markChunkRecoveryRouteReady('page')`;qiankun 子应用 `afterMount` 调 `markChunkRecoveryRouteReady('micro-app')`。
|
|
59
|
+
|
|
60
|
+
行为契约(以下字符串/数值一字不差):
|
|
61
|
+
|
|
62
|
+
- sessionStorage 键:`chunk-error-last-reload-at`、`chunk-error-pending-recovery`、`chunk-diagnostic-outbox-v1`
|
|
63
|
+
- chunk 失败识别正则 `CHUNK_ERROR_PATTERN` 原文:`/ChunkLoadError|CSS_CHUNK_LOAD_FAILED|Loading (?:CSS )?chunk \S+ failed|error loading dynamically imported module/i`
|
|
64
|
+
- reload 闸门 60 秒;待恢复记录 5 分钟;诊断有效期 5 分钟;发件箱 16 条、补发重试 30 次 × 1 秒;SW 请求记录 40 条;关联窗口 120 秒取最近 3 条;单条载荷 < 16,000 字符;去重集合上限 128
|
|
65
|
+
- 结果名:`RELOAD`、`SKIP-OFFLINE`、`SKIP-GUARD`、`SKIP-STORAGE`、`REENTERED`、`ROUTE-READY`、`MICRO-APP-READY`、`ROUTE-REDIRECTED`、`RECURRED`;SW 侧 `SW-REQUEST`
|
|
66
|
+
- ARMS:`custom` 的 `kind: 'chunk-diagnostic'`、`schema: 1`、`eventId`、`attemptId`、`outcome`、`replay`、`payload`;行为日志 `name: 'chunk-recovery'`、正文只有 `{id, outcome}`;`correlation: 'resource-time-window'`;`reconstructedFrom: 'REENTERED'`
|
|
67
|
+
|
|
68
|
+
依赖链约束:`./recovery` 的依赖链(含 side-effect import)不含 `@microbt/environment`、任何 `workbox-*`,也不含模块加载期就读 `window`/`document` 的模块——它要在首屏最早加载,自己不能被别的依赖拖垮。
|
|
69
|
+
|
|
41
70
|
## 行为契约
|
|
42
71
|
|
|
43
72
|
以下字符串/参数与帮助中心 0.15.16 一字不差:
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { createChunkDiagnosticReporter } from './shared/chunk-diagnostic-reporter';
|
|
2
|
+
export { markChunkRecoveryRouteReady, setupChunkErrorRecovery, } from './shared/chunk-error-recovery';
|
|
3
|
+
export type { ChunkRecoveryEvent, RecoveryCompletionKind } from './shared/chunk-error-recovery';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// src/recovery.ts
|
|
2
|
+
import { createChunkDiagnosticReporter } from "./shared/chunk-diagnostic-reporter";
|
|
3
|
+
import {
|
|
4
|
+
markChunkRecoveryRouteReady,
|
|
5
|
+
setupChunkErrorRecovery
|
|
6
|
+
} from "./shared/chunk-error-recovery";
|
|
7
|
+
export {
|
|
8
|
+
createChunkDiagnosticReporter,
|
|
9
|
+
markChunkRecoveryRouteReady,
|
|
10
|
+
setupChunkErrorRecovery
|
|
11
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface DiagnosticRecord {
|
|
2
|
+
eventId: string;
|
|
3
|
+
attemptId: string;
|
|
4
|
+
at: number;
|
|
5
|
+
outcome: string;
|
|
6
|
+
payload: string;
|
|
7
|
+
}
|
|
8
|
+
/** custom() 不提供服务端 ACK:当前记录保留到下个文档再发一次,由 eventId 去重。 */
|
|
9
|
+
export declare function createDiagnosticOutbox(): {
|
|
10
|
+
has: (eventId: string) => boolean;
|
|
11
|
+
enqueue(record: DiagnosticRecord, replay?: boolean): void;
|
|
12
|
+
dispose(): void;
|
|
13
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// src/shared/chunk-diagnostic-outbox.ts
|
|
2
|
+
import { DIAGNOSTIC_ID, DIAGNOSTIC_TTL, sanitizeStoredPayload } from "./chunk-diagnostic-payload";
|
|
3
|
+
var OUTBOX_KEY = "chunk-diagnostic-outbox-v1";
|
|
4
|
+
var MAX_RECORDS = 16;
|
|
5
|
+
var OUTCOMES = /* @__PURE__ */ new Set([
|
|
6
|
+
"RELOAD",
|
|
7
|
+
"REENTERED",
|
|
8
|
+
"ROUTE-READY",
|
|
9
|
+
"MICRO-APP-READY",
|
|
10
|
+
"ROUTE-REDIRECTED",
|
|
11
|
+
"RECURRED",
|
|
12
|
+
"SKIP-OFFLINE",
|
|
13
|
+
"SKIP-GUARD",
|
|
14
|
+
"SKIP-STORAGE",
|
|
15
|
+
"SW-REQUEST"
|
|
16
|
+
]);
|
|
17
|
+
function restore() {
|
|
18
|
+
try {
|
|
19
|
+
const saved = JSON.parse(window.sessionStorage.getItem(OUTBOX_KEY) || "[]");
|
|
20
|
+
if (!Array.isArray(saved))
|
|
21
|
+
return [];
|
|
22
|
+
const records = [];
|
|
23
|
+
for (const r of saved.slice(-MAX_RECORDS)) {
|
|
24
|
+
try {
|
|
25
|
+
if (!r || typeof r.eventId !== "string" || !DIAGNOSTIC_ID.test(r.eventId) || typeof r.attemptId !== "string" || !DIAGNOSTIC_ID.test(r.attemptId) || !Number.isFinite(r.at) || Date.now() < r.at || Date.now() - r.at >= DIAGNOSTIC_TTL || !OUTCOMES.has(r.outcome) || typeof r.payload !== "string" || r.payload.length >= 16e3)
|
|
26
|
+
continue;
|
|
27
|
+
records.push({
|
|
28
|
+
eventId: r.eventId,
|
|
29
|
+
attemptId: r.attemptId,
|
|
30
|
+
at: r.at,
|
|
31
|
+
outcome: r.outcome,
|
|
32
|
+
payload: sanitizeStoredPayload(r.payload)
|
|
33
|
+
});
|
|
34
|
+
} catch {
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return records;
|
|
38
|
+
} catch {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function createDiagnosticOutbox() {
|
|
43
|
+
const delivered = /* @__PURE__ */ new Set();
|
|
44
|
+
const breadcrumbs = /* @__PURE__ */ new Set();
|
|
45
|
+
let queue = restore();
|
|
46
|
+
const restored = new Set(queue.map((r) => r.eventId));
|
|
47
|
+
let timer;
|
|
48
|
+
let retries = 0;
|
|
49
|
+
let disposed = false;
|
|
50
|
+
const persist = () => {
|
|
51
|
+
try {
|
|
52
|
+
window.sessionStorage.setItem(OUTBOX_KEY, JSON.stringify(queue));
|
|
53
|
+
} catch {
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const needsBehavior = (r) => r.outcome !== "SW-REQUEST" && !breadcrumbs.has(r.eventId);
|
|
57
|
+
const flush = () => {
|
|
58
|
+
if (disposed)
|
|
59
|
+
return;
|
|
60
|
+
queue = queue.filter((r) => Date.now() - r.at < DIAGNOSTIC_TTL && Date.now() >= r.at);
|
|
61
|
+
for (const record of queue) {
|
|
62
|
+
const logger = window.__bl;
|
|
63
|
+
try {
|
|
64
|
+
if (!delivered.has(record.eventId) && typeof (logger == null ? void 0 : logger.custom) === "function") {
|
|
65
|
+
logger.custom(
|
|
66
|
+
{
|
|
67
|
+
kind: "chunk-diagnostic",
|
|
68
|
+
schema: 1,
|
|
69
|
+
eventId: record.eventId,
|
|
70
|
+
attemptId: record.attemptId,
|
|
71
|
+
outcome: record.outcome,
|
|
72
|
+
replay: restored.has(record.eventId) ? 1 : 0,
|
|
73
|
+
payload: record.payload
|
|
74
|
+
},
|
|
75
|
+
1
|
|
76
|
+
);
|
|
77
|
+
delivered.add(record.eventId);
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
if (needsBehavior(record) && typeof (logger == null ? void 0 : logger.addBehavior) === "function" && typeof logger.reportBehavior === "function") {
|
|
83
|
+
logger.addBehavior({
|
|
84
|
+
data: {
|
|
85
|
+
name: "chunk-recovery",
|
|
86
|
+
message: JSON.stringify({
|
|
87
|
+
id: record.attemptId,
|
|
88
|
+
outcome: record.outcome
|
|
89
|
+
})
|
|
90
|
+
},
|
|
91
|
+
page: JSON.parse(record.payload).page || ""
|
|
92
|
+
});
|
|
93
|
+
logger.reportBehavior();
|
|
94
|
+
breadcrumbs.add(record.eventId);
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
queue = queue.filter(
|
|
100
|
+
(r) => !(restored.has(r.eventId) && delivered.has(r.eventId) && !needsBehavior(r))
|
|
101
|
+
);
|
|
102
|
+
persist();
|
|
103
|
+
if (queue.some((r) => !delivered.has(r.eventId) || needsBehavior(r)) && retries < 30 && !timer) {
|
|
104
|
+
retries += 1;
|
|
105
|
+
timer = setTimeout(() => {
|
|
106
|
+
timer = void 0;
|
|
107
|
+
flush();
|
|
108
|
+
}, 1e3);
|
|
109
|
+
}
|
|
110
|
+
for (const set of [delivered, breadcrumbs, restored])
|
|
111
|
+
while (set.size > 128)
|
|
112
|
+
set.delete(set.values().next().value);
|
|
113
|
+
};
|
|
114
|
+
const has = (eventId) => delivered.has(eventId) || queue.some((r) => r.eventId === eventId);
|
|
115
|
+
flush();
|
|
116
|
+
return {
|
|
117
|
+
has,
|
|
118
|
+
enqueue(record, replay = false) {
|
|
119
|
+
if (disposed || has(record.eventId))
|
|
120
|
+
return;
|
|
121
|
+
if (replay)
|
|
122
|
+
restored.add(record.eventId);
|
|
123
|
+
queue = [...queue, record].slice(-MAX_RECORDS);
|
|
124
|
+
retries = 0;
|
|
125
|
+
persist();
|
|
126
|
+
flush();
|
|
127
|
+
},
|
|
128
|
+
dispose() {
|
|
129
|
+
disposed = true;
|
|
130
|
+
if (timer)
|
|
131
|
+
clearTimeout(timer);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
export {
|
|
136
|
+
createDiagnosticOutbox
|
|
137
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ChunkRequestTrace } from './chunk-diagnostics';
|
|
2
|
+
export declare const DIAGNOSTIC_TTL: number;
|
|
3
|
+
export declare const DIAGNOSTIC_ID: RegExp;
|
|
4
|
+
/** 可选浏览器/WebView API 不可用时仍保留核心错误事件。 */
|
|
5
|
+
export declare function diagnosticContext(release: string, appVersion: () => string): {
|
|
6
|
+
release: string;
|
|
7
|
+
appVersion: string;
|
|
8
|
+
online: boolean;
|
|
9
|
+
visibility: string;
|
|
10
|
+
swControlled: boolean;
|
|
11
|
+
swScript: string;
|
|
12
|
+
nativeResourceSource: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function diagnosticTiming(resource: string, occurredAt: number): Record<string, string | number> | undefined;
|
|
15
|
+
/** 消息和 storage 都只接受协议字段,不夹带 token、正文或任意扩展属性。 */
|
|
16
|
+
export declare function readChunkTrace(value: unknown): ChunkRequestTrace | undefined;
|
|
17
|
+
export declare function sanitizeStoredPayload(payload: string): string;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// src/shared/chunk-diagnostic-payload.ts
|
|
2
|
+
import { sanitizeChunkUrl } from "./chunk-diagnostics";
|
|
3
|
+
var DIAGNOSTIC_TTL = 5 * 6e4;
|
|
4
|
+
var DIAGNOSTIC_ID = /^[a-zA-Z0-9_-]{1,100}$/;
|
|
5
|
+
var safeVersion = (value) => typeof value === "string" ? value.replace(/[^\w.-]/g, "").slice(0, 80) : "unknown";
|
|
6
|
+
var visibility = (value) => value === "visible" || value === "hidden" ? value : "unknown";
|
|
7
|
+
function diagnosticContext(release, appVersion) {
|
|
8
|
+
var _a;
|
|
9
|
+
let version = "unknown";
|
|
10
|
+
let controller;
|
|
11
|
+
try {
|
|
12
|
+
version = safeVersion(appVersion());
|
|
13
|
+
} catch {
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
controller = (_a = navigator.serviceWorker) == null ? void 0 : _a.controller;
|
|
17
|
+
} catch {
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
release: safeVersion(release),
|
|
21
|
+
appVersion: version,
|
|
22
|
+
online: navigator.onLine,
|
|
23
|
+
visibility: typeof document === "undefined" ? "unknown" : visibility(document.visibilityState),
|
|
24
|
+
swControlled: Boolean(controller),
|
|
25
|
+
swScript: controller ? sanitizeChunkUrl(controller.scriptURL) : "",
|
|
26
|
+
nativeResourceSource: "unavailable"
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function safeTiming(value) {
|
|
30
|
+
const result = {
|
|
31
|
+
visibility: "may-be-redacted"
|
|
32
|
+
};
|
|
33
|
+
for (const key of [
|
|
34
|
+
"startTime",
|
|
35
|
+
"duration",
|
|
36
|
+
"transferSize",
|
|
37
|
+
"encodedBodySize",
|
|
38
|
+
"decodedBodySize",
|
|
39
|
+
"workerStart",
|
|
40
|
+
"status",
|
|
41
|
+
"completedAt"
|
|
42
|
+
]) {
|
|
43
|
+
const number = value[key];
|
|
44
|
+
if (typeof number === "number" && Number.isFinite(number) && number >= 0)
|
|
45
|
+
result[key] = number;
|
|
46
|
+
}
|
|
47
|
+
if (typeof value.initiatorType === "string" && /^(link|script|css|fetch|other)$/.test(value.initiatorType))
|
|
48
|
+
result.initiatorType = value.initiatorType;
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
function diagnosticTiming(resource, occurredAt) {
|
|
52
|
+
var _a;
|
|
53
|
+
try {
|
|
54
|
+
const origin = performance.timeOrigin || ((_a = performance.timing) == null ? void 0 : _a.navigationStart);
|
|
55
|
+
if (!Number.isFinite(origin) || origin <= 0)
|
|
56
|
+
return void 0;
|
|
57
|
+
const entries = performance.getEntriesByType("resource");
|
|
58
|
+
const entry = entries.filter(
|
|
59
|
+
(e) => sanitizeChunkUrl(e.name) === resource && Number.isFinite(e.startTime) && Number.isFinite(e.duration) && Math.abs(origin + e.startTime + e.duration - occurredAt) < 12e4
|
|
60
|
+
).pop();
|
|
61
|
+
if (!entry)
|
|
62
|
+
return void 0;
|
|
63
|
+
return safeTiming({
|
|
64
|
+
startTime: entry.startTime,
|
|
65
|
+
duration: entry.duration,
|
|
66
|
+
completedAt: origin + entry.startTime + entry.duration,
|
|
67
|
+
transferSize: entry.transferSize,
|
|
68
|
+
encodedBodySize: entry.encodedBodySize,
|
|
69
|
+
decodedBodySize: entry.decodedBodySize,
|
|
70
|
+
workerStart: entry.workerStart,
|
|
71
|
+
initiatorType: entry.initiatorType,
|
|
72
|
+
status: entry.responseStatus
|
|
73
|
+
});
|
|
74
|
+
} catch {
|
|
75
|
+
return void 0;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function readChunkTrace(value) {
|
|
79
|
+
if (!value || typeof value !== "object")
|
|
80
|
+
return;
|
|
81
|
+
const r = value;
|
|
82
|
+
if (typeof r.requestId !== "string" || !DIAGNOSTIC_ID.test(r.requestId) || !Number.isFinite(r.at) || Math.abs(Date.now() - r.at) > DIAGNOSTIC_TTL || !["response", "complete"].includes(r.phase))
|
|
83
|
+
return;
|
|
84
|
+
const resource = sanitizeChunkUrl(r.resource);
|
|
85
|
+
if (!resource)
|
|
86
|
+
return;
|
|
87
|
+
const result = {
|
|
88
|
+
requestId: r.requestId,
|
|
89
|
+
at: r.at,
|
|
90
|
+
resource,
|
|
91
|
+
phase: r.phase,
|
|
92
|
+
destination: ["script", "style"].includes(r.destination) ? r.destination : "unknown",
|
|
93
|
+
mode: ["cors", "no-cors", "same-origin"].includes(r.mode) ? r.mode : "unknown",
|
|
94
|
+
credentials: ["include", "omit", "same-origin"].includes(r.credentials) ? r.credentials : "unknown",
|
|
95
|
+
cache: ["hit", "miss"].includes(r.cache) ? r.cache : "unknown",
|
|
96
|
+
network: ["pending", "response", "error"].includes(r.network) ? r.network : "pending",
|
|
97
|
+
swRelease: safeVersion(r.swRelease)
|
|
98
|
+
};
|
|
99
|
+
for (const key of ["networkStatus", "responseStatus"])
|
|
100
|
+
if (Number.isInteger(r[key]) && r[key] >= 0 && r[key] <= 599)
|
|
101
|
+
result[key] = r[key];
|
|
102
|
+
if (Number.isFinite(r.completedAt) && r.completedAt >= r.at && r.completedAt <= Date.now() + 1e3)
|
|
103
|
+
result.completedAt = r.completedAt;
|
|
104
|
+
for (const key of ["networkType", "responseType"])
|
|
105
|
+
if (["basic", "cors", "default", "error", "opaque", "opaqueredirect"].includes(r[key] || ""))
|
|
106
|
+
result[key] = r[key];
|
|
107
|
+
for (const key of ["networkMime", "responseMime"]) {
|
|
108
|
+
const mime = typeof r[key] === "string" ? r[key].split(";", 1)[0].trim() : "";
|
|
109
|
+
if (/^[\w.+-]+\/[\w.+-]+$/.test(mime))
|
|
110
|
+
result[key] = mime.slice(0, 80);
|
|
111
|
+
}
|
|
112
|
+
if (/^(Error|TypeError|RangeError|AbortError|NetworkError|TimeoutError|SecurityError|InvalidStateError|QuotaExceededError|no-response)$/.test(
|
|
113
|
+
r.errorName || ""
|
|
114
|
+
))
|
|
115
|
+
result.errorName = r.errorName;
|
|
116
|
+
if (["cache", "network", "none"].includes(r.servedFrom || ""))
|
|
117
|
+
result.servedFrom = r.servedFrom;
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
function sanitizeStoredPayload(payload) {
|
|
121
|
+
const r = JSON.parse(payload);
|
|
122
|
+
if (!r || typeof r !== "object" || Array.isArray(r))
|
|
123
|
+
throw new Error("Invalid diagnostic payload");
|
|
124
|
+
const result = {};
|
|
125
|
+
for (const key of ["page", "resource", "observedResource", "diagnosticResource"])
|
|
126
|
+
if (typeof r[key] === "string")
|
|
127
|
+
result[key] = sanitizeChunkUrl(r[key]);
|
|
128
|
+
for (const key of ["attemptId", "outcome", "source", "observedSource"])
|
|
129
|
+
if (typeof r[key] === "string" && DIAGNOSTIC_ID.test(r[key]))
|
|
130
|
+
result[key] = r[key];
|
|
131
|
+
for (const key of ["at", "occurredAt"])
|
|
132
|
+
if (Number.isFinite(r[key]))
|
|
133
|
+
result[key] = r[key];
|
|
134
|
+
if (r.reconstructedFrom === "REENTERED")
|
|
135
|
+
result.reconstructedFrom = "REENTERED";
|
|
136
|
+
if (r.context && typeof r.context === "object")
|
|
137
|
+
result.context = {
|
|
138
|
+
release: safeVersion(r.context.release),
|
|
139
|
+
appVersion: safeVersion(r.context.appVersion),
|
|
140
|
+
online: typeof r.context.online === "boolean" ? r.context.online : void 0,
|
|
141
|
+
visibility: visibility(r.context.visibility),
|
|
142
|
+
swControlled: r.context.swControlled === true,
|
|
143
|
+
swScript: sanitizeChunkUrl(r.context.swScript || ""),
|
|
144
|
+
nativeResourceSource: "unavailable"
|
|
145
|
+
};
|
|
146
|
+
if (r.timing && typeof r.timing === "object")
|
|
147
|
+
result.timing = safeTiming(r.timing);
|
|
148
|
+
if (Array.isArray(r.swTraces))
|
|
149
|
+
result.swTraces = r.swTraces.slice(-3).map(readChunkTrace).filter(Boolean);
|
|
150
|
+
if (r.trace)
|
|
151
|
+
result.trace = readChunkTrace(r.trace);
|
|
152
|
+
result.correlation = "resource-time-window";
|
|
153
|
+
return JSON.stringify(result);
|
|
154
|
+
}
|
|
155
|
+
export {
|
|
156
|
+
DIAGNOSTIC_ID,
|
|
157
|
+
DIAGNOSTIC_TTL,
|
|
158
|
+
diagnosticContext,
|
|
159
|
+
diagnosticTiming,
|
|
160
|
+
readChunkTrace,
|
|
161
|
+
sanitizeStoredPayload
|
|
162
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ChunkRecoveryEvent } from './chunk-error-recovery';
|
|
2
|
+
/** 完整诊断走 t=custom / x-* 字段,不制造 error 或 API 指标。 */
|
|
3
|
+
export declare function createChunkDiagnosticReporter(options: {
|
|
4
|
+
release: string;
|
|
5
|
+
appVersion?: () => string;
|
|
6
|
+
}): {
|
|
7
|
+
report: (event: ChunkRecoveryEvent) => void;
|
|
8
|
+
dispose: () => void;
|
|
9
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// src/shared/chunk-diagnostic-reporter.ts
|
|
2
|
+
import { createDiagnosticOutbox } from "./chunk-diagnostic-outbox";
|
|
3
|
+
import {
|
|
4
|
+
DIAGNOSTIC_TTL,
|
|
5
|
+
diagnosticContext,
|
|
6
|
+
diagnosticTiming,
|
|
7
|
+
readChunkTrace
|
|
8
|
+
} from "./chunk-diagnostic-payload";
|
|
9
|
+
import { CHUNK_TRACE_MESSAGE, sanitizeChunkUrl } from "./chunk-diagnostics";
|
|
10
|
+
function createChunkDiagnosticReporter(options) {
|
|
11
|
+
const { release, appVersion = () => "unknown" } = options;
|
|
12
|
+
const outbox = createDiagnosticOutbox();
|
|
13
|
+
let traces = [];
|
|
14
|
+
let active;
|
|
15
|
+
const diagnosticResource = (event) => sanitizeChunkUrl(event.observedResource || event.resource);
|
|
16
|
+
const matching = (event) => traces.filter(
|
|
17
|
+
(trace) => trace.resource === diagnosticResource(event) && Math.abs((trace.completedAt ?? trace.at) - event.occurredAt) < 12e4
|
|
18
|
+
).slice(-3);
|
|
19
|
+
const report = (event) => {
|
|
20
|
+
try {
|
|
21
|
+
active = event;
|
|
22
|
+
const safeEvent = {
|
|
23
|
+
attemptId: event.attemptId,
|
|
24
|
+
at: event.at,
|
|
25
|
+
occurredAt: event.occurredAt,
|
|
26
|
+
outcome: event.outcome,
|
|
27
|
+
source: event.source,
|
|
28
|
+
observedSource: event.observedSource,
|
|
29
|
+
page: sanitizeChunkUrl(event.page),
|
|
30
|
+
resource: sanitizeChunkUrl(event.resource),
|
|
31
|
+
observedResource: event.observedResource ? sanitizeChunkUrl(event.observedResource) : void 0
|
|
32
|
+
};
|
|
33
|
+
if (event.outcome === "REENTERED" && !outbox.has(`${event.attemptId}-RELOAD`)) {
|
|
34
|
+
outbox.enqueue(
|
|
35
|
+
{
|
|
36
|
+
eventId: `${event.attemptId}-RELOAD`,
|
|
37
|
+
attemptId: event.attemptId,
|
|
38
|
+
at: Date.now(),
|
|
39
|
+
outcome: "RELOAD",
|
|
40
|
+
payload: JSON.stringify({
|
|
41
|
+
attemptId: event.attemptId,
|
|
42
|
+
at: event.at,
|
|
43
|
+
page: safeEvent.page,
|
|
44
|
+
resource: safeEvent.resource,
|
|
45
|
+
source: event.source,
|
|
46
|
+
outcome: "RELOAD",
|
|
47
|
+
reconstructedFrom: "REENTERED"
|
|
48
|
+
})
|
|
49
|
+
},
|
|
50
|
+
true
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
outbox.enqueue({
|
|
54
|
+
eventId: `${event.attemptId}-${event.outcome}`,
|
|
55
|
+
attemptId: event.attemptId,
|
|
56
|
+
at: Date.now(),
|
|
57
|
+
outcome: event.outcome,
|
|
58
|
+
payload: JSON.stringify({
|
|
59
|
+
...safeEvent,
|
|
60
|
+
diagnosticResource: diagnosticResource(event),
|
|
61
|
+
context: diagnosticContext(release, appVersion),
|
|
62
|
+
timing: diagnosticTiming(diagnosticResource(event), event.occurredAt),
|
|
63
|
+
swTraces: matching(event),
|
|
64
|
+
correlation: "resource-time-window"
|
|
65
|
+
})
|
|
66
|
+
});
|
|
67
|
+
} catch {
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const onMessage = (message) => {
|
|
71
|
+
var _a, _b;
|
|
72
|
+
try {
|
|
73
|
+
if (!message.source || message.source !== ((_a = navigator.serviceWorker) == null ? void 0 : _a.controller) || ((_b = message.data) == null ? void 0 : _b.type) !== CHUNK_TRACE_MESSAGE)
|
|
74
|
+
return;
|
|
75
|
+
const trace = readChunkTrace(message.data.payload);
|
|
76
|
+
if (!trace)
|
|
77
|
+
return;
|
|
78
|
+
traces = traces.filter(
|
|
79
|
+
(r) => r.requestId !== trace.requestId && Date.now() - r.at < DIAGNOSTIC_TTL
|
|
80
|
+
);
|
|
81
|
+
traces.push(trace);
|
|
82
|
+
traces = traces.slice(-40);
|
|
83
|
+
if (active && Date.now() - active.occurredAt < DIAGNOSTIC_TTL && matching(active).includes(trace)) {
|
|
84
|
+
outbox.enqueue({
|
|
85
|
+
eventId: `${trace.requestId}-${trace.phase}`,
|
|
86
|
+
attemptId: active.attemptId,
|
|
87
|
+
at: Date.now(),
|
|
88
|
+
outcome: "SW-REQUEST",
|
|
89
|
+
payload: JSON.stringify({
|
|
90
|
+
attemptId: active.attemptId,
|
|
91
|
+
page: sanitizeChunkUrl(active.page),
|
|
92
|
+
resource: sanitizeChunkUrl(active.resource),
|
|
93
|
+
diagnosticResource: diagnosticResource(active),
|
|
94
|
+
trace,
|
|
95
|
+
correlation: "resource-time-window"
|
|
96
|
+
})
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const serviceWorker = navigator.serviceWorker;
|
|
103
|
+
serviceWorker == null ? void 0 : serviceWorker.addEventListener("message", onMessage);
|
|
104
|
+
return {
|
|
105
|
+
report,
|
|
106
|
+
dispose: () => {
|
|
107
|
+
outbox.dispose();
|
|
108
|
+
serviceWorker == null ? void 0 : serviceWorker.removeEventListener("message", onMessage);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export {
|
|
113
|
+
createChunkDiagnosticReporter
|
|
114
|
+
};
|
|
@@ -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
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@microbt/pwa",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "microbt service worker runtime and page-side registration for web apps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"microbt",
|
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
"./sw": {
|
|
22
22
|
"types": "./dist/esm/sw.d.ts",
|
|
23
23
|
"import": "./dist/esm/sw.js"
|
|
24
|
+
},
|
|
25
|
+
"./recovery": {
|
|
26
|
+
"types": "./dist/esm/recovery.d.ts",
|
|
27
|
+
"import": "./dist/esm/recovery.js"
|
|
24
28
|
}
|
|
25
29
|
},
|
|
26
30
|
"main": "dist/esm/index.js",
|