zip-peek 0.7.2 → 0.7.3
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 +27 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +164 -24
- package/dist/types.d.ts +17 -2
- package/dist/types.js +5 -1
- package/dist/version.d.ts +2 -0
- package/dist/version.js +5 -0
- package/dist/zipServiceWorker.js +1 -1
- package/dist/zipServiceWorker.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ The package provides two pieces:
|
|
|
40
40
|
At runtime, the package:
|
|
41
41
|
|
|
42
42
|
1. Registers the ZIP service worker with the configured `workerUrl` and `scopeUrl`.
|
|
43
|
-
2. Reloads once after first service worker install when needed, so the page becomes controlled.
|
|
43
|
+
2. Reloads once after first service worker install when needed, so the page becomes controlled. On later loads it calls `registration.update()` and reloads once when a newer worker version activates.
|
|
44
44
|
3. Intercepts matching `.zip/...` asset requests within the service worker scope.
|
|
45
45
|
4. Loads and caches the ZIP central directory on first use, or during init when `allowedZipUrls` is provided.
|
|
46
46
|
5. Streams the entry byte span (`offset` through `nextOffset − 1`), parses the local file header, and incrementally inflates deflated entries using `fflate`.
|
|
@@ -95,13 +95,14 @@ type InitZipPeekOptions = {
|
|
|
95
95
|
workerUrl: string;
|
|
96
96
|
scopeUrl: string;
|
|
97
97
|
reloadOnFirstInstall?: boolean;
|
|
98
|
+
reloadOnWorkerUpdate?: boolean;
|
|
98
99
|
cacheClearingStrategy?: ZipCacheClearingStrategy;
|
|
99
100
|
allowedZipUrls?: string[];
|
|
100
101
|
requireExactManifestPath?: boolean;
|
|
101
102
|
zipAssetCacheName?: string;
|
|
102
103
|
assetCacheTtlMs?: number;
|
|
103
104
|
logPrefix?: string;
|
|
104
|
-
onError?: (error: Error, info
|
|
105
|
+
onError?: (error: Error, info: string | undefined, level: "error" | "warn") => void;
|
|
105
106
|
};
|
|
106
107
|
|
|
107
108
|
type ZipCacheClearingStrategy = "keep-all" | "keep-allowed-urls" | "clear-all";
|
|
@@ -152,6 +153,16 @@ Choose the smallest scope that includes the pages and asset requests you want th
|
|
|
152
153
|
|
|
153
154
|
Defaults to `true`. When a service worker is installed for the first time, the current page may not yet be controlled by it. With this option enabled, the package reloads once and returns `{ reloaded: true, initialized: false }`.
|
|
154
155
|
|
|
156
|
+
`reloadOnWorkerUpdate`
|
|
157
|
+
|
|
158
|
+
Defaults to `true`. When a matching registration already exists, `initZipPeek()` calls `registration.update()` so a new `zipServiceWorker.js` is found on this load instead of after the browser's own update check. If the page already has a controller and a newer worker is installing, init waits up to 10 seconds for it to activate, then reloads the page and returns `{ reloaded: true, initialized: false }`. If the new worker fails to install or does not activate in time, init continues on the current worker and `onError` receives a `warn` titled `Updated service worker did not activate` (`reason=redundant` or `reason=timeout`).
|
|
159
|
+
|
|
160
|
+
After a successful init, zip-peek also reloads the page if the controlling worker changes mid-session (for example after the browser's own update check), because the new worker has not received this page's configuration.
|
|
161
|
+
|
|
162
|
+
Set it to `false` to skip both reloads. Init then sends its configuration to the newly activated worker and continues without a reload. A mid-session worker change is not reloaded either, and that worker runs without this page's configuration until the next init.
|
|
163
|
+
|
|
164
|
+
The reload is `window.location.reload()` of the window that runs `initZipPeek()`. Inside an iframe, only the iframe reloads.
|
|
165
|
+
|
|
155
166
|
`cacheClearingStrategy`
|
|
156
167
|
|
|
157
168
|
Defaults to `'keep-all'`. Controls which zip-peek cache entries are cleared during initialization.
|
|
@@ -188,15 +199,17 @@ When fallback resolution succeeds, the asset is still served normally, but `onEr
|
|
|
188
199
|
|
|
189
200
|
`onError`
|
|
190
201
|
|
|
191
|
-
Optional error handler for zip-peek failures and informational reports from the service worker. Receives `(error, info
|
|
202
|
+
Optional error handler for zip-peek failures and informational reports from the service worker. Receives `(error, info, level)`. `level` is `"warn"` for a thrown network fetch, a file missing from the manifest, a successful zip-basename fallback, a service worker version mismatch, and an updated worker that does not activate. Every other report is `"error"`.
|
|
192
203
|
|
|
193
204
|
- `error.message` is a **stable title** for telemetry grouping, always prefixed with `[zip-peek-error]`. Unique presigned query strings are not included in the title.
|
|
194
|
-
- `info` holds request-specific details (`path=… zipPath=… reason=… status=… cause=…`).
|
|
205
|
+
- `info` holds request-specific details (`path=… zipPath=… reason=… status=… cause=… zipSW=… zipPeek=…`). `zipSW` is the service-worker build version (the package version baked into `zipServiceWorker.js`). `zipPeek` is the zip-peek package version running on the page. They match on a fresh install and differ when a cached worker is older than the page. Manifest preload and manifest fetch failures also include `phase` (`tail` or `cd`) and `online`. The init step label is kept in front of those fields.
|
|
195
206
|
|
|
196
207
|
Service-worker fetch failures post `ZIP_SW_ERROR` so `onError` runs (allow-list 403, missing manifest/file 404, renew 502, origin/handler 500). A thrown `fetch()` (offline, DNS, CORS) uses `[zip-peek-error] [NetworkError] ZIP asset fetch failed` or `[zip-peek-error] [NetworkError] ZIP manifest fetch failed`. An HTTP error response from origin is not tagged `[NetworkError]`.
|
|
197
208
|
|
|
198
209
|
Besides those failures, `onError` is also called when exact manifest lookup fails but zip-basename folder fallback succeeds (see `requireExactManifestPath`). Use this to log or telemetry mismatches between request paths and manifest layout without blocking the response.
|
|
199
210
|
|
|
211
|
+
During init, zip-peek asks the worker for its build version. If it differs from the page's zip-peek version, `onError` receives a `warn` titled `Service worker version mismatch` with `info` such as `zipSW=0.7.2 zipPeek=0.7.3`. This means the page and the worker came from different deploys, for example because a CDN served an old `zipServiceWorker.js`. Workers built before this check do not answer; after a 500 ms timeout the same warning is reported with `zipSW=unknown`. Init continues in every case.
|
|
212
|
+
|
|
200
213
|
`zipAssetCacheName`
|
|
201
214
|
|
|
202
215
|
Overrides the browser Cache API bucket used by the service worker for ZIP manifests and extracted assets.
|
|
@@ -375,6 +388,16 @@ or:
|
|
|
375
388
|
Service-Worker-Allowed: /
|
|
376
389
|
```
|
|
377
390
|
|
|
391
|
+
### 6. Do not cache a stable worker URL
|
|
392
|
+
|
|
393
|
+
When the worker is served under a stable name such as `zipServiceWorker.js`, serve it with:
|
|
394
|
+
|
|
395
|
+
```http
|
|
396
|
+
Cache-Control: no-cache
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
(or `max-age=0`), and make sure a CDN in front of it does not cache it for long. The browser skips its own HTTP cache when checking for worker updates, but a CDN or proxy can still return an old script. In that case `registration.update()` finds nothing new, and pages keep running the old worker. The `Service worker version mismatch` warning in `onError` shows when this happens.
|
|
400
|
+
|
|
378
401
|
## Bundler Configuration
|
|
379
402
|
|
|
380
403
|
The package ships the worker as:
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { InitZipPeekOptions, InitZipPeekResult, RenewPresignedUrlOptions } from "./types";
|
|
2
2
|
import { isZipPackage } from "./zip-utils";
|
|
3
|
-
export type { InitZipPeekOptions, InitZipPeekResult, RenewPresignedUrlOptions, ZipCacheClearingStrategy, ZipPeekErrorHandler, ZipPeekUrlExpiryHandler, ZipWorkerConfig, } from "./types";
|
|
3
|
+
export type { InitZipPeekOptions, InitZipPeekResult, RenewPresignedUrlOptions, ZipCacheClearingStrategy, ZipPeekErrorHandler, ZipPeekLogLevel, ZipPeekUrlExpiryHandler, ZipWorkerConfig, } from "./types";
|
|
4
4
|
export { getPresignedUrlExpiryMs, isPresignedUrlExpired } from "./zip-utils";
|
|
5
5
|
/**
|
|
6
6
|
* Registers the zip-peek service worker and configures lazy ZIP asset loading.
|
|
@@ -23,7 +23,7 @@ export { getPresignedUrlExpiryMs, isPresignedUrlExpired } from "./zip-utils";
|
|
|
23
23
|
* sleep or backgrounding, and when a cache-miss fetch finds the URL already
|
|
24
24
|
* expired. Concurrent callers share one in-flight renewal.
|
|
25
25
|
*/
|
|
26
|
-
export declare function initZipPeek({ workerUrl, scopeUrl, reloadOnFirstInstall, cacheClearingStrategy, allowedZipUrls, requireExactManifestPath, zipAssetCacheName, assetCacheTtlMs, logPrefix, onError, onUrlExpiry, }: InitZipPeekOptions): Promise<InitZipPeekResult>;
|
|
26
|
+
export declare function initZipPeek({ workerUrl, scopeUrl, reloadOnFirstInstall, reloadOnWorkerUpdate, cacheClearingStrategy, allowedZipUrls, requireExactManifestPath, zipAssetCacheName, assetCacheTtlMs, logPrefix, onError, onUrlExpiry, }: InitZipPeekOptions): Promise<InitZipPeekResult>;
|
|
27
27
|
/**
|
|
28
28
|
* Replace a presigned ZIP package URL after renewal so the service worker
|
|
29
29
|
* continue serving assets with the new credentials.
|
package/dist/index.js
CHANGED
|
@@ -4,21 +4,38 @@ exports.isZipPackage = exports.isPresignedUrlExpired = exports.getPresignedUrlEx
|
|
|
4
4
|
exports.initZipPeek = initZipPeek;
|
|
5
5
|
exports.renewPresignedUrl = renewPresignedUrl;
|
|
6
6
|
const types_1 = require("./types");
|
|
7
|
+
const version_1 = require("./version");
|
|
7
8
|
const zip_utils_1 = require("./zip-utils");
|
|
8
9
|
Object.defineProperty(exports, "isZipPackage", { enumerable: true, get: function () { return zip_utils_1.isZipPackage; } });
|
|
9
10
|
var zip_utils_2 = require("./zip-utils");
|
|
10
11
|
Object.defineProperty(exports, "getPresignedUrlExpiryMs", { enumerable: true, get: function () { return zip_utils_2.getPresignedUrlExpiryMs; } });
|
|
11
12
|
Object.defineProperty(exports, "isPresignedUrlExpired", { enumerable: true, get: function () { return zip_utils_2.isPresignedUrlExpired; } });
|
|
13
|
+
const ZIP_PEEK_VERSION_FIELD = `zipPeek=${version_1.ZIP_PEEK_VERSION}`;
|
|
14
|
+
/** Append the page package version. Kept off error.message so titles stay stable. */
|
|
15
|
+
function withPackageVersion(info) {
|
|
16
|
+
if (!info)
|
|
17
|
+
return ZIP_PEEK_VERSION_FIELD;
|
|
18
|
+
if (info.includes("zipPeek="))
|
|
19
|
+
return info;
|
|
20
|
+
return `${info} ${ZIP_PEEK_VERSION_FIELD}`;
|
|
21
|
+
}
|
|
12
22
|
/** Default lead time before presigned URL expiry when scheduling renewal. */
|
|
13
23
|
const URL_EXPIRY_LEAD_MS = 10000;
|
|
14
24
|
/** Retry delay when onUrlExpiry runs but the URL is still tracked (renew failed / skipped). */
|
|
15
25
|
const URL_EXPIRY_RETRY_MS = 5000;
|
|
16
26
|
/** Avoid delayMs=0 tight loops while a renewal is already in flight. */
|
|
17
27
|
const URL_EXPIRY_MIN_DELAY_MS = 1000;
|
|
28
|
+
/** How long init waits for an updated service worker to activate before continuing on the current one. */
|
|
29
|
+
const WORKER_ACTIVATION_TIMEOUT_MS = 10000;
|
|
30
|
+
/** Workers built before ZIP_SW_VERSION never reply, so the version check must not wait forever. */
|
|
31
|
+
const WORKER_VERSION_TIMEOUT_MS = 500;
|
|
18
32
|
let currentOnError;
|
|
19
33
|
let currentOnUrlExpiry;
|
|
34
|
+
let currentReloadOnWorkerUpdate = true;
|
|
20
35
|
let clientMessageListenerInstalled = false;
|
|
36
|
+
let controllerChangeListenerInstalled = false;
|
|
21
37
|
let expiryWakeListenersInstalled = false;
|
|
38
|
+
let reloadRequested = false;
|
|
22
39
|
/**
|
|
23
40
|
* Last allow-list sent to the service worker from this page.
|
|
24
41
|
* `undefined` = initZipPeek not completed; `null` = unrestricted; `string[]` = claim.
|
|
@@ -31,6 +48,39 @@ const trackedZipUrls = new Map();
|
|
|
31
48
|
/** In-flight onUrlExpiry promises keyed by normalized zip URL. */
|
|
32
49
|
const urlExpiryInFlight = new Map();
|
|
33
50
|
const sessionManifests = new Map();
|
|
51
|
+
function reloadPage(reason) {
|
|
52
|
+
if (reloadRequested) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
reloadRequested = true;
|
|
56
|
+
console.log(ZIP_PEEK_VERSION_FIELD, `Reloading page: ${reason}`);
|
|
57
|
+
window.location.reload();
|
|
58
|
+
}
|
|
59
|
+
function waitForWorkerActivated(worker, timeoutMs) {
|
|
60
|
+
if (worker.state === "activated") {
|
|
61
|
+
return Promise.resolve("activated");
|
|
62
|
+
}
|
|
63
|
+
if (worker.state === "redundant") {
|
|
64
|
+
return Promise.resolve("redundant");
|
|
65
|
+
}
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
const finish = (result) => {
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
worker.removeEventListener("statechange", onStateChange);
|
|
70
|
+
resolve(result);
|
|
71
|
+
};
|
|
72
|
+
const onStateChange = () => {
|
|
73
|
+
if (worker.state === "activated") {
|
|
74
|
+
finish("activated");
|
|
75
|
+
}
|
|
76
|
+
else if (worker.state === "redundant") {
|
|
77
|
+
finish("redundant");
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
const timer = setTimeout(() => finish("timeout"), timeoutMs);
|
|
81
|
+
worker.addEventListener("statechange", onStateChange);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
34
84
|
function normalizeScopePath(href) {
|
|
35
85
|
let p = new URL(href).pathname;
|
|
36
86
|
if (p.length > 1 && p.endsWith("/")) {
|
|
@@ -79,38 +129,60 @@ async function unregisterMismatchedScopeZipServiceWorkerIfNeeded(expectedWorkerU
|
|
|
79
129
|
const regScopePath = normalizeScopePath(reg.scope);
|
|
80
130
|
if (regScopePath !== wantScopePath) {
|
|
81
131
|
await reg.unregister();
|
|
82
|
-
console.log(`Unregistered zip service worker (scope ${reg.scope}); re-registering with ${expectedScopeUrl}`);
|
|
132
|
+
console.log(ZIP_PEEK_VERSION_FIELD, `Unregistered zip service worker (scope ${reg.scope}); re-registering with ${expectedScopeUrl}`);
|
|
83
133
|
}
|
|
84
134
|
}
|
|
85
135
|
}
|
|
86
|
-
async function ensureZipServiceWorkerRegistered({ workerUrl, scopeUrl, reloadOnFirstInstall = true, }) {
|
|
87
|
-
var _a, _b, _c;
|
|
136
|
+
async function ensureZipServiceWorkerRegistered({ workerUrl, scopeUrl, reloadOnFirstInstall = true, reloadOnWorkerUpdate = true, }) {
|
|
137
|
+
var _a, _b, _c, _d;
|
|
88
138
|
if (!("serviceWorker" in navigator)) {
|
|
89
139
|
return false;
|
|
90
140
|
}
|
|
141
|
+
const hadController = Boolean(navigator.serviceWorker.controller);
|
|
91
142
|
await unregisterMismatchedScopeZipServiceWorkerIfNeeded(workerUrl, scopeUrl);
|
|
92
143
|
const existing = await navigator.serviceWorker.getRegistration(scopeUrl);
|
|
93
144
|
const alreadyRegistered = existing &&
|
|
94
145
|
(zipServiceWorkerScriptMatches((_a = existing.active) === null || _a === void 0 ? void 0 : _a.scriptURL, workerUrl) ||
|
|
95
146
|
zipServiceWorkerScriptMatches((_b = existing.waiting) === null || _b === void 0 ? void 0 : _b.scriptURL, workerUrl) ||
|
|
96
147
|
zipServiceWorkerScriptMatches((_c = existing.installing) === null || _c === void 0 ? void 0 : _c.scriptURL, workerUrl));
|
|
148
|
+
let registration;
|
|
97
149
|
if (!alreadyRegistered) {
|
|
98
150
|
try {
|
|
99
|
-
await navigator.serviceWorker.register(workerUrl, { scope: scopeUrl });
|
|
100
|
-
console.log("Zip service worker registered successfully");
|
|
151
|
+
registration = await navigator.serviceWorker.register(workerUrl, { scope: scopeUrl });
|
|
152
|
+
console.log(ZIP_PEEK_VERSION_FIELD, "Zip service worker registered successfully");
|
|
101
153
|
}
|
|
102
154
|
catch (error) {
|
|
103
155
|
const context = describeRegistrationFailureContext();
|
|
104
|
-
console.error("zip-peek: service worker registration failed", context, error);
|
|
156
|
+
console.error(ZIP_PEEK_VERSION_FIELD, "zip-peek: service worker registration failed", context, error);
|
|
105
157
|
throw attachZipPeekInfo(error, `Service worker registration failed (${context})`);
|
|
106
158
|
}
|
|
107
159
|
}
|
|
108
160
|
else {
|
|
109
|
-
|
|
161
|
+
registration = existing;
|
|
162
|
+
console.log(ZIP_PEEK_VERSION_FIELD, "Zip service worker already registered; checking for updates.");
|
|
163
|
+
try {
|
|
164
|
+
await existing.update();
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
console.warn(ZIP_PEEK_VERSION_FIELD, "zip-peek: service worker update check failed", error);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const incomingWorker = (_d = registration.installing) !== null && _d !== void 0 ? _d : registration.waiting;
|
|
171
|
+
if (hadController && incomingWorker) {
|
|
172
|
+
const result = await waitForWorkerActivated(incomingWorker, WORKER_ACTIVATION_TIMEOUT_MS);
|
|
173
|
+
if (result === "activated") {
|
|
174
|
+
if (reloadOnWorkerUpdate) {
|
|
175
|
+
reloadPage("service worker updated");
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
reportZipPeekError(currentOnError, (0, types_1.createZipPeekError)("Updated service worker did not activate", "warn"), `reason=${result}`);
|
|
181
|
+
}
|
|
110
182
|
}
|
|
111
183
|
await navigator.serviceWorker.ready;
|
|
112
184
|
if (reloadOnFirstInstall && !navigator.serviceWorker.controller) {
|
|
113
|
-
|
|
185
|
+
reloadPage("first service worker install");
|
|
114
186
|
return true;
|
|
115
187
|
}
|
|
116
188
|
return false;
|
|
@@ -138,21 +210,26 @@ function describeRegistrationFailureContext() {
|
|
|
138
210
|
}
|
|
139
211
|
function attachZipPeekInfo(error, info) {
|
|
140
212
|
const err = (0, types_1.toZipPeekError)(error);
|
|
141
|
-
if (info)
|
|
142
|
-
err
|
|
143
|
-
|
|
213
|
+
if (!info)
|
|
214
|
+
return err;
|
|
215
|
+
const step = (0, types_1.formatZipPeekError)(info);
|
|
216
|
+
err.zipPeekInfo = err.zipPeekInfo ? `${step} ${err.zipPeekInfo}` : step;
|
|
144
217
|
return err;
|
|
145
218
|
}
|
|
146
219
|
function reportZipPeekError(onError, error, info) {
|
|
147
220
|
const err = (0, types_1.toZipPeekError)(error);
|
|
148
|
-
onError === null || onError === void 0 ? void 0 : onError(err, info !== null && info !== void 0 ? info : err.zipPeekInfo);
|
|
221
|
+
onError === null || onError === void 0 ? void 0 : onError(err, withPackageVersion(info !== null && info !== void 0 ? info : err.zipPeekInfo), err.zipPeekLevel);
|
|
149
222
|
}
|
|
150
223
|
function errorFromWorkerResponse(response) {
|
|
151
|
-
var _a;
|
|
224
|
+
var _a, _b;
|
|
152
225
|
const error = new Error((_a = response.error) !== null && _a !== void 0 ? _a : (0, types_1.formatZipPeekError)("Service worker did not acknowledge the message."));
|
|
153
226
|
if (response.errorStack) {
|
|
154
227
|
error.stack = response.errorStack;
|
|
155
228
|
}
|
|
229
|
+
if (response.info) {
|
|
230
|
+
error.zipPeekInfo = response.info;
|
|
231
|
+
}
|
|
232
|
+
error.zipPeekLevel = (_b = response.level) !== null && _b !== void 0 ? _b : "error";
|
|
156
233
|
return error;
|
|
157
234
|
}
|
|
158
235
|
function clearUrlExpiryTimer(normalizedZipUrl) {
|
|
@@ -312,7 +389,7 @@ function ensureClientMessageListener() {
|
|
|
312
389
|
}
|
|
313
390
|
clientMessageListenerInstalled = true;
|
|
314
391
|
navigator.serviceWorker.addEventListener("message", (event) => {
|
|
315
|
-
var _a, _b, _c;
|
|
392
|
+
var _a, _b, _c, _d;
|
|
316
393
|
const data = event.data;
|
|
317
394
|
if (!data) {
|
|
318
395
|
return;
|
|
@@ -364,7 +441,26 @@ function ensureClientMessageListener() {
|
|
|
364
441
|
if ((_c = data.error) === null || _c === void 0 ? void 0 : _c.stack) {
|
|
365
442
|
error.stack = data.error.stack;
|
|
366
443
|
}
|
|
367
|
-
currentOnError(error, data.info ? (0, types_1.formatZipPeekError)(data.info) : undefined);
|
|
444
|
+
currentOnError(error, withPackageVersion(data.info ? (0, types_1.formatZipPeekError)(data.info) : undefined), (_d = data.level) !== null && _d !== void 0 ? _d : "error");
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* After init, a worker swap (browser soft update + skipWaiting/claim) leaves
|
|
449
|
+
* the page talking to a worker that never received ZIP_SW_CONFIG, so reload.
|
|
450
|
+
*/
|
|
451
|
+
function ensureControllerChangeListener() {
|
|
452
|
+
if (controllerChangeListenerInstalled || !("serviceWorker" in navigator)) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
controllerChangeListenerInstalled = true;
|
|
456
|
+
let hadController = Boolean(navigator.serviceWorker.controller);
|
|
457
|
+
navigator.serviceWorker.addEventListener("controllerchange", () => {
|
|
458
|
+
const previouslyControlled = hadController;
|
|
459
|
+
hadController = Boolean(navigator.serviceWorker.controller);
|
|
460
|
+
if (!previouslyControlled || !currentReloadOnWorkerUpdate) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
reloadPage("service worker changed mid-session");
|
|
368
464
|
});
|
|
369
465
|
}
|
|
370
466
|
function normalizeAllowedZipUrls(allowedZipUrls) {
|
|
@@ -372,13 +468,13 @@ function normalizeAllowedZipUrls(allowedZipUrls) {
|
|
|
372
468
|
return undefined;
|
|
373
469
|
}
|
|
374
470
|
if (allowedZipUrls.length === 0) {
|
|
375
|
-
throw (0, types_1.createZipPeekError)("allowedZipUrls must contain at least one ZIP URL when provided.");
|
|
471
|
+
throw (0, types_1.createZipPeekError)("allowedZipUrls must contain at least one ZIP URL when provided.", "error");
|
|
376
472
|
}
|
|
377
473
|
const normalized = new Set();
|
|
378
474
|
for (const zipUrl of allowedZipUrls) {
|
|
379
475
|
const resolvedZipUrl = new URL(zipUrl, window.location.href).href;
|
|
380
476
|
if (!(0, zip_utils_1.isZipPackage)(resolvedZipUrl)) {
|
|
381
|
-
throw (0, types_1.createZipPeekError)(`allowedZipUrls contains a non-ZIP URL: ${zipUrl}
|
|
477
|
+
throw (0, types_1.createZipPeekError)(`allowedZipUrls contains a non-ZIP URL: ${zipUrl}`, "error");
|
|
382
478
|
}
|
|
383
479
|
normalized.add((0, zip_utils_1.normalizeZipUrl)(resolvedZipUrl));
|
|
384
480
|
}
|
|
@@ -388,7 +484,7 @@ async function postWorkerMessage(message) {
|
|
|
388
484
|
var _a;
|
|
389
485
|
const worker = (_a = navigator.serviceWorker.controller) !== null && _a !== void 0 ? _a : (await navigator.serviceWorker.ready).active;
|
|
390
486
|
if (!worker) {
|
|
391
|
-
throw (0, types_1.createZipPeekError)("No active service worker.");
|
|
487
|
+
throw (0, types_1.createZipPeekError)("No active service worker.", "error");
|
|
392
488
|
}
|
|
393
489
|
return new Promise((resolve, reject) => {
|
|
394
490
|
const channel = new MessageChannel();
|
|
@@ -404,11 +500,51 @@ async function postWorkerMessage(message) {
|
|
|
404
500
|
};
|
|
405
501
|
channel.port1.onmessageerror = () => {
|
|
406
502
|
channel.port1.close();
|
|
407
|
-
reject((0, types_1.createZipPeekError)("Failed to deserialize the service worker response on MessageChannel."));
|
|
503
|
+
reject((0, types_1.createZipPeekError)("Failed to deserialize the service worker response on MessageChannel.", "error"));
|
|
408
504
|
};
|
|
409
505
|
worker.postMessage(message, [channel.port2]);
|
|
410
506
|
});
|
|
411
507
|
}
|
|
508
|
+
async function readWorkerVersion(timeoutMs) {
|
|
509
|
+
var _a;
|
|
510
|
+
const worker = (_a = navigator.serviceWorker.controller) !== null && _a !== void 0 ? _a : (await navigator.serviceWorker.ready).active;
|
|
511
|
+
if (!worker) {
|
|
512
|
+
return undefined;
|
|
513
|
+
}
|
|
514
|
+
return new Promise((resolve) => {
|
|
515
|
+
const channel = new MessageChannel();
|
|
516
|
+
const finish = (version) => {
|
|
517
|
+
clearTimeout(timer);
|
|
518
|
+
channel.port1.onmessage = null;
|
|
519
|
+
channel.port1.close();
|
|
520
|
+
resolve(version);
|
|
521
|
+
};
|
|
522
|
+
const timer = setTimeout(() => finish(undefined), timeoutMs);
|
|
523
|
+
channel.port1.onmessage = (event) => {
|
|
524
|
+
const data = event.data;
|
|
525
|
+
finish(typeof (data === null || data === void 0 ? void 0 : data.version) === "string" ? data.version : undefined);
|
|
526
|
+
};
|
|
527
|
+
try {
|
|
528
|
+
worker.postMessage({ type: "ZIP_SW_VERSION" }, [channel.port2]);
|
|
529
|
+
}
|
|
530
|
+
catch (_a) {
|
|
531
|
+
finish(undefined);
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
/** Diagnostics only: reports a page/worker build mismatch through onError and never blocks init. */
|
|
536
|
+
async function checkWorkerVersion(onError) {
|
|
537
|
+
try {
|
|
538
|
+
const workerVersion = await readWorkerVersion(WORKER_VERSION_TIMEOUT_MS);
|
|
539
|
+
if (workerVersion === version_1.ZIP_PEEK_VERSION) {
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
reportZipPeekError(onError, (0, types_1.createZipPeekError)("Service worker version mismatch", "warn"), `zipSW=${workerVersion !== null && workerVersion !== void 0 ? workerVersion : "unknown"}`);
|
|
543
|
+
}
|
|
544
|
+
catch (error) {
|
|
545
|
+
console.warn(ZIP_PEEK_VERSION_FIELD, "zip-peek: service worker version check failed", error);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
412
548
|
async function postWorkerMessageOrThrow(message, info) {
|
|
413
549
|
try {
|
|
414
550
|
await postWorkerMessage(message);
|
|
@@ -438,21 +574,22 @@ async function postWorkerMessageOrThrow(message, info) {
|
|
|
438
574
|
* sleep or backgrounding, and when a cache-miss fetch finds the URL already
|
|
439
575
|
* expired. Concurrent callers share one in-flight renewal.
|
|
440
576
|
*/
|
|
441
|
-
async function initZipPeek({ workerUrl, scopeUrl, reloadOnFirstInstall = true, cacheClearingStrategy = "keep-all", allowedZipUrls, requireExactManifestPath = false, zipAssetCacheName, assetCacheTtlMs, logPrefix, onError, onUrlExpiry, }) {
|
|
577
|
+
async function initZipPeek({ workerUrl, scopeUrl, reloadOnFirstInstall = true, reloadOnWorkerUpdate = true, cacheClearingStrategy = "keep-all", allowedZipUrls, requireExactManifestPath = false, zipAssetCacheName, assetCacheTtlMs, logPrefix, onError, onUrlExpiry, }) {
|
|
442
578
|
if (onError) {
|
|
443
579
|
currentOnError = onError;
|
|
444
580
|
}
|
|
445
581
|
if (onUrlExpiry) {
|
|
446
582
|
currentOnUrlExpiry = onUrlExpiry;
|
|
447
583
|
}
|
|
584
|
+
currentReloadOnWorkerUpdate = reloadOnWorkerUpdate;
|
|
448
585
|
try {
|
|
449
586
|
const normalizedAllowedZipUrls = normalizeAllowedZipUrls(allowedZipUrls);
|
|
450
587
|
if (cacheClearingStrategy === "keep-allowed-urls" && !normalizedAllowedZipUrls) {
|
|
451
|
-
throw (0, types_1.createZipPeekError)('cacheClearingStrategy "keep-allowed-urls" requires a non-empty allowedZipUrls allow-list.');
|
|
588
|
+
throw (0, types_1.createZipPeekError)('cacheClearingStrategy "keep-allowed-urls" requires a non-empty allowedZipUrls allow-list.', "error");
|
|
452
589
|
}
|
|
453
590
|
if (!("serviceWorker" in navigator)) {
|
|
454
591
|
// Usually happens in the built in Facebook/Messenger browser.
|
|
455
|
-
console.warn("zip-peek: Service Worker API not supported in this browser.");
|
|
592
|
+
console.warn(ZIP_PEEK_VERSION_FIELD, "zip-peek: Service Worker API not supported in this browser.");
|
|
456
593
|
alert("Browser not supported. Please try a different browser.");
|
|
457
594
|
return { reloaded: false, initialized: false };
|
|
458
595
|
}
|
|
@@ -463,6 +600,7 @@ async function initZipPeek({ workerUrl, scopeUrl, reloadOnFirstInstall = true, c
|
|
|
463
600
|
workerUrl,
|
|
464
601
|
scopeUrl,
|
|
465
602
|
reloadOnFirstInstall,
|
|
603
|
+
reloadOnWorkerUpdate,
|
|
466
604
|
});
|
|
467
605
|
}
|
|
468
606
|
catch (error) {
|
|
@@ -475,6 +613,7 @@ async function initZipPeek({ workerUrl, scopeUrl, reloadOnFirstInstall = true, c
|
|
|
475
613
|
if (reloaded) {
|
|
476
614
|
return { reloaded: true, initialized: false };
|
|
477
615
|
}
|
|
616
|
+
await checkWorkerVersion(onError);
|
|
478
617
|
await postWorkerMessageOrThrow({
|
|
479
618
|
type: "ZIP_SW_CONFIG",
|
|
480
619
|
config: {
|
|
@@ -525,6 +664,7 @@ async function initZipPeek({ workerUrl, scopeUrl, reloadOnFirstInstall = true, c
|
|
|
525
664
|
}
|
|
526
665
|
}
|
|
527
666
|
}
|
|
667
|
+
ensureControllerChangeListener();
|
|
528
668
|
return { reloaded: false, initialized: true };
|
|
529
669
|
}
|
|
530
670
|
catch (error) {
|
|
@@ -540,7 +680,7 @@ async function renewPresignedUrl({ previousUrl, nextUrl }) {
|
|
|
540
680
|
if (!("serviceWorker" in navigator)) {
|
|
541
681
|
// Usually happens in the built in Facebook/Messenger browser.
|
|
542
682
|
alert("Browser not supported. Please try a different browser.");
|
|
543
|
-
throw (0, types_1.createZipPeekError)("Service Worker API not supported in this browser.");
|
|
683
|
+
throw (0, types_1.createZipPeekError)("Service Worker API not supported in this browser.", "error");
|
|
544
684
|
}
|
|
545
685
|
// Prefer the host-provided absolute strings as-is so SigV4 query encoding is
|
|
546
686
|
// preserved; only resolve relative URLs against the page location.
|
|
@@ -551,7 +691,7 @@ async function renewPresignedUrl({ previousUrl, nextUrl }) {
|
|
|
551
691
|
? nextUrl
|
|
552
692
|
: new URL(nextUrl, window.location.href).href;
|
|
553
693
|
if (!(0, zip_utils_1.isZipPackage)(resolvedPrevious) || !(0, zip_utils_1.isZipPackage)(resolvedNext)) {
|
|
554
|
-
throw (0, types_1.createZipPeekError)("renewPresignedUrl requires both previousUrl and nextUrl to be ZIP package URLs.");
|
|
694
|
+
throw (0, types_1.createZipPeekError)("renewPresignedUrl requires both previousUrl and nextUrl to be ZIP package URLs.", "error");
|
|
555
695
|
}
|
|
556
696
|
const previous = (0, zip_utils_1.normalizeZipUrl)(resolvedPrevious);
|
|
557
697
|
const next = (0, zip_utils_1.normalizeZipUrl)(resolvedNext);
|
package/dist/types.d.ts
CHANGED
|
@@ -9,7 +9,8 @@ export type ZipCacheClearingStrategy = "keep-all" | "keep-allowed-urls" | "clear
|
|
|
9
9
|
* client claims** (all tabs under the same service worker scope that have
|
|
10
10
|
* configured `allowedZipUrls`), not only the calling tab's list.
|
|
11
11
|
*/
|
|
12
|
-
export type
|
|
12
|
+
export type ZipPeekLogLevel = "error" | "warn";
|
|
13
|
+
export type ZipPeekErrorHandler = (error: Error, info: string | undefined, level: ZipPeekLogLevel) => void;
|
|
13
14
|
/**
|
|
14
15
|
* Invoked shortly before a tracked presigned ZIP URL expires.
|
|
15
16
|
*/
|
|
@@ -25,6 +26,12 @@ export type InitZipPeekOptions = {
|
|
|
25
26
|
workerUrl: string;
|
|
26
27
|
scopeUrl: string;
|
|
27
28
|
reloadOnFirstInstall?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Reload the page after a newer service worker version activates, either
|
|
31
|
+
* during init (found by `registration.update()`) or later in the session.
|
|
32
|
+
* Defaults to `true`.
|
|
33
|
+
*/
|
|
34
|
+
reloadOnWorkerUpdate?: boolean;
|
|
28
35
|
cacheClearingStrategy?: ZipCacheClearingStrategy;
|
|
29
36
|
allowedZipUrls?: string[];
|
|
30
37
|
requireExactManifestPath?: boolean;
|
|
@@ -51,6 +58,9 @@ export type WorkerMessageResponse = {
|
|
|
51
58
|
ok: boolean;
|
|
52
59
|
error?: string;
|
|
53
60
|
errorStack?: string;
|
|
61
|
+
/** `key=value` diagnostics (`phase`, `zipPath`, `online`, `reason`, `status`, `cause`). */
|
|
62
|
+
info?: string;
|
|
63
|
+
level?: ZipPeekLogLevel;
|
|
54
64
|
};
|
|
55
65
|
export declare const ZIP_PEEK_ERROR_PREFIX = "[zip-peek-error]";
|
|
56
66
|
export declare const NETWORK_ERROR_PREFIX = "[NetworkError]";
|
|
@@ -59,8 +69,13 @@ export declare function formatZipPeekError(message: string): string;
|
|
|
59
69
|
export declare function zipErrorInfo(fields: Record<string, string | number | boolean | null | undefined>): string;
|
|
60
70
|
export type ZipPeekError = Error & {
|
|
61
71
|
zipPeekInfo?: string;
|
|
72
|
+
/** HTTP status to return for this failure. Unset means the fetch-handler default (500). */
|
|
73
|
+
httpStatus?: number;
|
|
74
|
+
/** Origin status when the response status was remapped (for example 200 → 502). */
|
|
75
|
+
upstreamStatus?: number;
|
|
76
|
+
zipPeekLevel: ZipPeekLogLevel;
|
|
62
77
|
};
|
|
63
|
-
export declare function createZipPeekError(message: string, info?: string):
|
|
78
|
+
export declare function createZipPeekError(message: string, level: ZipPeekLogLevel, info?: string): ZipPeekError;
|
|
64
79
|
export declare function toZipPeekError(error: unknown): ZipPeekError;
|
|
65
80
|
export declare function zipPeekErrorInfo(error: unknown): string | undefined;
|
|
66
81
|
export declare function errorMessage(error: unknown): string;
|
package/dist/types.js
CHANGED
|
@@ -26,8 +26,9 @@ function zipErrorInfo(fields) {
|
|
|
26
26
|
.map(([key, value]) => `${key}=${String(value)}`)
|
|
27
27
|
.join(" ");
|
|
28
28
|
}
|
|
29
|
-
function createZipPeekError(message, info) {
|
|
29
|
+
function createZipPeekError(message, level, info) {
|
|
30
30
|
const err = new Error(formatZipPeekError(message));
|
|
31
|
+
err.zipPeekLevel = level;
|
|
31
32
|
if (info) {
|
|
32
33
|
err.zipPeekInfo = info;
|
|
33
34
|
}
|
|
@@ -38,6 +39,9 @@ function toZipPeekError(error) {
|
|
|
38
39
|
if (!err.message.startsWith(exports.ZIP_PEEK_ERROR_PREFIX)) {
|
|
39
40
|
err.message = formatZipPeekError(err.message);
|
|
40
41
|
}
|
|
42
|
+
if (!err.zipPeekLevel) {
|
|
43
|
+
err.zipPeekLevel = "error";
|
|
44
|
+
}
|
|
41
45
|
return err;
|
|
42
46
|
}
|
|
43
47
|
function zipPeekErrorInfo(error) {
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ZIP_PEEK_VERSION = void 0;
|
|
4
|
+
/** Package version. The page reads the installed package; the worker build inlines this number. */
|
|
5
|
+
exports.ZIP_PEEK_VERSION = require("../package.json").version;
|