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 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?: string) => void;
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
- console.log("Zip service worker already registered; skipping register().");
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
- window.location.reload();
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.zipPeekInfo = (0, types_1.formatZipPeekError)(info);
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 ZipPeekErrorHandler = (error: Error, info?: string) => void;
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): Error;
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) {
@@ -0,0 +1,2 @@
1
+ /** Package version. The page reads the installed package; the worker build inlines this number. */
2
+ export declare const ZIP_PEEK_VERSION: string;
@@ -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;