@broberg/pwa 0.2.2 → 0.4.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 +38 -0
- package/dist/index.cjs +63 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +74 -1
- package/dist/index.d.ts +74 -1
- package/dist/index.js +63 -14
- package/dist/index.js.map +1 -1
- package/dist/preact.cjs +89 -22
- package/dist/preact.cjs.map +1 -1
- package/dist/preact.d.cts +18 -0
- package/dist/preact.d.ts +18 -0
- package/dist/preact.js +89 -22
- package/dist/preact.js.map +1 -1
- package/dist/react.cjs +89 -22
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +18 -0
- package/dist/react.d.ts +18 -0
- package/dist/react.js +89 -22
- package/dist/react.js.map +1 -1
- package/package.json +4 -3
package/dist/preact.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/message.ts","../src/index.ts","../src/preact.tsx"],"names":[],"mappings":"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","file":"preact.js","sourcesContent":["/**\n * The single source of the client↔service-worker \"activate the waiting worker\"\n * message. Both the client controller (`applyUpdate`) and the SW helper\n * (`listenForSkipWaiting`) import it so the contract can never drift.\n */\nexport const SKIP_WAITING = \"SKIP_WAITING\" as const;\n\nexport const SKIP_WAITING_MESSAGE = { type: SKIP_WAITING } as const;\n\nexport type SkipWaitingMessage = typeof SKIP_WAITING_MESSAGE;\n","import { SKIP_WAITING_MESSAGE } from \"./message.js\";\n\nexport { SKIP_WAITING, SKIP_WAITING_MESSAGE } from \"./message.js\";\nexport type { SkipWaitingMessage } from \"./message.js\";\n\nexport interface PwaUpdaterOptions {\n /** Service-worker script URL to register. Default `/sw.js`. */\n swUrl?: string;\n /**\n * Register `swUrl` ourselves (manual `sw.js` style). Set `false` when the\n * app already registers the SW (e.g. Serwist's `registerSW` / `@serwist/next`)\n * — the updater then attaches to the existing registration via\n * `navigator.serviceWorker.ready` instead of registering again. Default `true`.\n */\n register?: boolean;\n /** How often to poll `registration.update()` (ms). `0` disables. Default 60 min. */\n pollIntervalMs?: number;\n /**\n * Also check for an update when the tab regains focus / becomes visible.\n * A constantly-focused tab never fires `visibilitychange`, so `focus` is the\n * one that catches \"user came back to the app\". Default `true`.\n */\n updateOnFocus?: boolean;\n /** Reload the page once the new worker takes control. Default `true`. */\n reloadOnControllerChange?: boolean;\n /**\n * Consumer guard. When `true` the updater is an inert no-op (registers\n * nothing). Pass your own policy, e.g. `disabled: isNativeCapacitor || isDev`\n * — the package never hardcodes an environment or a `.native` check.\n */\n disabled?: boolean;\n}\n\nexport interface PwaUpdaterState {\n /** A new service worker is installed and waiting to activate. */\n updateReady: boolean;\n}\n\nexport interface PwaUpdater {\n /** Subscribe to state changes. Returns an unsubscribe fn. */\n subscribe(listener: (state: PwaUpdaterState) => void): () => void;\n getState(): PwaUpdaterState;\n /** Tell the waiting worker to activate (posts SKIP_WAITING). No-op if none waits. */\n applyUpdate(): void;\n /** Stop polling + remove all listeners. */\n destroy(): void;\n}\n\nconst DEFAULT_POLL_MS = 60 * 60 * 1000;\n\n/**\n * Framework- and bundler-agnostic controller for the PWA \"new version\n * available\" lifecycle: register (or attach) → detect a waiting worker\n * (suppressing the first install) → let the consumer activate it → reload on\n * takeover, re-checking on interval + focus/visibility.\n *\n * Distilled from the pattern hand-rolled across the fleet (fds, cardmem,\n * pitch-vault). Works with any service worker (Serwist, Workbox, hand-rolled)\n * as long as the SW answers a `SKIP_WAITING` message — see `@broberg/pwa/sw`.\n */\nexport function createPwaUpdater(options: PwaUpdaterOptions = {}): PwaUpdater {\n const {\n swUrl = \"/sw.js\",\n register = true,\n pollIntervalMs = DEFAULT_POLL_MS,\n updateOnFocus = true,\n reloadOnControllerChange = true,\n disabled = false,\n } = options;\n\n const listeners = new Set<(state: PwaUpdaterState) => void>();\n let updateReady = false;\n let waitingWorker: ServiceWorker | null = null;\n\n const getState = (): PwaUpdaterState => ({ updateReady });\n const emit = (): void => {\n const state = getState();\n for (const listener of listeners) listener(state);\n };\n const subscribe = (listener: (state: PwaUpdaterState) => void): (() => void) => {\n listeners.add(listener);\n return () => listeners.delete(listener);\n };\n\n const supported =\n typeof navigator !== \"undefined\" && \"serviceWorker\" in navigator;\n\n // Inert no-op: disabled by the consumer, or no service-worker support at all.\n if (disabled || !supported) {\n return {\n subscribe,\n getState,\n applyUpdate() {},\n destroy() {\n listeners.clear();\n },\n };\n }\n\n const container = navigator.serviceWorker;\n let pollId: ReturnType<typeof setInterval> | null = null;\n let removeFocusListeners: (() => void) | null = null;\n let reloading = false;\n let destroyed = false;\n\n const markReady = (worker: ServiceWorker): void => {\n waitingWorker = worker;\n if (!updateReady) {\n updateReady = true;\n emit();\n }\n };\n\n // With `clientsClaim: true` a brand-new worker claims a previously\n // uncontrolled page and fires `controllerchange` on the FIRST install — that\n // is not an update, so reloading there would yank a first-time visitor out of\n // whatever they are doing. Only a `controllerchange` that REPLACES an existing\n // controller is a real takeover worth reloading for.\n let hadController = !!container.controller;\n const onControllerChange = (): void => {\n if (!reloadOnControllerChange || reloading) return;\n if (!hadController) {\n hadController = true; // first claim of an uncontrolled page — not an update\n return;\n }\n reloading = true; // guard against a reload-loop\n if (typeof location !== \"undefined\") location.reload();\n };\n container.addEventListener(\"controllerchange\", onControllerChange);\n\n const watchInstalling = (registration: ServiceWorkerRegistration): void => {\n const installing = registration.installing;\n if (!installing) return;\n installing.addEventListener(\"statechange\", () => {\n // A new worker reaching `installed` WHILE a controller already exists is\n // an update. Reaching `installed` with no controller is the FIRST install\n // — nothing to update, so we stay quiet.\n if (installing.state === \"installed\" && container.controller) {\n markReady(installing);\n }\n });\n };\n\n const wireFocusChecks = (registration: ServiceWorkerRegistration): void => {\n if (!updateOnFocus) return;\n const check = (): void => {\n registration.update().catch(() => {});\n };\n const onFocus = (): void => check();\n const onVisibility = (): void => {\n if (typeof document !== \"undefined\" && document.visibilityState === \"visible\") {\n check();\n }\n };\n if (typeof window !== \"undefined\") window.addEventListener(\"focus\", onFocus);\n if (typeof document !== \"undefined\") {\n document.addEventListener(\"visibilitychange\", onVisibility);\n }\n removeFocusListeners = () => {\n if (typeof window !== \"undefined\") window.removeEventListener(\"focus\", onFocus);\n if (typeof document !== \"undefined\") {\n document.removeEventListener(\"visibilitychange\", onVisibility);\n }\n };\n };\n\n // Manual style registers swUrl; serwist/registerSW style attaches to the\n // registration the app already made via `.ready`.\n const registrationPromise = register ? container.register(swUrl) : container.ready;\n\n registrationPromise\n .then((registration) => {\n if (destroyed) return;\n if (registration.waiting) markReady(registration.waiting);\n registration.addEventListener(\"updatefound\", () => watchInstalling(registration));\n if (pollIntervalMs > 0) {\n pollId = setInterval(() => {\n registration.update().catch(() => {\n // ignore transient update-check network errors\n });\n }, pollIntervalMs);\n }\n wireFocusChecks(registration);\n })\n .catch(() => {\n // registration failed — stay inert rather than throw into the app\n });\n\n return {\n subscribe,\n getState,\n applyUpdate() {\n if (waitingWorker) waitingWorker.postMessage(SKIP_WAITING_MESSAGE);\n },\n destroy() {\n destroyed = true;\n if (pollId !== null) clearInterval(pollId);\n removeFocusListeners?.();\n container.removeEventListener(\"controllerchange\", onControllerChange);\n listeners.clear();\n },\n };\n}\n","/** @jsxImportSource preact */\n\nimport { useEffect, useRef, useState } from \"preact/hooks\";\nimport { createPwaUpdater, type PwaUpdater, type PwaUpdaterOptions } from \"./index.js\";\n\n/**\n * Preact binding for {@link createPwaUpdater} — API-identical to the React\n * adapter, for the Stack B (Bun/Hono/Preact/Vite) apps. Returns whether an\n * update is ready plus the `applyUpdate` action; the controller is created\n * once and torn down on unmount.\n */\nexport function usePwaUpdate(options: PwaUpdaterOptions = {}): {\n updateReady: boolean;\n applyUpdate: () => void;\n} {\n const { swUrl, pollIntervalMs, reloadOnControllerChange, disabled } = options;\n const [updateReady, setUpdateReady] = useState(false);\n const updaterRef = useRef<PwaUpdater | null>(null);\n\n useEffect(() => {\n const updater = createPwaUpdater({\n swUrl,\n pollIntervalMs,\n reloadOnControllerChange,\n disabled,\n });\n updaterRef.current = updater;\n setUpdateReady(updater.getState().updateReady);\n const unsubscribe = updater.subscribe((state) => setUpdateReady(state.updateReady));\n return () => {\n unsubscribe();\n updater.destroy();\n updaterRef.current = null;\n };\n }, [swUrl, pollIntervalMs, reloadOnControllerChange, disabled]);\n\n return {\n updateReady,\n applyUpdate: () => updaterRef.current?.applyUpdate(),\n };\n}\n\nexport interface PwaUpdateBannerLabels {\n title?: string;\n body?: string;\n update?: string;\n dismiss?: string;\n close?: string;\n}\n\nconst DEFAULT_LABELS: Required<PwaUpdateBannerLabels> = {\n title: \"New version available\",\n body: \"Reload to get the latest version.\",\n update: \"Update\",\n dismiss: \"Later\",\n close: \"Close\",\n};\n\nexport interface PwaUpdateBannerProps {\n /** From `usePwaUpdate().updateReady`. Nothing renders when false. */\n updateReady: boolean;\n /** From `usePwaUpdate().applyUpdate`. */\n onUpdate: () => void;\n /** Optional dismiss handler (wired to both \"Later\" and the close ✕). */\n onDismiss?: () => void;\n /** Override any of the built-in strings (i18n). */\n labels?: PwaUpdateBannerLabels;\n /** Your class(es) — the skeleton carries NO colours or design-system styling. */\n className?: string;\n}\n\n/**\n * Unstyled, accessible \"new version available\" banner skeleton for Preact.\n * Structure + a11y + stable testids only; you own the look via `className`\n * and your design tokens. Prefer {@link usePwaUpdate} directly if you'd rather\n * build the UI yourself.\n */\nexport function PwaUpdateBanner({\n updateReady,\n onUpdate,\n onDismiss,\n labels,\n className,\n}: PwaUpdateBannerProps) {\n if (!updateReady) return null;\n const l = { ...DEFAULT_LABELS, ...labels };\n const dismiss = () => onDismiss?.();\n return (\n <div role=\"status\" aria-live=\"polite\" data-testid=\"pwa-update-banner\" className={className}>\n <p data-testid=\"pwa-update-title\">{l.title}</p>\n {l.body ? <p data-testid=\"pwa-update-body\">{l.body}</p> : null}\n <button type=\"button\" data-testid=\"pwa-update-confirm\" onClick={onUpdate}>\n {l.update}\n </button>\n <button type=\"button\" data-testid=\"pwa-update-dismiss\" onClick={dismiss}>\n {l.dismiss}\n </button>\n <button\n type=\"button\"\n data-testid=\"pwa-update-close\"\n aria-label={l.close}\n onClick={dismiss}\n >\n {\"×\"}\n </button>\n </div>\n );\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/message.ts","../src/should-offer.ts","../src/index.ts","../src/options-key.ts","../src/preact.tsx"],"names":[],"mappings":"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","file":"preact.js","sourcesContent":["/**\n * The single source of the client↔service-worker \"activate the waiting worker\"\n * message. Both the client controller (`applyUpdate`) and the SW helper\n * (`listenForSkipWaiting`) import it so the contract can never drift.\n */\nexport const SKIP_WAITING = \"SKIP_WAITING\" as const;\n\nexport const SKIP_WAITING_MESSAGE = { type: SKIP_WAITING } as const;\n\nexport type SkipWaitingMessage = typeof SKIP_WAITING_MESSAGE;\n","/**\n * F054.8 — the decision, as a pure function.\n *\n * Split out so it can be proven with NO service worker, NO DOM and no fake\n * container: every branch is reachable from three plain values. The rest of the\n * updater is plumbing around this.\n *\n * Why it is a function and not a flag: the old updater REMEMBERED whether an\n * update was ready instead of READING whether one is waiting, so the answer\n * could only ever be given once.\n */\nexport interface ShouldOfferInput {\n /** `registration.waiting` — read fresh at the moment of the decision. */\n waiting: unknown;\n /** Epoch ms until which the user has snoozed the offer, or null. */\n snoozedUntil: number | null;\n /** Epoch ms now. Passed in so the function stays pure and testable. */\n now: number;\n}\n\n/**\n * Should the consumer be offered the update right now?\n *\n * WHY `controller` IS NOT A TERM HERE, though the sketch we were shown had one\n * and the old code checked it on one of its two paths.\n *\n * The old updater answered this question inconsistently: the attach path\n * (`if (registration.waiting)`) offered with no controller check, while the\n * `updatefound` path required `container.controller` to suppress a first\n * install. Same situation, two answers, depending on which path happened to see\n * it. Making them agree is part of this fix — the question is which way.\n *\n * Requiring a controller would be the tighter rule, and it would REMOVE a banner\n * that some consumer sees today: a page that is not yet controlled (no\n * `clientsClaim`, or a hard reload) but has a genuine update waiting. With\n * fd-sundhed live to 16,838 people on the current version, a change in that\n * direction is not one to make on an unverified reading.\n *\n * And the reading in question is this: a worker only enters `waiting` when\n * another worker is already ACTIVE — on a first install there is nothing to wait\n * behind, so it activates directly. If that is right, `registration.waiting`\n * being non-null already implies this is not a first install, and the controller\n * term is redundant rather than protective. **I have not verified that against a\n * real service worker.** So the old, more permissive behaviour stays as the\n * floor, and first-install suppression rides on the same fact rather than on a\n * second guard that could disagree with it.\n *\n * If someone later measures that a first-install worker CAN sit in `waiting`,\n * this is the function to add the term to — and the test to write first is the\n * one that fails today.\n */\nexport function shouldOfferUpdate({ waiting, snoozedUntil, now }: ShouldOfferInput): boolean {\n if (!waiting) return false;\n if (snoozedUntil !== null && snoozedUntil > now) return false;\n return true;\n}\n","import { SKIP_WAITING_MESSAGE } from \"./message.js\";\nimport { shouldOfferUpdate } from \"./should-offer.js\";\n\nexport { SKIP_WAITING, SKIP_WAITING_MESSAGE } from \"./message.js\";\nexport { shouldOfferUpdate } from \"./should-offer.js\";\nexport type { ShouldOfferInput } from \"./should-offer.js\";\nexport type { SkipWaitingMessage } from \"./message.js\";\n\nexport interface PwaUpdaterOptions {\n /** Service-worker script URL to register. Default `/sw.js`. */\n swUrl?: string;\n /**\n * Register `swUrl` ourselves (manual `sw.js` style). Set `false` when the\n * app already registers the SW (e.g. Serwist's `registerSW` / `@serwist/next`)\n * — the updater then attaches to the existing registration via\n * `navigator.serviceWorker.ready` instead of registering again. Default `true`.\n */\n register?: boolean;\n /** How often to poll `registration.update()` (ms). `0` disables. Default 60 min. */\n pollIntervalMs?: number;\n /**\n * Also check for an update when the tab regains focus / becomes visible.\n * A constantly-focused tab never fires `visibilitychange`, so `focus` is the\n * one that catches \"user came back to the app\". Default `true`.\n */\n updateOnFocus?: boolean;\n /** Reload the page once the new worker takes control. Default `true`. */\n reloadOnControllerChange?: boolean;\n /**\n * How long `snooze()` holds the offer down (ms). Default 30 min.\n *\n * F054.8 — \"Later\" is a SNOOZE, never a mute. The defect this replaced was a\n * banner that got exactly one chance to be seen; a permanent dismiss is that\n * defect made official.\n */\n snoozeMs?: number;\n /**\n * Where a snooze is remembered, so it survives the reload the banner is\n * asking for. Defaults to `localStorage` when available; pass `null` to keep\n * the snooze in memory only.\n */\n snoozeStorage?: Pick<Storage, \"getItem\" | \"setItem\" | \"removeItem\"> | null;\n /**\n * Consumer guard. When `true` the updater is an inert no-op (registers\n * nothing). Pass your own policy, e.g. `disabled: isNativeCapacitor || isDev`\n * — the package never hardcodes an environment or a `.native` check.\n */\n disabled?: boolean;\n}\n\nexport interface PwaUpdaterState {\n /** A new service worker is installed and waiting to activate. */\n updateReady: boolean;\n}\n\nexport interface PwaUpdater {\n /** Subscribe to state changes. Returns an unsubscribe fn. */\n subscribe(listener: (state: PwaUpdaterState) => void): () => void;\n getState(): PwaUpdaterState;\n /** Tell the waiting worker to activate (posts SKIP_WAITING). No-op if none waits. */\n applyUpdate(): void;\n /**\n * \"Later\". Holds the offer down for `snoozeMs`, then it comes back on the\n * next tick — this is NOT a mute, and it deliberately cannot be made one.\n */\n snooze(): void;\n /** Re-read `registration.waiting` right now and emit if the answer moved. */\n check(): void;\n /** Stop polling + remove all listeners. */\n destroy(): void;\n}\n\nconst DEFAULT_POLL_MS = 60 * 60 * 1000;\nconst DEFAULT_SNOOZE_MS = 30 * 60 * 1000;\nconst SNOOZE_KEY = \"broberg-pwa:snoozed-until\";\n\n/**\n * Framework- and bundler-agnostic controller for the PWA \"new version\n * available\" lifecycle: register (or attach) → detect a waiting worker\n * (suppressing the first install) → let the consumer activate it → reload on\n * takeover, re-checking on interval + focus/visibility.\n *\n * Distilled from the pattern hand-rolled across the fleet (fds, cardmem,\n * pitch-vault). Works with any service worker (Serwist, Workbox, hand-rolled)\n * as long as the SW answers a `SKIP_WAITING` message — see `@broberg/pwa/sw`.\n */\nexport function createPwaUpdater(options: PwaUpdaterOptions = {}): PwaUpdater {\n const {\n swUrl = \"/sw.js\",\n register = true,\n pollIntervalMs = DEFAULT_POLL_MS,\n updateOnFocus = true,\n reloadOnControllerChange = true,\n snoozeMs = DEFAULT_SNOOZE_MS,\n disabled = false,\n } = options;\n\n const listeners = new Set<(state: PwaUpdaterState) => void>();\n let updateReady = false;\n let waitingWorker: ServiceWorker | null = null;\n\n const getState = (): PwaUpdaterState => ({ updateReady });\n const emit = (): void => {\n const state = getState();\n for (const listener of listeners) listener(state);\n };\n const subscribe = (listener: (state: PwaUpdaterState) => void): (() => void) => {\n listeners.add(listener);\n return () => listeners.delete(listener);\n };\n\n const supported =\n typeof navigator !== \"undefined\" && \"serviceWorker\" in navigator;\n\n // Inert no-op: disabled by the consumer, or no service-worker support at all.\n if (disabled || !supported) {\n return {\n subscribe,\n getState,\n applyUpdate() {},\n snooze() {},\n check() {},\n destroy() {\n listeners.clear();\n },\n };\n }\n\n const container = navigator.serviceWorker;\n let pollId: ReturnType<typeof setInterval> | null = null;\n let removeFocusListeners: (() => void) | null = null;\n let reloading = false;\n let destroyed = false;\n\n // F054.8 — READ the answer, never remember it.\n //\n // This used to be markReady(worker), the only writer of `updateReady`, and it\n // was guarded by `if (!updateReady)` — so `emit()` fired AT MOST ONCE for the\n // life of the updater. Both adapters drive their state from subscribe(), so a\n // consumer who dismissed the banner had no channel that could ever raise it\n // again. cardmem measured the consequence in production: a client sat on an\n // old bundle indefinitely, and the only symptom anyone had was a person\n // saying a feature was missing.\n //\n // `registration` is captured once resolved so every tick can re-read\n // `.waiting` rather than wait to be told.\n let registrationRef: ServiceWorkerRegistration | null = null;\n\n const storage: PwaUpdaterOptions[\"snoozeStorage\"] =\n options.snoozeStorage !== undefined\n ? options.snoozeStorage\n : typeof localStorage !== \"undefined\"\n ? localStorage\n : null;\n\n let snoozedUntil: number | null = null;\n const readSnooze = (): number | null => {\n if (!storage) return snoozedUntil;\n try {\n const raw = storage.getItem(SNOOZE_KEY);\n if (!raw) return null;\n const n = Number(raw);\n return Number.isFinite(n) ? n : null;\n } catch {\n // A browser with site data blocked THROWS on read. That is not a reason to\n // stop offering updates — fall back to the in-memory value.\n return snoozedUntil;\n }\n };\n const writeSnooze = (until: number | null): void => {\n snoozedUntil = until;\n if (!storage) return;\n try {\n if (until === null) storage.removeItem(SNOOZE_KEY);\n else storage.setItem(SNOOZE_KEY, String(until));\n } catch {\n // in-memory only for this tab; already recorded above\n }\n };\n\n /**\n * Re-derive from what is true RIGHT NOW and emit only when the answer moved.\n * Emits in BOTH directions: false→true raises the banner, true→false takes it\n * down when the worker is gone or the user snoozed.\n */\n const check = (): void => {\n const waiting = registrationRef?.waiting ?? null;\n waitingWorker = waiting;\n const next = shouldOfferUpdate({\n waiting,\n snoozedUntil: readSnooze(),\n now: Date.now(),\n });\n if (next !== updateReady) {\n updateReady = next;\n emit();\n }\n };\n\n // With `clientsClaim: true` a brand-new worker claims a previously\n // uncontrolled page and fires `controllerchange` on the FIRST install — that\n // is not an update, so reloading there would yank a first-time visitor out of\n // whatever they are doing. Only a `controllerchange` that REPLACES an existing\n // controller is a real takeover worth reloading for.\n let hadController = !!container.controller;\n const onControllerChange = (): void => {\n if (!reloadOnControllerChange || reloading) return;\n if (!hadController) {\n hadController = true; // first claim of an uncontrolled page — not an update\n return;\n }\n reloading = true; // guard against a reload-loop\n if (typeof location !== \"undefined\") location.reload();\n };\n container.addEventListener(\"controllerchange\", onControllerChange);\n\n const watchInstalling = (registration: ServiceWorkerRegistration): void => {\n const installing = registration.installing;\n if (!installing) return;\n installing.addEventListener(\"statechange\", () => {\n // A new worker reaching `installed` WHILE a controller already exists is\n // an update. Reaching `installed` with no controller is the FIRST install\n // — nothing to update, so we stay quiet.\n if (installing.state === \"installed\") check();\n });\n };\n\n const wireFocusChecks = (registration: ServiceWorkerRegistration): void => {\n if (!updateOnFocus) return;\n // update() asks the SERVER for a new worker; check() reads whether one is\n // already WAITING. The second is the half that was missing — reg.update()\n // resolves without firing updatefound when a worker is already waiting, so\n // a tab that missed the original event could never learn about it.\n const tick = (): void => {\n check();\n registration.update().catch(() => {}).then(check, check);\n };\n const onFocus = (): void => tick();\n const onVisibility = (): void => {\n if (typeof document !== \"undefined\" && document.visibilityState === \"visible\") {\n tick();\n }\n };\n if (typeof window !== \"undefined\") window.addEventListener(\"focus\", onFocus);\n if (typeof document !== \"undefined\") {\n document.addEventListener(\"visibilitychange\", onVisibility);\n }\n removeFocusListeners = () => {\n if (typeof window !== \"undefined\") window.removeEventListener(\"focus\", onFocus);\n if (typeof document !== \"undefined\") {\n document.removeEventListener(\"visibilitychange\", onVisibility);\n }\n };\n };\n\n // Manual style registers swUrl; serwist/registerSW style attaches to the\n // registration the app already made via `.ready`.\n const registrationPromise = register ? container.register(swUrl) : container.ready;\n\n registrationPromise\n .then((registration) => {\n if (destroyed) return;\n registrationRef = registration;\n check();\n registration.addEventListener(\"updatefound\", () => watchInstalling(registration));\n if (pollIntervalMs > 0) {\n pollId = setInterval(() => {\n check();\n registration\n .update()\n .catch(() => {\n // ignore transient update-check network errors\n })\n .then(check, check);\n }, pollIntervalMs);\n }\n wireFocusChecks(registration);\n })\n .catch(() => {\n // registration failed — stay inert rather than throw into the app\n });\n\n return {\n subscribe,\n getState,\n applyUpdate() {\n if (waitingWorker) waitingWorker.postMessage(SKIP_WAITING_MESSAGE);\n },\n snooze() {\n writeSnooze(Date.now() + snoozeMs);\n check();\n },\n check,\n destroy() {\n destroyed = true;\n if (pollId !== null) clearInterval(pollId);\n removeFocusListeners?.();\n container.removeEventListener(\"controllerchange\", onControllerChange);\n listeners.clear();\n },\n };\n}\n","import type { PwaUpdaterOptions } from \"./index.js\";\n\n/**\n * A stable identity for an options object, for an adapter's effect dependency.\n *\n * F054.7 — WHY THIS EXISTS INSTEAD OF A DEPENDENCY LIST. Both adapters used to\n * destructure four named options and list those same four as their effect deps.\n * That is a second copy of the core's option list, and it had already drifted:\n * `register` and `updateOnFocus` were never forwarded, so a consumer writing\n * `register: false` got a service-worker registration anyway, with no error and\n * no warning. fd-sundhed found it by reading our shipped dist, which is not a\n * detector anyone should need.\n *\n * Deriving the key from the options the CALLER passed means a new core option\n * reaches both adapters with neither adapter edited. The list was the defect —\n * not the two names missing from it.\n *\n * Keys are SORTED so `{a, b}` and `{b, a}` are the same identity, and\n * `undefined` values are dropped so an explicitly-omitted option and an absent\n * one do not look like different configurations.\n *\n * THE ONE BOUNDARY, stated because the claim above is otherwise broader than\n * what is true: the identity is JSON, so a value JSON cannot express collapses.\n * Measured — `{onError: () => 1}` and `{onError: () => 2}` produce the byte-\n * identical key `[[\"onError\",null]]`, so a caller swapping the callback would\n * keep the first updater and never see the new one take effect. Every option\n * the core has today is a string, number or boolean, so nothing is affected;\n * this is here for whoever adds the first FUNCTION option (an `onUpdateReady`\n * is the obvious candidate). That option must be given its own identity here,\n * not merely added to the core.\n */\n/**\n * F054.8 — the boundary above is no longer hypothetical: `snoozeStorage` is an\n * object of functions, and `JSON.stringify` renders it `{}`. Two different\n * storages would produce the byte-identical key, so a caller swapping one would\n * keep the first updater and never see the new one — the exact collapse the\n * paragraph above predicted, arriving one option later.\n *\n * So a value JSON cannot express gets a stable identity of its own instead: the\n * same object gives the same tag for the life of the page, a different object a\n * different tag. Kept in a WeakMap so holding the tag cannot hold the object.\n */\nconst identities = new WeakMap<object, string>();\nlet nextIdentity = 0;\nconst identify = (value: unknown): unknown => {\n if (value === null || (typeof value !== \"object\" && typeof value !== \"function\")) {\n return value;\n }\n const obj = value as object;\n let tag = identities.get(obj);\n if (tag === undefined) {\n tag = `@ref:${nextIdentity++}`;\n identities.set(obj, tag);\n }\n return tag;\n};\n\nexport function optionsKey(options: PwaUpdaterOptions): string {\n return JSON.stringify(\n Object.entries(options)\n .filter(([, v]) => v !== undefined)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n .map(([k, v]) => [k, identify(v)]),\n );\n}\n","/** @jsxImportSource preact */\n\nimport { useEffect, useRef, useState } from \"preact/hooks\";\nimport { createPwaUpdater, type PwaUpdater, type PwaUpdaterOptions } from \"./index.js\";\nimport { optionsKey } from \"./options-key.js\";\n\n/**\n * Preact binding for {@link createPwaUpdater} — API-identical to the React\n * adapter, for the Stack B (Bun/Hono/Preact/Vite) apps. Returns whether an\n * update is ready plus the `applyUpdate` action; the controller is created\n * once and torn down on unmount.\n */\nexport function usePwaUpdate(options: PwaUpdaterOptions = {}): {\n updateReady: boolean;\n applyUpdate: () => void;\n /** \"Later\" — F054.8. Holds the offer down for `snoozeMs`, then it returns. */\n snooze: () => void;\n /** Re-read `registration.waiting` now. Rarely needed; the ticks do it. */\n check: () => void;\n} {\n // The effect's identity comes from the options the CALLER passed, so a new\n // core option needs no edit here. eslint's exhaustive-deps cannot see\n // through `key`; that is the trade, and it is the right way round.\n const key = optionsKey(options);\n const [updateReady, setUpdateReady] = useState(false);\n const updaterRef = useRef<PwaUpdater | null>(null);\n\n useEffect(() => {\n // THE WHOLE OBJECT, not a hand-picked subset. See options-key.ts: the\n // subset is what silently dropped `register` and `updateOnFocus`.\n const updater = createPwaUpdater(options);\n updaterRef.current = updater;\n setUpdateReady(updater.getState().updateReady);\n const unsubscribe = updater.subscribe((state) => setUpdateReady(state.updateReady));\n return () => {\n unsubscribe();\n updater.destroy();\n updaterRef.current = null;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [key]);\n\n return {\n updateReady,\n applyUpdate: () => updaterRef.current?.applyUpdate(),\n snooze: () => updaterRef.current?.snooze(),\n check: () => updaterRef.current?.check(),\n };\n}\n\nexport interface PwaUpdateBannerLabels {\n title?: string;\n body?: string;\n update?: string;\n dismiss?: string;\n close?: string;\n}\n\nconst DEFAULT_LABELS: Required<PwaUpdateBannerLabels> = {\n title: \"New version available\",\n body: \"Reload to get the latest version.\",\n update: \"Update\",\n dismiss: \"Later\",\n close: \"Close\",\n};\n\nexport interface PwaUpdateBannerProps {\n /** From `usePwaUpdate().updateReady`. Nothing renders when false. */\n updateReady: boolean;\n /** From `usePwaUpdate().applyUpdate`. */\n onUpdate: () => void;\n /** Optional dismiss handler (wired to both \"Later\" and the close ✕). */\n onDismiss?: () => void;\n /** Override any of the built-in strings (i18n). */\n labels?: PwaUpdateBannerLabels;\n /** Your class(es) — the skeleton carries NO colours or design-system styling. */\n className?: string;\n}\n\n/**\n * Unstyled, accessible \"new version available\" banner skeleton for Preact.\n * Structure + a11y + stable testids only; you own the look via `className`\n * and your design tokens. Prefer {@link usePwaUpdate} directly if you'd rather\n * build the UI yourself.\n */\nexport function PwaUpdateBanner({\n updateReady,\n onUpdate,\n onDismiss,\n labels,\n className,\n}: PwaUpdateBannerProps) {\n if (!updateReady) return null;\n const l = { ...DEFAULT_LABELS, ...labels };\n const dismiss = () => onDismiss?.();\n return (\n <div role=\"status\" aria-live=\"polite\" data-testid=\"pwa-update-banner\" className={className}>\n <p data-testid=\"pwa-update-title\">{l.title}</p>\n {l.body ? <p data-testid=\"pwa-update-body\">{l.body}</p> : null}\n <button type=\"button\" data-testid=\"pwa-update-confirm\" onClick={onUpdate}>\n {l.update}\n </button>\n <button type=\"button\" data-testid=\"pwa-update-dismiss\" onClick={dismiss}>\n {l.dismiss}\n </button>\n <button\n type=\"button\"\n data-testid=\"pwa-update-close\"\n aria-label={l.close}\n onClick={dismiss}\n >\n {\"×\"}\n </button>\n </div>\n );\n}\n"]}
|
package/dist/react.cjs
CHANGED
|
@@ -9,8 +9,17 @@ var jsxRuntime = require('react/jsx-runtime');
|
|
|
9
9
|
var SKIP_WAITING = "SKIP_WAITING";
|
|
10
10
|
var SKIP_WAITING_MESSAGE = { type: SKIP_WAITING };
|
|
11
11
|
|
|
12
|
+
// src/should-offer.ts
|
|
13
|
+
function shouldOfferUpdate({ waiting, snoozedUntil, now }) {
|
|
14
|
+
if (!waiting) return false;
|
|
15
|
+
if (snoozedUntil !== null && snoozedUntil > now) return false;
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
|
|
12
19
|
// src/index.ts
|
|
13
20
|
var DEFAULT_POLL_MS = 60 * 60 * 1e3;
|
|
21
|
+
var DEFAULT_SNOOZE_MS = 30 * 60 * 1e3;
|
|
22
|
+
var SNOOZE_KEY = "broberg-pwa:snoozed-until";
|
|
14
23
|
function createPwaUpdater(options = {}) {
|
|
15
24
|
const {
|
|
16
25
|
swUrl = "/sw.js",
|
|
@@ -18,6 +27,7 @@ function createPwaUpdater(options = {}) {
|
|
|
18
27
|
pollIntervalMs = DEFAULT_POLL_MS,
|
|
19
28
|
updateOnFocus = true,
|
|
20
29
|
reloadOnControllerChange = true,
|
|
30
|
+
snoozeMs = DEFAULT_SNOOZE_MS,
|
|
21
31
|
disabled = false
|
|
22
32
|
} = options;
|
|
23
33
|
const listeners = /* @__PURE__ */ new Set();
|
|
@@ -39,6 +49,10 @@ function createPwaUpdater(options = {}) {
|
|
|
39
49
|
getState,
|
|
40
50
|
applyUpdate() {
|
|
41
51
|
},
|
|
52
|
+
snooze() {
|
|
53
|
+
},
|
|
54
|
+
check() {
|
|
55
|
+
},
|
|
42
56
|
destroy() {
|
|
43
57
|
listeners.clear();
|
|
44
58
|
}
|
|
@@ -49,10 +63,39 @@ function createPwaUpdater(options = {}) {
|
|
|
49
63
|
let removeFocusListeners = null;
|
|
50
64
|
let reloading = false;
|
|
51
65
|
let destroyed = false;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
66
|
+
let registrationRef = null;
|
|
67
|
+
const storage = options.snoozeStorage !== void 0 ? options.snoozeStorage : typeof localStorage !== "undefined" ? localStorage : null;
|
|
68
|
+
let snoozedUntil = null;
|
|
69
|
+
const readSnooze = () => {
|
|
70
|
+
if (!storage) return snoozedUntil;
|
|
71
|
+
try {
|
|
72
|
+
const raw = storage.getItem(SNOOZE_KEY);
|
|
73
|
+
if (!raw) return null;
|
|
74
|
+
const n = Number(raw);
|
|
75
|
+
return Number.isFinite(n) ? n : null;
|
|
76
|
+
} catch {
|
|
77
|
+
return snoozedUntil;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
const writeSnooze = (until) => {
|
|
81
|
+
snoozedUntil = until;
|
|
82
|
+
if (!storage) return;
|
|
83
|
+
try {
|
|
84
|
+
if (until === null) storage.removeItem(SNOOZE_KEY);
|
|
85
|
+
else storage.setItem(SNOOZE_KEY, String(until));
|
|
86
|
+
} catch {
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
const check = () => {
|
|
90
|
+
const waiting = registrationRef?.waiting ?? null;
|
|
91
|
+
waitingWorker = waiting;
|
|
92
|
+
const next = shouldOfferUpdate({
|
|
93
|
+
waiting,
|
|
94
|
+
snoozedUntil: readSnooze(),
|
|
95
|
+
now: Date.now()
|
|
96
|
+
});
|
|
97
|
+
if (next !== updateReady) {
|
|
98
|
+
updateReady = next;
|
|
56
99
|
emit();
|
|
57
100
|
}
|
|
58
101
|
};
|
|
@@ -71,21 +114,20 @@ function createPwaUpdater(options = {}) {
|
|
|
71
114
|
const installing = registration.installing;
|
|
72
115
|
if (!installing) return;
|
|
73
116
|
installing.addEventListener("statechange", () => {
|
|
74
|
-
if (installing.state === "installed"
|
|
75
|
-
markReady(installing);
|
|
76
|
-
}
|
|
117
|
+
if (installing.state === "installed") check();
|
|
77
118
|
});
|
|
78
119
|
};
|
|
79
120
|
const wireFocusChecks = (registration) => {
|
|
80
121
|
if (!updateOnFocus) return;
|
|
81
|
-
const
|
|
122
|
+
const tick = () => {
|
|
123
|
+
check();
|
|
82
124
|
registration.update().catch(() => {
|
|
83
|
-
});
|
|
125
|
+
}).then(check, check);
|
|
84
126
|
};
|
|
85
|
-
const onFocus = () =>
|
|
127
|
+
const onFocus = () => tick();
|
|
86
128
|
const onVisibility = () => {
|
|
87
129
|
if (typeof document !== "undefined" && document.visibilityState === "visible") {
|
|
88
|
-
|
|
130
|
+
tick();
|
|
89
131
|
}
|
|
90
132
|
};
|
|
91
133
|
if (typeof window !== "undefined") window.addEventListener("focus", onFocus);
|
|
@@ -102,12 +144,14 @@ function createPwaUpdater(options = {}) {
|
|
|
102
144
|
const registrationPromise = register ? container.register(swUrl) : container.ready;
|
|
103
145
|
registrationPromise.then((registration) => {
|
|
104
146
|
if (destroyed) return;
|
|
105
|
-
|
|
147
|
+
registrationRef = registration;
|
|
148
|
+
check();
|
|
106
149
|
registration.addEventListener("updatefound", () => watchInstalling(registration));
|
|
107
150
|
if (pollIntervalMs > 0) {
|
|
108
151
|
pollId = setInterval(() => {
|
|
152
|
+
check();
|
|
109
153
|
registration.update().catch(() => {
|
|
110
|
-
});
|
|
154
|
+
}).then(check, check);
|
|
111
155
|
}, pollIntervalMs);
|
|
112
156
|
}
|
|
113
157
|
wireFocusChecks(registration);
|
|
@@ -119,6 +163,11 @@ function createPwaUpdater(options = {}) {
|
|
|
119
163
|
applyUpdate() {
|
|
120
164
|
if (waitingWorker) waitingWorker.postMessage(SKIP_WAITING_MESSAGE);
|
|
121
165
|
},
|
|
166
|
+
snooze() {
|
|
167
|
+
writeSnooze(Date.now() + snoozeMs);
|
|
168
|
+
check();
|
|
169
|
+
},
|
|
170
|
+
check,
|
|
122
171
|
destroy() {
|
|
123
172
|
destroyed = true;
|
|
124
173
|
if (pollId !== null) clearInterval(pollId);
|
|
@@ -128,17 +177,33 @@ function createPwaUpdater(options = {}) {
|
|
|
128
177
|
}
|
|
129
178
|
};
|
|
130
179
|
}
|
|
180
|
+
|
|
181
|
+
// src/options-key.ts
|
|
182
|
+
var identities = /* @__PURE__ */ new WeakMap();
|
|
183
|
+
var nextIdentity = 0;
|
|
184
|
+
var identify = (value) => {
|
|
185
|
+
if (value === null || typeof value !== "object" && typeof value !== "function") {
|
|
186
|
+
return value;
|
|
187
|
+
}
|
|
188
|
+
const obj = value;
|
|
189
|
+
let tag = identities.get(obj);
|
|
190
|
+
if (tag === void 0) {
|
|
191
|
+
tag = `@ref:${nextIdentity++}`;
|
|
192
|
+
identities.set(obj, tag);
|
|
193
|
+
}
|
|
194
|
+
return tag;
|
|
195
|
+
};
|
|
196
|
+
function optionsKey(options) {
|
|
197
|
+
return JSON.stringify(
|
|
198
|
+
Object.entries(options).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => [k, identify(v)])
|
|
199
|
+
);
|
|
200
|
+
}
|
|
131
201
|
function usePwaUpdate(options = {}) {
|
|
132
|
-
const
|
|
202
|
+
const key = optionsKey(options);
|
|
133
203
|
const [updateReady, setUpdateReady] = react.useState(false);
|
|
134
204
|
const updaterRef = react.useRef(null);
|
|
135
205
|
react.useEffect(() => {
|
|
136
|
-
const updater = createPwaUpdater(
|
|
137
|
-
swUrl,
|
|
138
|
-
pollIntervalMs,
|
|
139
|
-
reloadOnControllerChange,
|
|
140
|
-
disabled
|
|
141
|
-
});
|
|
206
|
+
const updater = createPwaUpdater(options);
|
|
142
207
|
updaterRef.current = updater;
|
|
143
208
|
setUpdateReady(updater.getState().updateReady);
|
|
144
209
|
const unsubscribe = updater.subscribe((state) => setUpdateReady(state.updateReady));
|
|
@@ -147,10 +212,12 @@ function usePwaUpdate(options = {}) {
|
|
|
147
212
|
updater.destroy();
|
|
148
213
|
updaterRef.current = null;
|
|
149
214
|
};
|
|
150
|
-
}, [
|
|
215
|
+
}, [key]);
|
|
151
216
|
return {
|
|
152
217
|
updateReady,
|
|
153
|
-
applyUpdate: () => updaterRef.current?.applyUpdate()
|
|
218
|
+
applyUpdate: () => updaterRef.current?.applyUpdate(),
|
|
219
|
+
snooze: () => updaterRef.current?.snooze(),
|
|
220
|
+
check: () => updaterRef.current?.check()
|
|
154
221
|
};
|
|
155
222
|
}
|
|
156
223
|
var DEFAULT_LABELS = {
|
package/dist/react.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/message.ts","../src/index.ts","../src/react.tsx"],"names":["useState","useRef","useEffect","jsxs","jsx"],"mappings":"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","file":"react.cjs","sourcesContent":["/**\n * The single source of the client↔service-worker \"activate the waiting worker\"\n * message. Both the client controller (`applyUpdate`) and the SW helper\n * (`listenForSkipWaiting`) import it so the contract can never drift.\n */\nexport const SKIP_WAITING = \"SKIP_WAITING\" as const;\n\nexport const SKIP_WAITING_MESSAGE = { type: SKIP_WAITING } as const;\n\nexport type SkipWaitingMessage = typeof SKIP_WAITING_MESSAGE;\n","import { SKIP_WAITING_MESSAGE } from \"./message.js\";\n\nexport { SKIP_WAITING, SKIP_WAITING_MESSAGE } from \"./message.js\";\nexport type { SkipWaitingMessage } from \"./message.js\";\n\nexport interface PwaUpdaterOptions {\n /** Service-worker script URL to register. Default `/sw.js`. */\n swUrl?: string;\n /**\n * Register `swUrl` ourselves (manual `sw.js` style). Set `false` when the\n * app already registers the SW (e.g. Serwist's `registerSW` / `@serwist/next`)\n * — the updater then attaches to the existing registration via\n * `navigator.serviceWorker.ready` instead of registering again. Default `true`.\n */\n register?: boolean;\n /** How often to poll `registration.update()` (ms). `0` disables. Default 60 min. */\n pollIntervalMs?: number;\n /**\n * Also check for an update when the tab regains focus / becomes visible.\n * A constantly-focused tab never fires `visibilitychange`, so `focus` is the\n * one that catches \"user came back to the app\". Default `true`.\n */\n updateOnFocus?: boolean;\n /** Reload the page once the new worker takes control. Default `true`. */\n reloadOnControllerChange?: boolean;\n /**\n * Consumer guard. When `true` the updater is an inert no-op (registers\n * nothing). Pass your own policy, e.g. `disabled: isNativeCapacitor || isDev`\n * — the package never hardcodes an environment or a `.native` check.\n */\n disabled?: boolean;\n}\n\nexport interface PwaUpdaterState {\n /** A new service worker is installed and waiting to activate. */\n updateReady: boolean;\n}\n\nexport interface PwaUpdater {\n /** Subscribe to state changes. Returns an unsubscribe fn. */\n subscribe(listener: (state: PwaUpdaterState) => void): () => void;\n getState(): PwaUpdaterState;\n /** Tell the waiting worker to activate (posts SKIP_WAITING). No-op if none waits. */\n applyUpdate(): void;\n /** Stop polling + remove all listeners. */\n destroy(): void;\n}\n\nconst DEFAULT_POLL_MS = 60 * 60 * 1000;\n\n/**\n * Framework- and bundler-agnostic controller for the PWA \"new version\n * available\" lifecycle: register (or attach) → detect a waiting worker\n * (suppressing the first install) → let the consumer activate it → reload on\n * takeover, re-checking on interval + focus/visibility.\n *\n * Distilled from the pattern hand-rolled across the fleet (fds, cardmem,\n * pitch-vault). Works with any service worker (Serwist, Workbox, hand-rolled)\n * as long as the SW answers a `SKIP_WAITING` message — see `@broberg/pwa/sw`.\n */\nexport function createPwaUpdater(options: PwaUpdaterOptions = {}): PwaUpdater {\n const {\n swUrl = \"/sw.js\",\n register = true,\n pollIntervalMs = DEFAULT_POLL_MS,\n updateOnFocus = true,\n reloadOnControllerChange = true,\n disabled = false,\n } = options;\n\n const listeners = new Set<(state: PwaUpdaterState) => void>();\n let updateReady = false;\n let waitingWorker: ServiceWorker | null = null;\n\n const getState = (): PwaUpdaterState => ({ updateReady });\n const emit = (): void => {\n const state = getState();\n for (const listener of listeners) listener(state);\n };\n const subscribe = (listener: (state: PwaUpdaterState) => void): (() => void) => {\n listeners.add(listener);\n return () => listeners.delete(listener);\n };\n\n const supported =\n typeof navigator !== \"undefined\" && \"serviceWorker\" in navigator;\n\n // Inert no-op: disabled by the consumer, or no service-worker support at all.\n if (disabled || !supported) {\n return {\n subscribe,\n getState,\n applyUpdate() {},\n destroy() {\n listeners.clear();\n },\n };\n }\n\n const container = navigator.serviceWorker;\n let pollId: ReturnType<typeof setInterval> | null = null;\n let removeFocusListeners: (() => void) | null = null;\n let reloading = false;\n let destroyed = false;\n\n const markReady = (worker: ServiceWorker): void => {\n waitingWorker = worker;\n if (!updateReady) {\n updateReady = true;\n emit();\n }\n };\n\n // With `clientsClaim: true` a brand-new worker claims a previously\n // uncontrolled page and fires `controllerchange` on the FIRST install — that\n // is not an update, so reloading there would yank a first-time visitor out of\n // whatever they are doing. Only a `controllerchange` that REPLACES an existing\n // controller is a real takeover worth reloading for.\n let hadController = !!container.controller;\n const onControllerChange = (): void => {\n if (!reloadOnControllerChange || reloading) return;\n if (!hadController) {\n hadController = true; // first claim of an uncontrolled page — not an update\n return;\n }\n reloading = true; // guard against a reload-loop\n if (typeof location !== \"undefined\") location.reload();\n };\n container.addEventListener(\"controllerchange\", onControllerChange);\n\n const watchInstalling = (registration: ServiceWorkerRegistration): void => {\n const installing = registration.installing;\n if (!installing) return;\n installing.addEventListener(\"statechange\", () => {\n // A new worker reaching `installed` WHILE a controller already exists is\n // an update. Reaching `installed` with no controller is the FIRST install\n // — nothing to update, so we stay quiet.\n if (installing.state === \"installed\" && container.controller) {\n markReady(installing);\n }\n });\n };\n\n const wireFocusChecks = (registration: ServiceWorkerRegistration): void => {\n if (!updateOnFocus) return;\n const check = (): void => {\n registration.update().catch(() => {});\n };\n const onFocus = (): void => check();\n const onVisibility = (): void => {\n if (typeof document !== \"undefined\" && document.visibilityState === \"visible\") {\n check();\n }\n };\n if (typeof window !== \"undefined\") window.addEventListener(\"focus\", onFocus);\n if (typeof document !== \"undefined\") {\n document.addEventListener(\"visibilitychange\", onVisibility);\n }\n removeFocusListeners = () => {\n if (typeof window !== \"undefined\") window.removeEventListener(\"focus\", onFocus);\n if (typeof document !== \"undefined\") {\n document.removeEventListener(\"visibilitychange\", onVisibility);\n }\n };\n };\n\n // Manual style registers swUrl; serwist/registerSW style attaches to the\n // registration the app already made via `.ready`.\n const registrationPromise = register ? container.register(swUrl) : container.ready;\n\n registrationPromise\n .then((registration) => {\n if (destroyed) return;\n if (registration.waiting) markReady(registration.waiting);\n registration.addEventListener(\"updatefound\", () => watchInstalling(registration));\n if (pollIntervalMs > 0) {\n pollId = setInterval(() => {\n registration.update().catch(() => {\n // ignore transient update-check network errors\n });\n }, pollIntervalMs);\n }\n wireFocusChecks(registration);\n })\n .catch(() => {\n // registration failed — stay inert rather than throw into the app\n });\n\n return {\n subscribe,\n getState,\n applyUpdate() {\n if (waitingWorker) waitingWorker.postMessage(SKIP_WAITING_MESSAGE);\n },\n destroy() {\n destroyed = true;\n if (pollId !== null) clearInterval(pollId);\n removeFocusListeners?.();\n container.removeEventListener(\"controllerchange\", onControllerChange);\n listeners.clear();\n },\n };\n}\n","import { useEffect, useRef, useState } from \"react\";\nimport { createPwaUpdater, type PwaUpdater, type PwaUpdaterOptions } from \"./index.js\";\n\n/**\n * React binding for {@link createPwaUpdater}. Returns whether an update is\n * ready plus the `applyUpdate` action. The controller is created once and\n * torn down on unmount.\n */\nexport function usePwaUpdate(options: PwaUpdaterOptions = {}): {\n updateReady: boolean;\n applyUpdate: () => void;\n} {\n const { swUrl, pollIntervalMs, reloadOnControllerChange, disabled } = options;\n const [updateReady, setUpdateReady] = useState(false);\n const updaterRef = useRef<PwaUpdater | null>(null);\n\n useEffect(() => {\n const updater = createPwaUpdater({\n swUrl,\n pollIntervalMs,\n reloadOnControllerChange,\n disabled,\n });\n updaterRef.current = updater;\n setUpdateReady(updater.getState().updateReady);\n const unsubscribe = updater.subscribe((state) => setUpdateReady(state.updateReady));\n return () => {\n unsubscribe();\n updater.destroy();\n updaterRef.current = null;\n };\n }, [swUrl, pollIntervalMs, reloadOnControllerChange, disabled]);\n\n return {\n updateReady,\n applyUpdate: () => updaterRef.current?.applyUpdate(),\n };\n}\n\nexport interface PwaUpdateBannerLabels {\n title?: string;\n body?: string;\n update?: string;\n dismiss?: string;\n close?: string;\n}\n\nconst DEFAULT_LABELS: Required<PwaUpdateBannerLabels> = {\n title: \"New version available\",\n body: \"Reload to get the latest version.\",\n update: \"Update\",\n dismiss: \"Later\",\n close: \"Close\",\n};\n\nexport interface PwaUpdateBannerProps {\n /** From `usePwaUpdate().updateReady`. Nothing renders when false. */\n updateReady: boolean;\n /** From `usePwaUpdate().applyUpdate`. */\n onUpdate: () => void;\n /** Optional dismiss handler (wired to both \"Later\" and the close ✕). */\n onDismiss?: () => void;\n /** Override any of the built-in strings (i18n). */\n labels?: PwaUpdateBannerLabels;\n /** Your class(es) — the skeleton carries NO colours or design-system styling. */\n className?: string;\n}\n\n/**\n * Unstyled, accessible \"new version available\" banner skeleton. It ships\n * structure + a11y + stable testids only; you own the look via `className`\n * and your design tokens. Prefer {@link usePwaUpdate} directly if you'd rather\n * build the UI yourself.\n */\nexport function PwaUpdateBanner({\n updateReady,\n onUpdate,\n onDismiss,\n labels,\n className,\n}: PwaUpdateBannerProps) {\n if (!updateReady) return null;\n const l = { ...DEFAULT_LABELS, ...labels };\n const dismiss = () => onDismiss?.();\n return (\n <div role=\"status\" aria-live=\"polite\" data-testid=\"pwa-update-banner\" className={className}>\n <p data-testid=\"pwa-update-title\">{l.title}</p>\n {l.body ? <p data-testid=\"pwa-update-body\">{l.body}</p> : null}\n <button type=\"button\" data-testid=\"pwa-update-confirm\" onClick={onUpdate}>\n {l.update}\n </button>\n <button type=\"button\" data-testid=\"pwa-update-dismiss\" onClick={dismiss}>\n {l.dismiss}\n </button>\n <button\n type=\"button\"\n data-testid=\"pwa-update-close\"\n aria-label={l.close}\n onClick={dismiss}\n >\n {\"×\"}\n </button>\n </div>\n );\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/message.ts","../src/should-offer.ts","../src/index.ts","../src/options-key.ts","../src/react.tsx"],"names":["useState","useRef","useEffect","jsxs","jsx"],"mappings":"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","file":"react.cjs","sourcesContent":["/**\n * The single source of the client↔service-worker \"activate the waiting worker\"\n * message. Both the client controller (`applyUpdate`) and the SW helper\n * (`listenForSkipWaiting`) import it so the contract can never drift.\n */\nexport const SKIP_WAITING = \"SKIP_WAITING\" as const;\n\nexport const SKIP_WAITING_MESSAGE = { type: SKIP_WAITING } as const;\n\nexport type SkipWaitingMessage = typeof SKIP_WAITING_MESSAGE;\n","/**\n * F054.8 — the decision, as a pure function.\n *\n * Split out so it can be proven with NO service worker, NO DOM and no fake\n * container: every branch is reachable from three plain values. The rest of the\n * updater is plumbing around this.\n *\n * Why it is a function and not a flag: the old updater REMEMBERED whether an\n * update was ready instead of READING whether one is waiting, so the answer\n * could only ever be given once.\n */\nexport interface ShouldOfferInput {\n /** `registration.waiting` — read fresh at the moment of the decision. */\n waiting: unknown;\n /** Epoch ms until which the user has snoozed the offer, or null. */\n snoozedUntil: number | null;\n /** Epoch ms now. Passed in so the function stays pure and testable. */\n now: number;\n}\n\n/**\n * Should the consumer be offered the update right now?\n *\n * WHY `controller` IS NOT A TERM HERE, though the sketch we were shown had one\n * and the old code checked it on one of its two paths.\n *\n * The old updater answered this question inconsistently: the attach path\n * (`if (registration.waiting)`) offered with no controller check, while the\n * `updatefound` path required `container.controller` to suppress a first\n * install. Same situation, two answers, depending on which path happened to see\n * it. Making them agree is part of this fix — the question is which way.\n *\n * Requiring a controller would be the tighter rule, and it would REMOVE a banner\n * that some consumer sees today: a page that is not yet controlled (no\n * `clientsClaim`, or a hard reload) but has a genuine update waiting. With\n * fd-sundhed live to 16,838 people on the current version, a change in that\n * direction is not one to make on an unverified reading.\n *\n * And the reading in question is this: a worker only enters `waiting` when\n * another worker is already ACTIVE — on a first install there is nothing to wait\n * behind, so it activates directly. If that is right, `registration.waiting`\n * being non-null already implies this is not a first install, and the controller\n * term is redundant rather than protective. **I have not verified that against a\n * real service worker.** So the old, more permissive behaviour stays as the\n * floor, and first-install suppression rides on the same fact rather than on a\n * second guard that could disagree with it.\n *\n * If someone later measures that a first-install worker CAN sit in `waiting`,\n * this is the function to add the term to — and the test to write first is the\n * one that fails today.\n */\nexport function shouldOfferUpdate({ waiting, snoozedUntil, now }: ShouldOfferInput): boolean {\n if (!waiting) return false;\n if (snoozedUntil !== null && snoozedUntil > now) return false;\n return true;\n}\n","import { SKIP_WAITING_MESSAGE } from \"./message.js\";\nimport { shouldOfferUpdate } from \"./should-offer.js\";\n\nexport { SKIP_WAITING, SKIP_WAITING_MESSAGE } from \"./message.js\";\nexport { shouldOfferUpdate } from \"./should-offer.js\";\nexport type { ShouldOfferInput } from \"./should-offer.js\";\nexport type { SkipWaitingMessage } from \"./message.js\";\n\nexport interface PwaUpdaterOptions {\n /** Service-worker script URL to register. Default `/sw.js`. */\n swUrl?: string;\n /**\n * Register `swUrl` ourselves (manual `sw.js` style). Set `false` when the\n * app already registers the SW (e.g. Serwist's `registerSW` / `@serwist/next`)\n * — the updater then attaches to the existing registration via\n * `navigator.serviceWorker.ready` instead of registering again. Default `true`.\n */\n register?: boolean;\n /** How often to poll `registration.update()` (ms). `0` disables. Default 60 min. */\n pollIntervalMs?: number;\n /**\n * Also check for an update when the tab regains focus / becomes visible.\n * A constantly-focused tab never fires `visibilitychange`, so `focus` is the\n * one that catches \"user came back to the app\". Default `true`.\n */\n updateOnFocus?: boolean;\n /** Reload the page once the new worker takes control. Default `true`. */\n reloadOnControllerChange?: boolean;\n /**\n * How long `snooze()` holds the offer down (ms). Default 30 min.\n *\n * F054.8 — \"Later\" is a SNOOZE, never a mute. The defect this replaced was a\n * banner that got exactly one chance to be seen; a permanent dismiss is that\n * defect made official.\n */\n snoozeMs?: number;\n /**\n * Where a snooze is remembered, so it survives the reload the banner is\n * asking for. Defaults to `localStorage` when available; pass `null` to keep\n * the snooze in memory only.\n */\n snoozeStorage?: Pick<Storage, \"getItem\" | \"setItem\" | \"removeItem\"> | null;\n /**\n * Consumer guard. When `true` the updater is an inert no-op (registers\n * nothing). Pass your own policy, e.g. `disabled: isNativeCapacitor || isDev`\n * — the package never hardcodes an environment or a `.native` check.\n */\n disabled?: boolean;\n}\n\nexport interface PwaUpdaterState {\n /** A new service worker is installed and waiting to activate. */\n updateReady: boolean;\n}\n\nexport interface PwaUpdater {\n /** Subscribe to state changes. Returns an unsubscribe fn. */\n subscribe(listener: (state: PwaUpdaterState) => void): () => void;\n getState(): PwaUpdaterState;\n /** Tell the waiting worker to activate (posts SKIP_WAITING). No-op if none waits. */\n applyUpdate(): void;\n /**\n * \"Later\". Holds the offer down for `snoozeMs`, then it comes back on the\n * next tick — this is NOT a mute, and it deliberately cannot be made one.\n */\n snooze(): void;\n /** Re-read `registration.waiting` right now and emit if the answer moved. */\n check(): void;\n /** Stop polling + remove all listeners. */\n destroy(): void;\n}\n\nconst DEFAULT_POLL_MS = 60 * 60 * 1000;\nconst DEFAULT_SNOOZE_MS = 30 * 60 * 1000;\nconst SNOOZE_KEY = \"broberg-pwa:snoozed-until\";\n\n/**\n * Framework- and bundler-agnostic controller for the PWA \"new version\n * available\" lifecycle: register (or attach) → detect a waiting worker\n * (suppressing the first install) → let the consumer activate it → reload on\n * takeover, re-checking on interval + focus/visibility.\n *\n * Distilled from the pattern hand-rolled across the fleet (fds, cardmem,\n * pitch-vault). Works with any service worker (Serwist, Workbox, hand-rolled)\n * as long as the SW answers a `SKIP_WAITING` message — see `@broberg/pwa/sw`.\n */\nexport function createPwaUpdater(options: PwaUpdaterOptions = {}): PwaUpdater {\n const {\n swUrl = \"/sw.js\",\n register = true,\n pollIntervalMs = DEFAULT_POLL_MS,\n updateOnFocus = true,\n reloadOnControllerChange = true,\n snoozeMs = DEFAULT_SNOOZE_MS,\n disabled = false,\n } = options;\n\n const listeners = new Set<(state: PwaUpdaterState) => void>();\n let updateReady = false;\n let waitingWorker: ServiceWorker | null = null;\n\n const getState = (): PwaUpdaterState => ({ updateReady });\n const emit = (): void => {\n const state = getState();\n for (const listener of listeners) listener(state);\n };\n const subscribe = (listener: (state: PwaUpdaterState) => void): (() => void) => {\n listeners.add(listener);\n return () => listeners.delete(listener);\n };\n\n const supported =\n typeof navigator !== \"undefined\" && \"serviceWorker\" in navigator;\n\n // Inert no-op: disabled by the consumer, or no service-worker support at all.\n if (disabled || !supported) {\n return {\n subscribe,\n getState,\n applyUpdate() {},\n snooze() {},\n check() {},\n destroy() {\n listeners.clear();\n },\n };\n }\n\n const container = navigator.serviceWorker;\n let pollId: ReturnType<typeof setInterval> | null = null;\n let removeFocusListeners: (() => void) | null = null;\n let reloading = false;\n let destroyed = false;\n\n // F054.8 — READ the answer, never remember it.\n //\n // This used to be markReady(worker), the only writer of `updateReady`, and it\n // was guarded by `if (!updateReady)` — so `emit()` fired AT MOST ONCE for the\n // life of the updater. Both adapters drive their state from subscribe(), so a\n // consumer who dismissed the banner had no channel that could ever raise it\n // again. cardmem measured the consequence in production: a client sat on an\n // old bundle indefinitely, and the only symptom anyone had was a person\n // saying a feature was missing.\n //\n // `registration` is captured once resolved so every tick can re-read\n // `.waiting` rather than wait to be told.\n let registrationRef: ServiceWorkerRegistration | null = null;\n\n const storage: PwaUpdaterOptions[\"snoozeStorage\"] =\n options.snoozeStorage !== undefined\n ? options.snoozeStorage\n : typeof localStorage !== \"undefined\"\n ? localStorage\n : null;\n\n let snoozedUntil: number | null = null;\n const readSnooze = (): number | null => {\n if (!storage) return snoozedUntil;\n try {\n const raw = storage.getItem(SNOOZE_KEY);\n if (!raw) return null;\n const n = Number(raw);\n return Number.isFinite(n) ? n : null;\n } catch {\n // A browser with site data blocked THROWS on read. That is not a reason to\n // stop offering updates — fall back to the in-memory value.\n return snoozedUntil;\n }\n };\n const writeSnooze = (until: number | null): void => {\n snoozedUntil = until;\n if (!storage) return;\n try {\n if (until === null) storage.removeItem(SNOOZE_KEY);\n else storage.setItem(SNOOZE_KEY, String(until));\n } catch {\n // in-memory only for this tab; already recorded above\n }\n };\n\n /**\n * Re-derive from what is true RIGHT NOW and emit only when the answer moved.\n * Emits in BOTH directions: false→true raises the banner, true→false takes it\n * down when the worker is gone or the user snoozed.\n */\n const check = (): void => {\n const waiting = registrationRef?.waiting ?? null;\n waitingWorker = waiting;\n const next = shouldOfferUpdate({\n waiting,\n snoozedUntil: readSnooze(),\n now: Date.now(),\n });\n if (next !== updateReady) {\n updateReady = next;\n emit();\n }\n };\n\n // With `clientsClaim: true` a brand-new worker claims a previously\n // uncontrolled page and fires `controllerchange` on the FIRST install — that\n // is not an update, so reloading there would yank a first-time visitor out of\n // whatever they are doing. Only a `controllerchange` that REPLACES an existing\n // controller is a real takeover worth reloading for.\n let hadController = !!container.controller;\n const onControllerChange = (): void => {\n if (!reloadOnControllerChange || reloading) return;\n if (!hadController) {\n hadController = true; // first claim of an uncontrolled page — not an update\n return;\n }\n reloading = true; // guard against a reload-loop\n if (typeof location !== \"undefined\") location.reload();\n };\n container.addEventListener(\"controllerchange\", onControllerChange);\n\n const watchInstalling = (registration: ServiceWorkerRegistration): void => {\n const installing = registration.installing;\n if (!installing) return;\n installing.addEventListener(\"statechange\", () => {\n // A new worker reaching `installed` WHILE a controller already exists is\n // an update. Reaching `installed` with no controller is the FIRST install\n // — nothing to update, so we stay quiet.\n if (installing.state === \"installed\") check();\n });\n };\n\n const wireFocusChecks = (registration: ServiceWorkerRegistration): void => {\n if (!updateOnFocus) return;\n // update() asks the SERVER for a new worker; check() reads whether one is\n // already WAITING. The second is the half that was missing — reg.update()\n // resolves without firing updatefound when a worker is already waiting, so\n // a tab that missed the original event could never learn about it.\n const tick = (): void => {\n check();\n registration.update().catch(() => {}).then(check, check);\n };\n const onFocus = (): void => tick();\n const onVisibility = (): void => {\n if (typeof document !== \"undefined\" && document.visibilityState === \"visible\") {\n tick();\n }\n };\n if (typeof window !== \"undefined\") window.addEventListener(\"focus\", onFocus);\n if (typeof document !== \"undefined\") {\n document.addEventListener(\"visibilitychange\", onVisibility);\n }\n removeFocusListeners = () => {\n if (typeof window !== \"undefined\") window.removeEventListener(\"focus\", onFocus);\n if (typeof document !== \"undefined\") {\n document.removeEventListener(\"visibilitychange\", onVisibility);\n }\n };\n };\n\n // Manual style registers swUrl; serwist/registerSW style attaches to the\n // registration the app already made via `.ready`.\n const registrationPromise = register ? container.register(swUrl) : container.ready;\n\n registrationPromise\n .then((registration) => {\n if (destroyed) return;\n registrationRef = registration;\n check();\n registration.addEventListener(\"updatefound\", () => watchInstalling(registration));\n if (pollIntervalMs > 0) {\n pollId = setInterval(() => {\n check();\n registration\n .update()\n .catch(() => {\n // ignore transient update-check network errors\n })\n .then(check, check);\n }, pollIntervalMs);\n }\n wireFocusChecks(registration);\n })\n .catch(() => {\n // registration failed — stay inert rather than throw into the app\n });\n\n return {\n subscribe,\n getState,\n applyUpdate() {\n if (waitingWorker) waitingWorker.postMessage(SKIP_WAITING_MESSAGE);\n },\n snooze() {\n writeSnooze(Date.now() + snoozeMs);\n check();\n },\n check,\n destroy() {\n destroyed = true;\n if (pollId !== null) clearInterval(pollId);\n removeFocusListeners?.();\n container.removeEventListener(\"controllerchange\", onControllerChange);\n listeners.clear();\n },\n };\n}\n","import type { PwaUpdaterOptions } from \"./index.js\";\n\n/**\n * A stable identity for an options object, for an adapter's effect dependency.\n *\n * F054.7 — WHY THIS EXISTS INSTEAD OF A DEPENDENCY LIST. Both adapters used to\n * destructure four named options and list those same four as their effect deps.\n * That is a second copy of the core's option list, and it had already drifted:\n * `register` and `updateOnFocus` were never forwarded, so a consumer writing\n * `register: false` got a service-worker registration anyway, with no error and\n * no warning. fd-sundhed found it by reading our shipped dist, which is not a\n * detector anyone should need.\n *\n * Deriving the key from the options the CALLER passed means a new core option\n * reaches both adapters with neither adapter edited. The list was the defect —\n * not the two names missing from it.\n *\n * Keys are SORTED so `{a, b}` and `{b, a}` are the same identity, and\n * `undefined` values are dropped so an explicitly-omitted option and an absent\n * one do not look like different configurations.\n *\n * THE ONE BOUNDARY, stated because the claim above is otherwise broader than\n * what is true: the identity is JSON, so a value JSON cannot express collapses.\n * Measured — `{onError: () => 1}` and `{onError: () => 2}` produce the byte-\n * identical key `[[\"onError\",null]]`, so a caller swapping the callback would\n * keep the first updater and never see the new one take effect. Every option\n * the core has today is a string, number or boolean, so nothing is affected;\n * this is here for whoever adds the first FUNCTION option (an `onUpdateReady`\n * is the obvious candidate). That option must be given its own identity here,\n * not merely added to the core.\n */\n/**\n * F054.8 — the boundary above is no longer hypothetical: `snoozeStorage` is an\n * object of functions, and `JSON.stringify` renders it `{}`. Two different\n * storages would produce the byte-identical key, so a caller swapping one would\n * keep the first updater and never see the new one — the exact collapse the\n * paragraph above predicted, arriving one option later.\n *\n * So a value JSON cannot express gets a stable identity of its own instead: the\n * same object gives the same tag for the life of the page, a different object a\n * different tag. Kept in a WeakMap so holding the tag cannot hold the object.\n */\nconst identities = new WeakMap<object, string>();\nlet nextIdentity = 0;\nconst identify = (value: unknown): unknown => {\n if (value === null || (typeof value !== \"object\" && typeof value !== \"function\")) {\n return value;\n }\n const obj = value as object;\n let tag = identities.get(obj);\n if (tag === undefined) {\n tag = `@ref:${nextIdentity++}`;\n identities.set(obj, tag);\n }\n return tag;\n};\n\nexport function optionsKey(options: PwaUpdaterOptions): string {\n return JSON.stringify(\n Object.entries(options)\n .filter(([, v]) => v !== undefined)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n .map(([k, v]) => [k, identify(v)]),\n );\n}\n","import { useEffect, useRef, useState } from \"react\";\nimport { createPwaUpdater, type PwaUpdater, type PwaUpdaterOptions } from \"./index.js\";\nimport { optionsKey } from \"./options-key.js\";\n\n/**\n * React binding for {@link createPwaUpdater}. Returns whether an update is\n * ready plus the `applyUpdate` action. The controller is created once and\n * torn down on unmount.\n */\nexport function usePwaUpdate(options: PwaUpdaterOptions = {}): {\n updateReady: boolean;\n applyUpdate: () => void;\n /** \"Later\" — F054.8. Holds the offer down for `snoozeMs`, then it returns. */\n snooze: () => void;\n /** Re-read `registration.waiting` now. Rarely needed; the ticks do it. */\n check: () => void;\n} {\n // The effect's identity comes from the options the CALLER passed, so a new\n // core option needs no edit here. eslint's exhaustive-deps cannot see\n // through `key`; that is the trade, and it is the right way round.\n const key = optionsKey(options);\n const [updateReady, setUpdateReady] = useState(false);\n const updaterRef = useRef<PwaUpdater | null>(null);\n\n useEffect(() => {\n // THE WHOLE OBJECT, not a hand-picked subset. See options-key.ts: the\n // subset is what silently dropped `register` and `updateOnFocus`.\n const updater = createPwaUpdater(options);\n updaterRef.current = updater;\n setUpdateReady(updater.getState().updateReady);\n const unsubscribe = updater.subscribe((state) => setUpdateReady(state.updateReady));\n return () => {\n unsubscribe();\n updater.destroy();\n updaterRef.current = null;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [key]);\n\n return {\n updateReady,\n applyUpdate: () => updaterRef.current?.applyUpdate(),\n snooze: () => updaterRef.current?.snooze(),\n check: () => updaterRef.current?.check(),\n };\n}\n\nexport interface PwaUpdateBannerLabels {\n title?: string;\n body?: string;\n update?: string;\n dismiss?: string;\n close?: string;\n}\n\nconst DEFAULT_LABELS: Required<PwaUpdateBannerLabels> = {\n title: \"New version available\",\n body: \"Reload to get the latest version.\",\n update: \"Update\",\n dismiss: \"Later\",\n close: \"Close\",\n};\n\nexport interface PwaUpdateBannerProps {\n /** From `usePwaUpdate().updateReady`. Nothing renders when false. */\n updateReady: boolean;\n /** From `usePwaUpdate().applyUpdate`. */\n onUpdate: () => void;\n /** Optional dismiss handler (wired to both \"Later\" and the close ✕). */\n onDismiss?: () => void;\n /** Override any of the built-in strings (i18n). */\n labels?: PwaUpdateBannerLabels;\n /** Your class(es) — the skeleton carries NO colours or design-system styling. */\n className?: string;\n}\n\n/**\n * Unstyled, accessible \"new version available\" banner skeleton. It ships\n * structure + a11y + stable testids only; you own the look via `className`\n * and your design tokens. Prefer {@link usePwaUpdate} directly if you'd rather\n * build the UI yourself.\n */\nexport function PwaUpdateBanner({\n updateReady,\n onUpdate,\n onDismiss,\n labels,\n className,\n}: PwaUpdateBannerProps) {\n if (!updateReady) return null;\n const l = { ...DEFAULT_LABELS, ...labels };\n const dismiss = () => onDismiss?.();\n return (\n <div role=\"status\" aria-live=\"polite\" data-testid=\"pwa-update-banner\" className={className}>\n <p data-testid=\"pwa-update-title\">{l.title}</p>\n {l.body ? <p data-testid=\"pwa-update-body\">{l.body}</p> : null}\n <button type=\"button\" data-testid=\"pwa-update-confirm\" onClick={onUpdate}>\n {l.update}\n </button>\n <button type=\"button\" data-testid=\"pwa-update-dismiss\" onClick={dismiss}>\n {l.dismiss}\n </button>\n <button\n type=\"button\"\n data-testid=\"pwa-update-close\"\n aria-label={l.close}\n onClick={dismiss}\n >\n {\"×\"}\n </button>\n </div>\n );\n}\n"]}
|
package/dist/react.d.cts
CHANGED
|
@@ -20,6 +20,20 @@ interface PwaUpdaterOptions {
|
|
|
20
20
|
updateOnFocus?: boolean;
|
|
21
21
|
/** Reload the page once the new worker takes control. Default `true`. */
|
|
22
22
|
reloadOnControllerChange?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* How long `snooze()` holds the offer down (ms). Default 30 min.
|
|
25
|
+
*
|
|
26
|
+
* F054.8 — "Later" is a SNOOZE, never a mute. The defect this replaced was a
|
|
27
|
+
* banner that got exactly one chance to be seen; a permanent dismiss is that
|
|
28
|
+
* defect made official.
|
|
29
|
+
*/
|
|
30
|
+
snoozeMs?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Where a snooze is remembered, so it survives the reload the banner is
|
|
33
|
+
* asking for. Defaults to `localStorage` when available; pass `null` to keep
|
|
34
|
+
* the snooze in memory only.
|
|
35
|
+
*/
|
|
36
|
+
snoozeStorage?: Pick<Storage, "getItem" | "setItem" | "removeItem"> | null;
|
|
23
37
|
/**
|
|
24
38
|
* Consumer guard. When `true` the updater is an inert no-op (registers
|
|
25
39
|
* nothing). Pass your own policy, e.g. `disabled: isNativeCapacitor || isDev`
|
|
@@ -36,6 +50,10 @@ interface PwaUpdaterOptions {
|
|
|
36
50
|
declare function usePwaUpdate(options?: PwaUpdaterOptions): {
|
|
37
51
|
updateReady: boolean;
|
|
38
52
|
applyUpdate: () => void;
|
|
53
|
+
/** "Later" — F054.8. Holds the offer down for `snoozeMs`, then it returns. */
|
|
54
|
+
snooze: () => void;
|
|
55
|
+
/** Re-read `registration.waiting` now. Rarely needed; the ticks do it. */
|
|
56
|
+
check: () => void;
|
|
39
57
|
};
|
|
40
58
|
interface PwaUpdateBannerLabels {
|
|
41
59
|
title?: string;
|
package/dist/react.d.ts
CHANGED
|
@@ -20,6 +20,20 @@ interface PwaUpdaterOptions {
|
|
|
20
20
|
updateOnFocus?: boolean;
|
|
21
21
|
/** Reload the page once the new worker takes control. Default `true`. */
|
|
22
22
|
reloadOnControllerChange?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* How long `snooze()` holds the offer down (ms). Default 30 min.
|
|
25
|
+
*
|
|
26
|
+
* F054.8 — "Later" is a SNOOZE, never a mute. The defect this replaced was a
|
|
27
|
+
* banner that got exactly one chance to be seen; a permanent dismiss is that
|
|
28
|
+
* defect made official.
|
|
29
|
+
*/
|
|
30
|
+
snoozeMs?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Where a snooze is remembered, so it survives the reload the banner is
|
|
33
|
+
* asking for. Defaults to `localStorage` when available; pass `null` to keep
|
|
34
|
+
* the snooze in memory only.
|
|
35
|
+
*/
|
|
36
|
+
snoozeStorage?: Pick<Storage, "getItem" | "setItem" | "removeItem"> | null;
|
|
23
37
|
/**
|
|
24
38
|
* Consumer guard. When `true` the updater is an inert no-op (registers
|
|
25
39
|
* nothing). Pass your own policy, e.g. `disabled: isNativeCapacitor || isDev`
|
|
@@ -36,6 +50,10 @@ interface PwaUpdaterOptions {
|
|
|
36
50
|
declare function usePwaUpdate(options?: PwaUpdaterOptions): {
|
|
37
51
|
updateReady: boolean;
|
|
38
52
|
applyUpdate: () => void;
|
|
53
|
+
/** "Later" — F054.8. Holds the offer down for `snoozeMs`, then it returns. */
|
|
54
|
+
snooze: () => void;
|
|
55
|
+
/** Re-read `registration.waiting` now. Rarely needed; the ticks do it. */
|
|
56
|
+
check: () => void;
|
|
39
57
|
};
|
|
40
58
|
interface PwaUpdateBannerLabels {
|
|
41
59
|
title?: string;
|