@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.
@@ -0,0 +1,49 @@
1
+ // src/shared/sw-script-url.ts
2
+ function buildSwRegisterUrl(baseUrl, version) {
3
+ return `/sw.js?baseUrl=${encodeURIComponent(baseUrl)}&v=${encodeURIComponent(version)}`;
4
+ }
5
+ function getSwScriptVersion(scriptUrl, pageOrigin) {
6
+ try {
7
+ return new URL(scriptUrl, pageOrigin).searchParams.get("v");
8
+ } catch {
9
+ return null;
10
+ }
11
+ }
12
+ function isSameOriginSwScriptUrl(scriptUrl, pageOrigin) {
13
+ try {
14
+ const url = new URL(scriptUrl, pageOrigin);
15
+ return url.origin === pageOrigin && url.pathname === "/sw.js";
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+ function normalizeScriptUrl(url, pageOrigin) {
21
+ try {
22
+ return new URL(url, pageOrigin).toString();
23
+ } catch {
24
+ return url;
25
+ }
26
+ }
27
+ function resolveSwScriptUrl({
28
+ publicPath,
29
+ existingScriptUrls,
30
+ version,
31
+ pageOrigin
32
+ }) {
33
+ if (typeof publicPath === "string" && (publicPath.startsWith("//") || /^https?:\/\//.test(publicPath))) {
34
+ return buildSwRegisterUrl(publicPath, version);
35
+ }
36
+ for (const scriptUrl of existingScriptUrls) {
37
+ if (isSameOriginSwScriptUrl(scriptUrl, pageOrigin) && getSwScriptVersion(scriptUrl, pageOrigin) === version) {
38
+ return scriptUrl;
39
+ }
40
+ }
41
+ return buildSwRegisterUrl("undefined", version);
42
+ }
43
+ export {
44
+ buildSwRegisterUrl,
45
+ getSwScriptVersion,
46
+ isSameOriginSwScriptUrl,
47
+ normalizeScriptUrl,
48
+ resolveSwScriptUrl
49
+ };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * ServiceWorker 资源缓存的响应校验。
3
+ *
4
+ * 背景:support 根域源站的 SPA fallback 会对任意路径(含 .js/.css)返回
5
+ * 200 + text/html 骨架页(响应头含 x-content-type-options: nosniff)。
6
+ * 当离线包拦截放行(站点禁用 / 包校验失败 / 版本未激活)时,SW 直接落网,
7
+ * 会把 HTML 响应写入 JS/CSS 缓存;StaleWhileRevalidate 之后先返回坏条目,
8
+ * 浏览器严格 MIME 检查拒绝执行,触发 ChunkLoadError 白屏(坏条目随后台
9
+ * revalidate 逐个自愈,表现为间歇性白屏)。
10
+ * 因此缓存前必须校验响应 MIME 与资源扩展名匹配,并在激活时清理历史坏条目。
11
+ */
12
+ /**
13
+ * 资源缓存名前缀。sw.ts 两条路由的 cacheName 必须复用这两个常量构造
14
+ * (`${PREFIX}${origin}`),否则 activate 清理会因名字漂移静默失效
15
+ */
16
+ export declare const JS_CSS_CACHE_NAME_PREFIX = "cache-js-css-";
17
+ export declare const STATIC_ASSETS_CACHE_NAME_PREFIX = "cache-static-assets-";
18
+ interface RequestLike {
19
+ readonly url: string;
20
+ }
21
+ interface ResponseLike {
22
+ readonly headers: {
23
+ get(name: string): string | null;
24
+ };
25
+ }
26
+ export interface CacheLike {
27
+ keys(): Promise<readonly Request[]>;
28
+ match(request: Request): Promise<Response | undefined>;
29
+ delete(request: Request): Promise<boolean>;
30
+ }
31
+ export interface CacheStorageLike {
32
+ keys(): Promise<string[]>;
33
+ open(cacheName: string): Promise<CacheLike>;
34
+ }
35
+ /**
36
+ * 判断响应 MIME 是否与请求 URL 扩展名不匹配(例如 .js 请求拿到 text/html)。
37
+ * 未知扩展名 / 无法解析的 URL 不校验,维持原有放行行为。
38
+ */
39
+ export declare function isMimeMismatch(request: RequestLike, response: ResponseLike): boolean;
40
+ export declare class ValidateResponsePlugin {
41
+ cacheWillUpdate({ request, response }: {
42
+ request: Request;
43
+ response: Response;
44
+ }): Promise<Response | null>;
45
+ }
46
+ /**
47
+ * 清理历史坏条目:旧版本 SW 可能把源站 HTML 存进了 JS/CSS/静态资源缓存。
48
+ * 每个坏条目原本要“再白屏一次”才被后台 revalidate 修复,激活时统一清理可立即自愈。
49
+ */
50
+ export declare function purgeMismatchedCacheEntries(cacheStorage?: CacheStorageLike): Promise<void>;
51
+ export {};
@@ -0,0 +1,111 @@
1
+ // src/sw-cache-validation.ts
2
+ var JS_MIME_TYPES = [
3
+ "application/javascript",
4
+ "text/javascript",
5
+ "application/x-javascript",
6
+ "application/ecmascript",
7
+ "text/ecmascript"
8
+ ];
9
+ var STATIC_MIME_RULES = {
10
+ js: JS_MIME_TYPES,
11
+ mjs: JS_MIME_TYPES,
12
+ css: ["text/css"],
13
+ png: ["image/png"],
14
+ jpg: ["image/jpeg"],
15
+ jpeg: ["image/jpeg"],
16
+ gif: ["image/gif"],
17
+ webp: ["image/webp"],
18
+ svg: ["image/svg+xml"],
19
+ ico: ["image/x-icon", "image/vnd.microsoft.icon"],
20
+ // 字体除 IANA 新型(font/*,2017 年注册)外,CDN/OSS 常见旧型
21
+ // application/font-* 与 application/x-font-* 一并放行,避免合法字体
22
+ // 被误判为不匹配而永远进不了缓存
23
+ woff: ["font/woff", "application/font-woff", "application/x-font-woff"],
24
+ woff2: ["font/woff2", "application/font-woff2", "application/x-font-woff2"],
25
+ ttf: ["font/ttf", "application/font-ttf", "application/x-font-ttf"],
26
+ otf: ["font/otf", "application/font-otf", "application/x-font-otf"],
27
+ eot: ["application/vnd.ms-fontobject"]
28
+ };
29
+ var JS_CSS_CACHE_NAME_PREFIX = "cache-js-css-";
30
+ var STATIC_ASSETS_CACHE_NAME_PREFIX = "cache-static-assets-";
31
+ var ASSET_CACHE_NAME_PREFIXES = [
32
+ JS_CSS_CACHE_NAME_PREFIX,
33
+ STATIC_ASSETS_CACHE_NAME_PREFIX
34
+ ];
35
+ function normalizeMimeType(contentType) {
36
+ return (contentType ?? "").split(";")[0].trim().toLowerCase();
37
+ }
38
+ function getKnownStaticExtension(url) {
39
+ let pathname;
40
+ try {
41
+ pathname = new URL(url).pathname;
42
+ } catch {
43
+ return null;
44
+ }
45
+ const lastSegment = pathname.slice(pathname.lastIndexOf("/") + 1);
46
+ const dotIndex = lastSegment.lastIndexOf(".");
47
+ if (dotIndex <= 0) {
48
+ return null;
49
+ }
50
+ const extension = lastSegment.slice(dotIndex + 1).toLowerCase();
51
+ return Object.prototype.hasOwnProperty.call(STATIC_MIME_RULES, extension) ? extension : null;
52
+ }
53
+ function isMimeMismatch(request, response) {
54
+ const extension = getKnownStaticExtension(request.url);
55
+ if (!extension) {
56
+ return false;
57
+ }
58
+ const mimeType = normalizeMimeType(response.headers.get("content-type"));
59
+ if (!mimeType) {
60
+ return true;
61
+ }
62
+ return !STATIC_MIME_RULES[extension].includes(mimeType);
63
+ }
64
+ var ValidateResponsePlugin = class {
65
+ async cacheWillUpdate({ request, response }) {
66
+ if (!response) {
67
+ console.warn("不缓存失败的响应", request.url, "No Response");
68
+ return null;
69
+ }
70
+ if (response.status === 0) {
71
+ return response;
72
+ }
73
+ if (!response.ok) {
74
+ console.warn("不缓存失败的响应", request.url, response.status);
75
+ return null;
76
+ }
77
+ if (isMimeMismatch(request, response)) {
78
+ console.warn(
79
+ "MIME 类型与资源扩展名不匹配,不缓存",
80
+ request.url,
81
+ response.headers.get("content-type")
82
+ );
83
+ return null;
84
+ }
85
+ return response;
86
+ }
87
+ };
88
+ async function purgeMismatchedCacheEntries(cacheStorage = caches) {
89
+ const cacheNames = await cacheStorage.keys();
90
+ const assetCacheNames = cacheNames.filter(
91
+ (name) => ASSET_CACHE_NAME_PREFIXES.some((prefix) => name.startsWith(prefix))
92
+ );
93
+ for (const cacheName of assetCacheNames) {
94
+ const cache = await cacheStorage.open(cacheName);
95
+ const requests = await cache.keys();
96
+ for (const request of requests) {
97
+ const cachedResponse = await cache.match(request);
98
+ if (cachedResponse && isMimeMismatch(request, cachedResponse)) {
99
+ await cache.delete(request);
100
+ console.warn("[ServiceWorker] 清理 MIME 不匹配的缓存条目", cacheName, request.url);
101
+ }
102
+ }
103
+ }
104
+ }
105
+ export {
106
+ JS_CSS_CACHE_NAME_PREFIX,
107
+ STATIC_ASSETS_CACHE_NAME_PREFIX,
108
+ ValidateResponsePlugin,
109
+ isMimeMismatch,
110
+ purgeMismatchedCacheEntries
111
+ };
@@ -0,0 +1,19 @@
1
+ import type { CachedResponseWillBeUsedCallbackParam, FetchDidFailCallbackParam, FetchDidSucceedCallbackParam, HandlerDidCompleteCallbackParam, HandlerDidRespondCallbackParam, HandlerWillStartCallbackParam, RequestWillFetchCallbackParam, WorkboxPlugin } from 'workbox-core/types';
2
+ /**
3
+ * 仅观察 script/style 的原始 SWR 请求。state 由 Workbox 为每个 handler
4
+ * 单独创建;不要把请求状态挂到共享 plugin 实例上。
5
+ */
6
+ export declare class ChunkDiagnosticsPlugin implements WorkboxPlugin {
7
+ private readonly options;
8
+ constructor(options: {
9
+ release: string;
10
+ });
11
+ handlerWillStart({ request, state }: HandlerWillStartCallbackParam): Promise<void>;
12
+ requestWillFetch({ request, state }: RequestWillFetchCallbackParam): Promise<Request>;
13
+ cachedResponseWillBeUsed({ cachedResponse, state }: CachedResponseWillBeUsedCallbackParam): Promise<Response | undefined>;
14
+ fetchDidSucceed({ response, state }: FetchDidSucceedCallbackParam): Promise<Response>;
15
+ fetchDidFail({ error, state }: FetchDidFailCallbackParam): Promise<void>;
16
+ handlerDidRespond(options: HandlerDidRespondCallbackParam): Promise<void>;
17
+ handlerDidComplete(options: HandlerDidCompleteCallbackParam): Promise<void>;
18
+ private report;
19
+ }
@@ -0,0 +1,124 @@
1
+ // src/sw-chunk-diagnostics.ts
2
+ import {
3
+ CHUNK_TRACE_MESSAGE,
4
+ createChunkDiagnosticId,
5
+ sanitizeChunkUrl
6
+ } from "./shared/chunk-diagnostics";
7
+ function safeErrorName(error) {
8
+ return /^(Error|TypeError|RangeError|AbortError|NetworkError|TimeoutError|SecurityError|InvalidStateError|QuotaExceededError|no-response)$/.test(
9
+ error.name
10
+ ) ? error.name : "Error";
11
+ }
12
+ function responseMime(response) {
13
+ return (response.headers.get("content-type") || "").split(";", 1)[0].trim();
14
+ }
15
+ var ChunkDiagnosticsPlugin = class {
16
+ constructor(options) {
17
+ this.options = options;
18
+ }
19
+ async handlerWillStart({ request, state }) {
20
+ try {
21
+ if (!state || request.destination !== "script" && request.destination !== "style") {
22
+ return;
23
+ }
24
+ state.trace = {
25
+ requestId: createChunkDiagnosticId(),
26
+ at: Date.now(),
27
+ resource: sanitizeChunkUrl(request.url),
28
+ phase: "response",
29
+ destination: request.destination,
30
+ mode: request.mode,
31
+ credentials: request.credentials,
32
+ cache: "unknown",
33
+ network: "pending",
34
+ swRelease: this.options.release
35
+ };
36
+ } catch {
37
+ }
38
+ }
39
+ async requestWillFetch({ request, state }) {
40
+ try {
41
+ const trace = state == null ? void 0 : state.trace;
42
+ if (trace) {
43
+ trace.mode = request.mode;
44
+ trace.credentials = request.credentials;
45
+ }
46
+ } catch {
47
+ }
48
+ return request;
49
+ }
50
+ async cachedResponseWillBeUsed({ cachedResponse, state }) {
51
+ try {
52
+ const trace = state == null ? void 0 : state.trace;
53
+ if (trace)
54
+ trace.cache = cachedResponse ? "hit" : "miss";
55
+ } catch {
56
+ }
57
+ return cachedResponse;
58
+ }
59
+ async fetchDidSucceed({ response, state }) {
60
+ try {
61
+ const trace = state == null ? void 0 : state.trace;
62
+ if (trace) {
63
+ trace.network = "response";
64
+ trace.networkStatus = response.status;
65
+ trace.networkType = response.type;
66
+ trace.networkMime = responseMime(response);
67
+ }
68
+ } catch {
69
+ }
70
+ return response;
71
+ }
72
+ async fetchDidFail({ error, state }) {
73
+ try {
74
+ const trace = state == null ? void 0 : state.trace;
75
+ if (trace) {
76
+ trace.network = "error";
77
+ trace.errorName = safeErrorName(error);
78
+ }
79
+ } catch {
80
+ }
81
+ }
82
+ async handlerDidRespond(options) {
83
+ this.report(options, "response");
84
+ }
85
+ async handlerDidComplete(options) {
86
+ this.report(options, "complete");
87
+ }
88
+ report({ event, response, state }, phase) {
89
+ try {
90
+ const diagnostic = state;
91
+ const trace = diagnostic == null ? void 0 : diagnostic.trace;
92
+ const sentKey = phase === "response" ? "responseSent" : "completeSent";
93
+ if (!diagnostic || !trace || diagnostic[sentKey])
94
+ return;
95
+ diagnostic[sentKey] = true;
96
+ const snapshot = {
97
+ ...trace,
98
+ phase,
99
+ ...phase === "complete" ? { completedAt: Date.now() } : {},
100
+ servedFrom: response ? trace.cache === "hit" ? "cache" : "network" : "none",
101
+ ...response ? {
102
+ responseStatus: response.status,
103
+ responseType: response.type,
104
+ responseMime: responseMime(response)
105
+ } : {}
106
+ };
107
+ const clientId = event.clientId;
108
+ if (!clientId)
109
+ return;
110
+ const delivery = (async () => {
111
+ try {
112
+ const client = await self.clients.get(clientId);
113
+ client == null ? void 0 : client.postMessage({ type: CHUNK_TRACE_MESSAGE, payload: snapshot });
114
+ } catch {
115
+ }
116
+ })();
117
+ event.waitUntil(delivery);
118
+ } catch {
119
+ }
120
+ }
121
+ };
122
+ export {
123
+ ChunkDiagnosticsPlugin
124
+ };
@@ -0,0 +1,58 @@
1
+ /** 固定计数项,避免把 URL、错误文案等高基数内容用作监控指标名。 */
2
+ export declare const SW_REJECTION_METRICS: {
3
+ readonly 'cache-write-network': "SW-CACHE-WRITE-NETWORK";
4
+ readonly 'cache-entry-exists': "SW-CACHE-ENTRY-EXISTS";
5
+ readonly 'cache-expiration-cursor': "SW-CACHE-EXPIRATION-CURSOR";
6
+ readonly 'feedback-media-no-response': "SW-FEEDBACK-MEDIA-NO-RESPONSE";
7
+ };
8
+ export interface SwRejectionDetails {
9
+ name: string;
10
+ message: string;
11
+ stack: string;
12
+ }
13
+ /** 在 SW 内转成纯字符串再 postMessage,保留 DOMException 信息并避免克隆失败。 */
14
+ export declare function serializeSwRejection(reason: unknown): SwRejectionDetails;
15
+ /**
16
+ * SW unhandledrejection 上报原则:影响不明就保留异常,已知重复故障才减少 error 频率。
17
+ * 本策略只处理未被业务捕获的 Promise 拒绝;SW 的 error 事件仍走独立的全量上报。
18
+ *
19
+ * 哪些上报:
20
+ * 1. 普通 NetworkError、Failed to fetch、AbortError、非反馈媒体的
21
+ * no-response 和所有未知错误:每次报 error。全局事件不能证明主动取消
22
+ * 或缓存已兜底;网络失败也可能是配置问题,非反馈媒体的 no-response
23
+ * (CDN chunk、页面导航)可能是资源最终加载失败(白屏信号),不能
24
+ * 当成用户断网直接忽略。
25
+ * cache-put-with-no-response 是另一种错误,同样每次报,不能用子串混为一类。
26
+ * 2. getCategory 精确匹配的四类故障:Cache.put 网络写入失败、缓存条目已存在、
27
+ * IndexedDB 游标异常、签名反馈媒体 URL 的 no-response(反馈图片/视频
28
+ * 请求失败,原因未明——日志只有 error:{},无法区分网络、取消或 CORS;
29
+ * 此类失败不伤 app 外壳)。每次通知页面记 sum;每类首次及距离上次允许
30
+ * 上报满 60 秒的下一次故障,同时报 error。其他错误名或文案变体回到第 1 条。
31
+ *
32
+ * 哪些不报 error:只有第 2 条中距上次允许上报不足 60 秒的同类重复故障。
33
+ * 此时仍通知页面记 sum,并非完全丢弃;当前没有仅凭错误名就彻底忽略的类别。
34
+ * 60 秒是控制重复量的固定窗口,不代表故障已恢复;重复事件不会推迟窗口,
35
+ * 到期也不会主动补报,必须有下一次故障才再次报 error。
36
+ *
37
+ * 限频的本质是故障详情抽样,不是"重复去重、信号不丢":限频键只有
38
+ * category,同类中不同 URL 的故障共享窗口——视频 A 首次报错后,视频 B
39
+ * 在一分钟内首次失败只剩 sum,B 的 URL 和堆栈不会上报。同时 SW 仍向
40
+ * 所有页面广播,多页面打开时一次故障可能产生多份 error/sum,本策略不
41
+ * 解决广播重复。接受这两个取舍的理由是此类故障原因未明且不伤外壳,
42
+ * 保留频次与代表性样本即可,不需要每一次详情。
43
+ *
44
+ * 为什么仍保留缓存故障:写入失败影响后续离线使用,游标异常可能中断缓存淘汰。
45
+ * 即使源于浏览器问题,也需保留频次和代表性堆栈;本策略不负责修复底层故障。
46
+ * 限频按 SW 实例、按类别独立保存,重启或时钟回拨后重新允许上报。
47
+ * sum 是页面接收通知的次数,多页面会重复、无客户端会漏收,不是准确的故障总数。
48
+ * 新旧 SW/页面共存时,旧页面不识别限频字段会继续全量报 error,待页面更新后生效。
49
+ * 若以后要忽略主动取消或成功兜底的请求,应在掌握请求结果的调用点处理,
50
+ * 不应在这里扩充关键词黑名单。NetworkFirst 超时只尝试缓存兜底,不会 abort 请求。
51
+ */
52
+ export declare function createSwRejectionPolicy(): (details: SwRejectionDetails, now?: number) => {
53
+ reportError: boolean;
54
+ category?: undefined;
55
+ } | {
56
+ category: "cache-write-network" | "cache-entry-exists" | "cache-expiration-cursor" | "feedback-media-no-response";
57
+ reportError: boolean;
58
+ };
@@ -0,0 +1,87 @@
1
+ // src/sw-error-filter.ts
2
+ var SW_REJECTION_METRICS = {
3
+ "cache-write-network": "SW-CACHE-WRITE-NETWORK",
4
+ "cache-entry-exists": "SW-CACHE-ENTRY-EXISTS",
5
+ "cache-expiration-cursor": "SW-CACHE-EXPIRATION-CURSOR",
6
+ "feedback-media-no-response": "SW-FEEDBACK-MEDIA-NO-RESPONSE"
7
+ };
8
+ function serializeSwRejection(reason) {
9
+ try {
10
+ if (reason !== null && typeof reason === "object") {
11
+ const error = reason;
12
+ return {
13
+ name: typeof error.name === "string" ? error.name : "",
14
+ message: typeof error.message === "string" ? error.message : JSON.stringify(reason) || String(reason),
15
+ stack: typeof error.stack === "string" ? error.stack : ""
16
+ };
17
+ }
18
+ return { name: "", message: String(reason), stack: "" };
19
+ } catch {
20
+ return {
21
+ name: "",
22
+ message: "[Unserializable rejection reason]",
23
+ stack: ""
24
+ };
25
+ }
26
+ }
27
+ var NO_RESPONSE_MESSAGE_PREFIX = "no-response :: ";
28
+ var FEEDBACK_MEDIA_HOST = "super-acme-online.oss-cn-hangzhou.aliyuncs.com";
29
+ var FEEDBACK_MEDIA_PATH_PREFIXES = ["/saas/feedbackVideo/", "/user-center/feedback/"];
30
+ function isSignedFeedbackMediaNoResponse(message) {
31
+ var _a;
32
+ if (!message.startsWith(NO_RESPONSE_MESSAGE_PREFIX)) {
33
+ return false;
34
+ }
35
+ try {
36
+ const details = JSON.parse(message.slice(NO_RESPONSE_MESSAGE_PREFIX.length));
37
+ const url = Array.isArray(details) ? (_a = details[0]) == null ? void 0 : _a.url : void 0;
38
+ if (typeof url !== "string") {
39
+ return false;
40
+ }
41
+ const parsed = new URL(url);
42
+ if (parsed.hostname !== FEEDBACK_MEDIA_HOST) {
43
+ return false;
44
+ }
45
+ if (!FEEDBACK_MEDIA_PATH_PREFIXES.some((prefix) => parsed.pathname.startsWith(prefix))) {
46
+ return false;
47
+ }
48
+ return parsed.searchParams.has("Expires") && parsed.searchParams.has("Signature");
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+ function getCategory({ name, message }) {
54
+ if (name === "NetworkError" && message === "Failed to execute 'put' on 'Cache': Cache.put() encountered a network error") {
55
+ return "cache-write-network";
56
+ }
57
+ if (name === "InvalidAccessError" && message === "Failed to execute 'put' on 'Cache': Entry already exists.") {
58
+ return "cache-entry-exists";
59
+ }
60
+ if (name === "UnknownError" && message === "Attempt to iterate a cursor that doesn't exist") {
61
+ return "cache-expiration-cursor";
62
+ }
63
+ if (name === "no-response" && isSignedFeedbackMediaNoResponse(message)) {
64
+ return "feedback-media-no-response";
65
+ }
66
+ return void 0;
67
+ }
68
+ function createSwRejectionPolicy() {
69
+ const lastReported = /* @__PURE__ */ new Map();
70
+ return (details, now = Date.now()) => {
71
+ const category = getCategory(details);
72
+ if (!category) {
73
+ return { reportError: true };
74
+ }
75
+ const previous = lastReported.get(category);
76
+ const reportError = previous === void 0 || now < previous || now - previous >= 6e4;
77
+ if (reportError) {
78
+ lastReported.set(category, now);
79
+ }
80
+ return { category, reportError };
81
+ };
82
+ }
83
+ export {
84
+ SW_REJECTION_METRICS,
85
+ createSwRejectionPolicy,
86
+ serializeSwRejection
87
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ // src/sw-polyfills.ts
2
+ if (typeof Promise.allSettled !== "function") {
3
+ Promise.allSettled = function(values) {
4
+ return Promise.all(
5
+ Array.from(
6
+ values,
7
+ (item) => Promise.resolve(item).then(
8
+ (value) => ({ status: "fulfilled", value }),
9
+ (reason) => ({ status: "rejected", reason })
10
+ )
11
+ )
12
+ );
13
+ };
14
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * SW 缓存路由的协议守卫:仅 http(s) 请求允许进入 workbox 缓存路由。
3
+ *
4
+ * 注:workbox-routing 已在路由匹配前排除非 http(s) 请求——
5
+ * Router.handleRequest 中 `!url.protocol.startsWith('http')` 直接 return,
6
+ * blob:/data: 不会 respondWith,由浏览器本地解析。本守卫与框架行为等价,
7
+ * 属防御性加固:防止未来改用自定义 fetch 监听或路由重构时,blob:/data:
8
+ * 被误引入缓存语义。
9
+ *
10
+ * 线上 blob: 图片加载失败(SLS 202609,Android WebView ×3)与本守卫无关,
11
+ * 根因未定位,需单独排查。
12
+ */
13
+ export declare function isHttpOrHttpsRequest(url: string): boolean;
@@ -0,0 +1,7 @@
1
+ // src/sw-request-filter.ts
2
+ function isHttpOrHttpsRequest(url) {
3
+ return url.startsWith("https://") || url.startsWith("http://");
4
+ }
5
+ export {
6
+ isHttpOrHttpsRequest
7
+ };
@@ -0,0 +1,4 @@
1
+ import './sw-polyfills';
2
+ export declare function setupServiceWorker(options: {
3
+ release: string;
4
+ }): void;