@broberg/pwa 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -0
- package/dist/chunk-ERNF6MZL.js +7 -0
- package/dist/chunk-ERNF6MZL.js.map +1 -0
- package/dist/index.cjs +126 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +55 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +121 -0
- package/dist/index.js.map +1 -0
- package/dist/message-CBBmtYuB.d.cts +12 -0
- package/dist/message-CBBmtYuB.d.ts +12 -0
- package/dist/react.cjs +189 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +67 -0
- package/dist/react.d.ts +67 -0
- package/dist/react.js +186 -0
- package/dist/react.js.map +1 -0
- package/dist/sw.cjs +21 -0
- package/dist/sw.cjs.map +1 -0
- package/dist/sw.d.cts +30 -0
- package/dist/sw.d.ts +30 -0
- package/dist/sw.js +16 -0
- package/dist/sw.js.map +1 -0
- package/package.json +78 -0
package/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# @broberg/pwa
|
|
2
|
+
|
|
3
|
+
The fleet's **PWA primitive**. Today it ships the piece four repos hand-rolled
|
|
4
|
+
independently — the *"a new version is available, tap to reload"* lifecycle —
|
|
5
|
+
as one small, framework- and bundler-agnostic package instead of a fifth copy.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i @broberg/pwa
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
- **`@broberg/pwa`** — `createPwaUpdater()`, a zero-dependency controller for the
|
|
12
|
+
service-worker update lifecycle (works with Serwist, Workbox or a hand-rolled SW).
|
|
13
|
+
- **`@broberg/pwa/react`** — `usePwaUpdate()` hook + an **unstyled** `<PwaUpdateBanner>`
|
|
14
|
+
skeleton (you style it with your own tokens).
|
|
15
|
+
- **`@broberg/pwa/sw`** — `listenForSkipWaiting()`, the service-worker side of the handshake.
|
|
16
|
+
|
|
17
|
+
> Web **push** notifications are a separate concern — use
|
|
18
|
+
> [`@broberg/webpush`](https://discovery.broberg.ai). This package never touches push.
|
|
19
|
+
|
|
20
|
+
## The update lifecycle
|
|
21
|
+
|
|
22
|
+
A service worker that finds a new version installs it and then **waits** so it
|
|
23
|
+
doesn't yank the page out from under the user. `@broberg/pwa` detects that
|
|
24
|
+
waiting worker, lets the user choose when to take it, then reloads once it's active:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
new deploy → SW installs → SW waiting → banner → user taps Update
|
|
28
|
+
→ applyUpdate() posts SKIP_WAITING → SW skipWaiting() → controllerchange → reload
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## React usage
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
"use client";
|
|
35
|
+
import { usePwaUpdate, PwaUpdateBanner } from "@broberg/pwa/react";
|
|
36
|
+
|
|
37
|
+
export function PwaUpdater() {
|
|
38
|
+
// Guards are YOUR policy — pass `disabled` for native shells / dev.
|
|
39
|
+
const isNative = document.documentElement.classList.contains("native");
|
|
40
|
+
const { updateReady, applyUpdate } = usePwaUpdate({
|
|
41
|
+
disabled: isNative || process.env.NODE_ENV !== "production",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<PwaUpdateBanner
|
|
46
|
+
updateReady={updateReady}
|
|
47
|
+
onUpdate={applyUpdate}
|
|
48
|
+
onDismiss={() => {/* hide for this session; it returns on next load */}}
|
|
49
|
+
className="my-banner" // ← you own the styling
|
|
50
|
+
labels={{ title: "Ny version klar", update: "Opdatér", dismiss: "Senere" }}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Prefer to build your own UI? Use the hook alone — `usePwaUpdate()` gives you
|
|
57
|
+
`{ updateReady, applyUpdate }` and nothing else. The banner is only a convenience
|
|
58
|
+
skeleton: it renders `role="status"` + `aria-live="polite"`, carries the stable
|
|
59
|
+
testids `pwa-update-confirm` / `pwa-update-dismiss` / `pwa-update-close`, and
|
|
60
|
+
ships **no** colours or design-system classes.
|
|
61
|
+
|
|
62
|
+
## Framework-agnostic core
|
|
63
|
+
|
|
64
|
+
No React? Drive the controller directly (Preact, Svelte, vanilla):
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { createPwaUpdater } from "@broberg/pwa";
|
|
68
|
+
|
|
69
|
+
const updater = createPwaUpdater({ swUrl: "/sw.js", disabled: isNative });
|
|
70
|
+
updater.subscribe(({ updateReady }) => renderBanner(updateReady));
|
|
71
|
+
// on the user's "Update" click:
|
|
72
|
+
updater.applyUpdate();
|
|
73
|
+
// later: updater.destroy();
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### `createPwaUpdater(options)`
|
|
77
|
+
|
|
78
|
+
| option | default | meaning |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| `swUrl` | `/sw.js` | service-worker script to register |
|
|
81
|
+
| `pollIntervalMs` | `3_600_000` | how often to check for a new SW; `0` disables |
|
|
82
|
+
| `reloadOnControllerChange` | `true` | reload the page once the new SW takes control |
|
|
83
|
+
| `disabled` | `false` | inert no-op — pass your own guard (native shell, dev) |
|
|
84
|
+
|
|
85
|
+
Returns `{ subscribe, getState, applyUpdate, destroy }`. With no service-worker
|
|
86
|
+
support (or `disabled`), it's an inert no-op that never throws.
|
|
87
|
+
|
|
88
|
+
## Service-worker side
|
|
89
|
+
|
|
90
|
+
Answer the client's activation request in your service worker:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
// sw.ts
|
|
94
|
+
import { listenForSkipWaiting } from "@broberg/pwa/sw";
|
|
95
|
+
listenForSkipWaiting(); // defaults to the worker's own global scope
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
With **Serwist**/**Workbox**, keep activation user-gated:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
new Serwist({ /* … */, skipWaiting: false, clientsClaim: true });
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Gotchas (baked into this package so you don't rediscover them)
|
|
105
|
+
|
|
106
|
+
- **First install is suppressed.** A worker reaching `installed` with no existing
|
|
107
|
+
controller is the *first* install — there's nothing to update, so no banner.
|
|
108
|
+
- **No reload-loop.** `controllerchange` reloads exactly once.
|
|
109
|
+
- **Serwist + Next.js needs the webpack builder** — `next build` with **Turbopack**
|
|
110
|
+
breaks the serwist SW build. Use `next build` (webpack).
|
|
111
|
+
- **Guards belong to you.** This package doesn't sniff `NODE_ENV` or a `.native`
|
|
112
|
+
class — pass `disabled` so the policy lives in one place in your app.
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT · part of the [`@broberg/*`](https://discovery.broberg.ai) shared inventory.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/message.ts"],"names":[],"mappings":";AAKO,IAAM,YAAA,GAAe;AAErB,IAAM,oBAAA,GAAuB,EAAE,IAAA,EAAM,YAAA","file":"chunk-ERNF6MZL.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"]}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/message.ts
|
|
4
|
+
var SKIP_WAITING = "SKIP_WAITING";
|
|
5
|
+
var SKIP_WAITING_MESSAGE = { type: SKIP_WAITING };
|
|
6
|
+
|
|
7
|
+
// src/index.ts
|
|
8
|
+
var DEFAULT_POLL_MS = 60 * 60 * 1e3;
|
|
9
|
+
function createPwaUpdater(options = {}) {
|
|
10
|
+
const {
|
|
11
|
+
swUrl = "/sw.js",
|
|
12
|
+
register = true,
|
|
13
|
+
pollIntervalMs = DEFAULT_POLL_MS,
|
|
14
|
+
updateOnFocus = true,
|
|
15
|
+
reloadOnControllerChange = true,
|
|
16
|
+
disabled = false
|
|
17
|
+
} = options;
|
|
18
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
19
|
+
let updateReady = false;
|
|
20
|
+
let waitingWorker = null;
|
|
21
|
+
const getState = () => ({ updateReady });
|
|
22
|
+
const emit = () => {
|
|
23
|
+
const state = getState();
|
|
24
|
+
for (const listener of listeners) listener(state);
|
|
25
|
+
};
|
|
26
|
+
const subscribe = (listener) => {
|
|
27
|
+
listeners.add(listener);
|
|
28
|
+
return () => listeners.delete(listener);
|
|
29
|
+
};
|
|
30
|
+
const supported = typeof navigator !== "undefined" && "serviceWorker" in navigator;
|
|
31
|
+
if (disabled || !supported) {
|
|
32
|
+
return {
|
|
33
|
+
subscribe,
|
|
34
|
+
getState,
|
|
35
|
+
applyUpdate() {
|
|
36
|
+
},
|
|
37
|
+
destroy() {
|
|
38
|
+
listeners.clear();
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const container = navigator.serviceWorker;
|
|
43
|
+
let pollId = null;
|
|
44
|
+
let removeFocusListeners = null;
|
|
45
|
+
let reloading = false;
|
|
46
|
+
let destroyed = false;
|
|
47
|
+
const markReady = (worker) => {
|
|
48
|
+
waitingWorker = worker;
|
|
49
|
+
if (!updateReady) {
|
|
50
|
+
updateReady = true;
|
|
51
|
+
emit();
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const onControllerChange = () => {
|
|
55
|
+
if (!reloadOnControllerChange || reloading) return;
|
|
56
|
+
reloading = true;
|
|
57
|
+
if (typeof location !== "undefined") location.reload();
|
|
58
|
+
};
|
|
59
|
+
container.addEventListener("controllerchange", onControllerChange);
|
|
60
|
+
const watchInstalling = (registration) => {
|
|
61
|
+
const installing = registration.installing;
|
|
62
|
+
if (!installing) return;
|
|
63
|
+
installing.addEventListener("statechange", () => {
|
|
64
|
+
if (installing.state === "installed" && container.controller) {
|
|
65
|
+
markReady(installing);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
const wireFocusChecks = (registration) => {
|
|
70
|
+
if (!updateOnFocus) return;
|
|
71
|
+
const check = () => {
|
|
72
|
+
registration.update().catch(() => {
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
const onFocus = () => check();
|
|
76
|
+
const onVisibility = () => {
|
|
77
|
+
if (typeof document !== "undefined" && document.visibilityState === "visible") {
|
|
78
|
+
check();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
if (typeof window !== "undefined") window.addEventListener("focus", onFocus);
|
|
82
|
+
if (typeof document !== "undefined") {
|
|
83
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
84
|
+
}
|
|
85
|
+
removeFocusListeners = () => {
|
|
86
|
+
if (typeof window !== "undefined") window.removeEventListener("focus", onFocus);
|
|
87
|
+
if (typeof document !== "undefined") {
|
|
88
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
const registrationPromise = register ? container.register(swUrl) : container.ready;
|
|
93
|
+
registrationPromise.then((registration) => {
|
|
94
|
+
if (destroyed) return;
|
|
95
|
+
if (registration.waiting) markReady(registration.waiting);
|
|
96
|
+
registration.addEventListener("updatefound", () => watchInstalling(registration));
|
|
97
|
+
if (pollIntervalMs > 0) {
|
|
98
|
+
pollId = setInterval(() => {
|
|
99
|
+
registration.update().catch(() => {
|
|
100
|
+
});
|
|
101
|
+
}, pollIntervalMs);
|
|
102
|
+
}
|
|
103
|
+
wireFocusChecks(registration);
|
|
104
|
+
}).catch(() => {
|
|
105
|
+
});
|
|
106
|
+
return {
|
|
107
|
+
subscribe,
|
|
108
|
+
getState,
|
|
109
|
+
applyUpdate() {
|
|
110
|
+
if (waitingWorker) waitingWorker.postMessage(SKIP_WAITING_MESSAGE);
|
|
111
|
+
},
|
|
112
|
+
destroy() {
|
|
113
|
+
destroyed = true;
|
|
114
|
+
if (pollId !== null) clearInterval(pollId);
|
|
115
|
+
removeFocusListeners?.();
|
|
116
|
+
container.removeEventListener("controllerchange", onControllerChange);
|
|
117
|
+
listeners.clear();
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
exports.SKIP_WAITING = SKIP_WAITING;
|
|
123
|
+
exports.SKIP_WAITING_MESSAGE = SKIP_WAITING_MESSAGE;
|
|
124
|
+
exports.createPwaUpdater = createPwaUpdater;
|
|
125
|
+
//# sourceMappingURL=index.cjs.map
|
|
126
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/message.ts","../src/index.ts"],"names":[],"mappings":";;;AAKO,IAAM,YAAA,GAAe;AAErB,IAAM,oBAAA,GAAuB,EAAE,IAAA,EAAM,YAAA;;;ACyC5C,IAAM,eAAA,GAAkB,KAAK,EAAA,GAAK,GAAA;AAY3B,SAAS,gBAAA,CAAiB,OAAA,GAA6B,EAAC,EAAe;AAC5E,EAAA,MAAM;AAAA,IACJ,KAAA,GAAQ,QAAA;AAAA,IACR,QAAA,GAAW,IAAA;AAAA,IACX,cAAA,GAAiB,eAAA;AAAA,IACjB,aAAA,GAAgB,IAAA;AAAA,IAChB,wBAAA,GAA2B,IAAA;AAAA,IAC3B,QAAA,GAAW;AAAA,GACb,GAAI,OAAA;AAEJ,EAAA,MAAM,SAAA,uBAAgB,GAAA,EAAsC;AAC5D,EAAA,IAAI,WAAA,GAAc,KAAA;AAClB,EAAA,IAAI,aAAA,GAAsC,IAAA;AAE1C,EAAA,MAAM,QAAA,GAAW,OAAwB,EAAE,WAAA,EAAY,CAAA;AACvD,EAAA,MAAM,OAAO,MAAY;AACvB,IAAA,MAAM,QAAQ,QAAA,EAAS;AACvB,IAAA,KAAA,MAAW,QAAA,IAAY,SAAA,EAAW,QAAA,CAAS,KAAK,CAAA;AAAA,EAClD,CAAA;AACA,EAAA,MAAM,SAAA,GAAY,CAAC,QAAA,KAA6D;AAC9E,IAAA,SAAA,CAAU,IAAI,QAAQ,CAAA;AACtB,IAAA,OAAO,MAAM,SAAA,CAAU,MAAA,CAAO,QAAQ,CAAA;AAAA,EACxC,CAAA;AAEA,EAAA,MAAM,SAAA,GACJ,OAAO,SAAA,KAAc,WAAA,IAAe,eAAA,IAAmB,SAAA;AAGzD,EAAA,IAAI,QAAA,IAAY,CAAC,SAAA,EAAW;AAC1B,IAAA,OAAO;AAAA,MACL,SAAA;AAAA,MACA,QAAA;AAAA,MACA,WAAA,GAAc;AAAA,MAAC,CAAA;AAAA,MACf,OAAA,GAAU;AACR,QAAA,SAAA,CAAU,KAAA,EAAM;AAAA,MAClB;AAAA,KACF;AAAA,EACF;AAEA,EAAA,MAAM,YAAY,SAAA,CAAU,aAAA;AAC5B,EAAA,IAAI,MAAA,GAAgD,IAAA;AACpD,EAAA,IAAI,oBAAA,GAA4C,IAAA;AAChD,EAAA,IAAI,SAAA,GAAY,KAAA;AAChB,EAAA,IAAI,SAAA,GAAY,KAAA;AAEhB,EAAA,MAAM,SAAA,GAAY,CAAC,MAAA,KAAgC;AACjD,IAAA,aAAA,GAAgB,MAAA;AAChB,IAAA,IAAI,CAAC,WAAA,EAAa;AAChB,MAAA,WAAA,GAAc,IAAA;AACd,MAAA,IAAA,EAAK;AAAA,IACP;AAAA,EACF,CAAA;AAEA,EAAA,MAAM,qBAAqB,MAAY;AACrC,IAAA,IAAI,CAAC,4BAA4B,SAAA,EAAW;AAC5C,IAAA,SAAA,GAAY,IAAA;AACZ,IAAA,IAAI,OAAO,QAAA,KAAa,WAAA,EAAa,QAAA,CAAS,MAAA,EAAO;AAAA,EACvD,CAAA;AACA,EAAA,SAAA,CAAU,gBAAA,CAAiB,oBAAoB,kBAAkB,CAAA;AAEjE,EAAA,MAAM,eAAA,GAAkB,CAAC,YAAA,KAAkD;AACzE,IAAA,MAAM,aAAa,YAAA,CAAa,UAAA;AAChC,IAAA,IAAI,CAAC,UAAA,EAAY;AACjB,IAAA,UAAA,CAAW,gBAAA,CAAiB,eAAe,MAAM;AAI/C,MAAA,IAAI,UAAA,CAAW,KAAA,KAAU,WAAA,IAAe,SAAA,CAAU,UAAA,EAAY;AAC5D,QAAA,SAAA,CAAU,UAAU,CAAA;AAAA,MACtB;AAAA,IACF,CAAC,CAAA;AAAA,EACH,CAAA;AAEA,EAAA,MAAM,eAAA,GAAkB,CAAC,YAAA,KAAkD;AACzE,IAAA,IAAI,CAAC,aAAA,EAAe;AACpB,IAAA,MAAM,QAAQ,MAAY;AACxB,MAAA,YAAA,CAAa,MAAA,EAAO,CAAE,KAAA,CAAM,MAAM;AAAA,MAAC,CAAC,CAAA;AAAA,IACtC,CAAA;AACA,IAAA,MAAM,OAAA,GAAU,MAAY,KAAA,EAAM;AAClC,IAAA,MAAM,eAAe,MAAY;AAC/B,MAAA,IAAI,OAAO,QAAA,KAAa,WAAA,IAAe,QAAA,CAAS,oBAAoB,SAAA,EAAW;AAC7E,QAAA,KAAA,EAAM;AAAA,MACR;AAAA,IACF,CAAA;AACA,IAAA,IAAI,OAAO,MAAA,KAAW,WAAA,EAAa,MAAA,CAAO,gBAAA,CAAiB,SAAS,OAAO,CAAA;AAC3E,IAAA,IAAI,OAAO,aAAa,WAAA,EAAa;AACnC,MAAA,QAAA,CAAS,gBAAA,CAAiB,oBAAoB,YAAY,CAAA;AAAA,IAC5D;AACA,IAAA,oBAAA,GAAuB,MAAM;AAC3B,MAAA,IAAI,OAAO,MAAA,KAAW,WAAA,EAAa,MAAA,CAAO,mBAAA,CAAoB,SAAS,OAAO,CAAA;AAC9E,MAAA,IAAI,OAAO,aAAa,WAAA,EAAa;AACnC,QAAA,QAAA,CAAS,mBAAA,CAAoB,oBAAoB,YAAY,CAAA;AAAA,MAC/D;AAAA,IACF,CAAA;AAAA,EACF,CAAA;AAIA,EAAA,MAAM,sBAAsB,QAAA,GAAW,SAAA,CAAU,QAAA,CAAS,KAAK,IAAI,SAAA,CAAU,KAAA;AAE7E,EAAA,mBAAA,CACG,IAAA,CAAK,CAAC,YAAA,KAAiB;AACtB,IAAA,IAAI,SAAA,EAAW;AACf,IAAA,IAAI,YAAA,CAAa,OAAA,EAAS,SAAA,CAAU,YAAA,CAAa,OAAO,CAAA;AACxD,IAAA,YAAA,CAAa,gBAAA,CAAiB,aAAA,EAAe,MAAM,eAAA,CAAgB,YAAY,CAAC,CAAA;AAChF,IAAA,IAAI,iBAAiB,CAAA,EAAG;AACtB,MAAA,MAAA,GAAS,YAAY,MAAM;AACzB,QAAA,YAAA,CAAa,MAAA,EAAO,CAAE,KAAA,CAAM,MAAM;AAAA,QAElC,CAAC,CAAA;AAAA,MACH,GAAG,cAAc,CAAA;AAAA,IACnB;AACA,IAAA,eAAA,CAAgB,YAAY,CAAA;AAAA,EAC9B,CAAC,CAAA,CACA,KAAA,CAAM,MAAM;AAAA,EAEb,CAAC,CAAA;AAEH,EAAA,OAAO;AAAA,IACL,SAAA;AAAA,IACA,QAAA;AAAA,IACA,WAAA,GAAc;AACZ,MAAA,IAAI,aAAA,EAAe,aAAA,CAAc,WAAA,CAAY,oBAAoB,CAAA;AAAA,IACnE,CAAA;AAAA,IACA,OAAA,GAAU;AACR,MAAA,SAAA,GAAY,IAAA;AACZ,MAAA,IAAI,MAAA,KAAW,IAAA,EAAM,aAAA,CAAc,MAAM,CAAA;AACzC,MAAA,oBAAA,IAAuB;AACvB,MAAA,SAAA,CAAU,mBAAA,CAAoB,oBAAoB,kBAAkB,CAAA;AACpE,MAAA,SAAA,CAAU,KAAA,EAAM;AAAA,IAClB;AAAA,GACF;AACF","file":"index.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 const onControllerChange = (): void => {\n if (!reloadOnControllerChange || reloading) return;\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"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export { S as SKIP_WAITING, a as SKIP_WAITING_MESSAGE, b as SkipWaitingMessage } from './message-CBBmtYuB.cjs';
|
|
2
|
+
|
|
3
|
+
interface PwaUpdaterOptions {
|
|
4
|
+
/** Service-worker script URL to register. Default `/sw.js`. */
|
|
5
|
+
swUrl?: string;
|
|
6
|
+
/**
|
|
7
|
+
* Register `swUrl` ourselves (manual `sw.js` style). Set `false` when the
|
|
8
|
+
* app already registers the SW (e.g. Serwist's `registerSW` / `@serwist/next`)
|
|
9
|
+
* — the updater then attaches to the existing registration via
|
|
10
|
+
* `navigator.serviceWorker.ready` instead of registering again. Default `true`.
|
|
11
|
+
*/
|
|
12
|
+
register?: boolean;
|
|
13
|
+
/** How often to poll `registration.update()` (ms). `0` disables. Default 60 min. */
|
|
14
|
+
pollIntervalMs?: number;
|
|
15
|
+
/**
|
|
16
|
+
* Also check for an update when the tab regains focus / becomes visible.
|
|
17
|
+
* A constantly-focused tab never fires `visibilitychange`, so `focus` is the
|
|
18
|
+
* one that catches "user came back to the app". Default `true`.
|
|
19
|
+
*/
|
|
20
|
+
updateOnFocus?: boolean;
|
|
21
|
+
/** Reload the page once the new worker takes control. Default `true`. */
|
|
22
|
+
reloadOnControllerChange?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Consumer guard. When `true` the updater is an inert no-op (registers
|
|
25
|
+
* nothing). Pass your own policy, e.g. `disabled: isNativeCapacitor || isDev`
|
|
26
|
+
* — the package never hardcodes an environment or a `.native` check.
|
|
27
|
+
*/
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
}
|
|
30
|
+
interface PwaUpdaterState {
|
|
31
|
+
/** A new service worker is installed and waiting to activate. */
|
|
32
|
+
updateReady: boolean;
|
|
33
|
+
}
|
|
34
|
+
interface PwaUpdater {
|
|
35
|
+
/** Subscribe to state changes. Returns an unsubscribe fn. */
|
|
36
|
+
subscribe(listener: (state: PwaUpdaterState) => void): () => void;
|
|
37
|
+
getState(): PwaUpdaterState;
|
|
38
|
+
/** Tell the waiting worker to activate (posts SKIP_WAITING). No-op if none waits. */
|
|
39
|
+
applyUpdate(): void;
|
|
40
|
+
/** Stop polling + remove all listeners. */
|
|
41
|
+
destroy(): void;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Framework- and bundler-agnostic controller for the PWA "new version
|
|
45
|
+
* available" lifecycle: register (or attach) → detect a waiting worker
|
|
46
|
+
* (suppressing the first install) → let the consumer activate it → reload on
|
|
47
|
+
* takeover, re-checking on interval + focus/visibility.
|
|
48
|
+
*
|
|
49
|
+
* Distilled from the pattern hand-rolled across the fleet (fds, cardmem,
|
|
50
|
+
* pitch-vault). Works with any service worker (Serwist, Workbox, hand-rolled)
|
|
51
|
+
* as long as the SW answers a `SKIP_WAITING` message — see `@broberg/pwa/sw`.
|
|
52
|
+
*/
|
|
53
|
+
declare function createPwaUpdater(options?: PwaUpdaterOptions): PwaUpdater;
|
|
54
|
+
|
|
55
|
+
export { type PwaUpdater, type PwaUpdaterOptions, type PwaUpdaterState, createPwaUpdater };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export { S as SKIP_WAITING, a as SKIP_WAITING_MESSAGE, b as SkipWaitingMessage } from './message-CBBmtYuB.js';
|
|
2
|
+
|
|
3
|
+
interface PwaUpdaterOptions {
|
|
4
|
+
/** Service-worker script URL to register. Default `/sw.js`. */
|
|
5
|
+
swUrl?: string;
|
|
6
|
+
/**
|
|
7
|
+
* Register `swUrl` ourselves (manual `sw.js` style). Set `false` when the
|
|
8
|
+
* app already registers the SW (e.g. Serwist's `registerSW` / `@serwist/next`)
|
|
9
|
+
* — the updater then attaches to the existing registration via
|
|
10
|
+
* `navigator.serviceWorker.ready` instead of registering again. Default `true`.
|
|
11
|
+
*/
|
|
12
|
+
register?: boolean;
|
|
13
|
+
/** How often to poll `registration.update()` (ms). `0` disables. Default 60 min. */
|
|
14
|
+
pollIntervalMs?: number;
|
|
15
|
+
/**
|
|
16
|
+
* Also check for an update when the tab regains focus / becomes visible.
|
|
17
|
+
* A constantly-focused tab never fires `visibilitychange`, so `focus` is the
|
|
18
|
+
* one that catches "user came back to the app". Default `true`.
|
|
19
|
+
*/
|
|
20
|
+
updateOnFocus?: boolean;
|
|
21
|
+
/** Reload the page once the new worker takes control. Default `true`. */
|
|
22
|
+
reloadOnControllerChange?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Consumer guard. When `true` the updater is an inert no-op (registers
|
|
25
|
+
* nothing). Pass your own policy, e.g. `disabled: isNativeCapacitor || isDev`
|
|
26
|
+
* — the package never hardcodes an environment or a `.native` check.
|
|
27
|
+
*/
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
}
|
|
30
|
+
interface PwaUpdaterState {
|
|
31
|
+
/** A new service worker is installed and waiting to activate. */
|
|
32
|
+
updateReady: boolean;
|
|
33
|
+
}
|
|
34
|
+
interface PwaUpdater {
|
|
35
|
+
/** Subscribe to state changes. Returns an unsubscribe fn. */
|
|
36
|
+
subscribe(listener: (state: PwaUpdaterState) => void): () => void;
|
|
37
|
+
getState(): PwaUpdaterState;
|
|
38
|
+
/** Tell the waiting worker to activate (posts SKIP_WAITING). No-op if none waits. */
|
|
39
|
+
applyUpdate(): void;
|
|
40
|
+
/** Stop polling + remove all listeners. */
|
|
41
|
+
destroy(): void;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Framework- and bundler-agnostic controller for the PWA "new version
|
|
45
|
+
* available" lifecycle: register (or attach) → detect a waiting worker
|
|
46
|
+
* (suppressing the first install) → let the consumer activate it → reload on
|
|
47
|
+
* takeover, re-checking on interval + focus/visibility.
|
|
48
|
+
*
|
|
49
|
+
* Distilled from the pattern hand-rolled across the fleet (fds, cardmem,
|
|
50
|
+
* pitch-vault). Works with any service worker (Serwist, Workbox, hand-rolled)
|
|
51
|
+
* as long as the SW answers a `SKIP_WAITING` message — see `@broberg/pwa/sw`.
|
|
52
|
+
*/
|
|
53
|
+
declare function createPwaUpdater(options?: PwaUpdaterOptions): PwaUpdater;
|
|
54
|
+
|
|
55
|
+
export { type PwaUpdater, type PwaUpdaterOptions, type PwaUpdaterState, createPwaUpdater };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { SKIP_WAITING_MESSAGE } from './chunk-ERNF6MZL.js';
|
|
2
|
+
export { SKIP_WAITING, SKIP_WAITING_MESSAGE } from './chunk-ERNF6MZL.js';
|
|
3
|
+
|
|
4
|
+
// src/index.ts
|
|
5
|
+
var DEFAULT_POLL_MS = 60 * 60 * 1e3;
|
|
6
|
+
function createPwaUpdater(options = {}) {
|
|
7
|
+
const {
|
|
8
|
+
swUrl = "/sw.js",
|
|
9
|
+
register = true,
|
|
10
|
+
pollIntervalMs = DEFAULT_POLL_MS,
|
|
11
|
+
updateOnFocus = true,
|
|
12
|
+
reloadOnControllerChange = true,
|
|
13
|
+
disabled = false
|
|
14
|
+
} = options;
|
|
15
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
16
|
+
let updateReady = false;
|
|
17
|
+
let waitingWorker = null;
|
|
18
|
+
const getState = () => ({ updateReady });
|
|
19
|
+
const emit = () => {
|
|
20
|
+
const state = getState();
|
|
21
|
+
for (const listener of listeners) listener(state);
|
|
22
|
+
};
|
|
23
|
+
const subscribe = (listener) => {
|
|
24
|
+
listeners.add(listener);
|
|
25
|
+
return () => listeners.delete(listener);
|
|
26
|
+
};
|
|
27
|
+
const supported = typeof navigator !== "undefined" && "serviceWorker" in navigator;
|
|
28
|
+
if (disabled || !supported) {
|
|
29
|
+
return {
|
|
30
|
+
subscribe,
|
|
31
|
+
getState,
|
|
32
|
+
applyUpdate() {
|
|
33
|
+
},
|
|
34
|
+
destroy() {
|
|
35
|
+
listeners.clear();
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const container = navigator.serviceWorker;
|
|
40
|
+
let pollId = null;
|
|
41
|
+
let removeFocusListeners = null;
|
|
42
|
+
let reloading = false;
|
|
43
|
+
let destroyed = false;
|
|
44
|
+
const markReady = (worker) => {
|
|
45
|
+
waitingWorker = worker;
|
|
46
|
+
if (!updateReady) {
|
|
47
|
+
updateReady = true;
|
|
48
|
+
emit();
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const onControllerChange = () => {
|
|
52
|
+
if (!reloadOnControllerChange || reloading) return;
|
|
53
|
+
reloading = true;
|
|
54
|
+
if (typeof location !== "undefined") location.reload();
|
|
55
|
+
};
|
|
56
|
+
container.addEventListener("controllerchange", onControllerChange);
|
|
57
|
+
const watchInstalling = (registration) => {
|
|
58
|
+
const installing = registration.installing;
|
|
59
|
+
if (!installing) return;
|
|
60
|
+
installing.addEventListener("statechange", () => {
|
|
61
|
+
if (installing.state === "installed" && container.controller) {
|
|
62
|
+
markReady(installing);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
const wireFocusChecks = (registration) => {
|
|
67
|
+
if (!updateOnFocus) return;
|
|
68
|
+
const check = () => {
|
|
69
|
+
registration.update().catch(() => {
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
const onFocus = () => check();
|
|
73
|
+
const onVisibility = () => {
|
|
74
|
+
if (typeof document !== "undefined" && document.visibilityState === "visible") {
|
|
75
|
+
check();
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
if (typeof window !== "undefined") window.addEventListener("focus", onFocus);
|
|
79
|
+
if (typeof document !== "undefined") {
|
|
80
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
81
|
+
}
|
|
82
|
+
removeFocusListeners = () => {
|
|
83
|
+
if (typeof window !== "undefined") window.removeEventListener("focus", onFocus);
|
|
84
|
+
if (typeof document !== "undefined") {
|
|
85
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
const registrationPromise = register ? container.register(swUrl) : container.ready;
|
|
90
|
+
registrationPromise.then((registration) => {
|
|
91
|
+
if (destroyed) return;
|
|
92
|
+
if (registration.waiting) markReady(registration.waiting);
|
|
93
|
+
registration.addEventListener("updatefound", () => watchInstalling(registration));
|
|
94
|
+
if (pollIntervalMs > 0) {
|
|
95
|
+
pollId = setInterval(() => {
|
|
96
|
+
registration.update().catch(() => {
|
|
97
|
+
});
|
|
98
|
+
}, pollIntervalMs);
|
|
99
|
+
}
|
|
100
|
+
wireFocusChecks(registration);
|
|
101
|
+
}).catch(() => {
|
|
102
|
+
});
|
|
103
|
+
return {
|
|
104
|
+
subscribe,
|
|
105
|
+
getState,
|
|
106
|
+
applyUpdate() {
|
|
107
|
+
if (waitingWorker) waitingWorker.postMessage(SKIP_WAITING_MESSAGE);
|
|
108
|
+
},
|
|
109
|
+
destroy() {
|
|
110
|
+
destroyed = true;
|
|
111
|
+
if (pollId !== null) clearInterval(pollId);
|
|
112
|
+
removeFocusListeners?.();
|
|
113
|
+
container.removeEventListener("controllerchange", onControllerChange);
|
|
114
|
+
listeners.clear();
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export { createPwaUpdater };
|
|
120
|
+
//# sourceMappingURL=index.js.map
|
|
121
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";;;;AAgDA,IAAM,eAAA,GAAkB,KAAK,EAAA,GAAK,GAAA;AAY3B,SAAS,gBAAA,CAAiB,OAAA,GAA6B,EAAC,EAAe;AAC5E,EAAA,MAAM;AAAA,IACJ,KAAA,GAAQ,QAAA;AAAA,IACR,QAAA,GAAW,IAAA;AAAA,IACX,cAAA,GAAiB,eAAA;AAAA,IACjB,aAAA,GAAgB,IAAA;AAAA,IAChB,wBAAA,GAA2B,IAAA;AAAA,IAC3B,QAAA,GAAW;AAAA,GACb,GAAI,OAAA;AAEJ,EAAA,MAAM,SAAA,uBAAgB,GAAA,EAAsC;AAC5D,EAAA,IAAI,WAAA,GAAc,KAAA;AAClB,EAAA,IAAI,aAAA,GAAsC,IAAA;AAE1C,EAAA,MAAM,QAAA,GAAW,OAAwB,EAAE,WAAA,EAAY,CAAA;AACvD,EAAA,MAAM,OAAO,MAAY;AACvB,IAAA,MAAM,QAAQ,QAAA,EAAS;AACvB,IAAA,KAAA,MAAW,QAAA,IAAY,SAAA,EAAW,QAAA,CAAS,KAAK,CAAA;AAAA,EAClD,CAAA;AACA,EAAA,MAAM,SAAA,GAAY,CAAC,QAAA,KAA6D;AAC9E,IAAA,SAAA,CAAU,IAAI,QAAQ,CAAA;AACtB,IAAA,OAAO,MAAM,SAAA,CAAU,MAAA,CAAO,QAAQ,CAAA;AAAA,EACxC,CAAA;AAEA,EAAA,MAAM,SAAA,GACJ,OAAO,SAAA,KAAc,WAAA,IAAe,eAAA,IAAmB,SAAA;AAGzD,EAAA,IAAI,QAAA,IAAY,CAAC,SAAA,EAAW;AAC1B,IAAA,OAAO;AAAA,MACL,SAAA;AAAA,MACA,QAAA;AAAA,MACA,WAAA,GAAc;AAAA,MAAC,CAAA;AAAA,MACf,OAAA,GAAU;AACR,QAAA,SAAA,CAAU,KAAA,EAAM;AAAA,MAClB;AAAA,KACF;AAAA,EACF;AAEA,EAAA,MAAM,YAAY,SAAA,CAAU,aAAA;AAC5B,EAAA,IAAI,MAAA,GAAgD,IAAA;AACpD,EAAA,IAAI,oBAAA,GAA4C,IAAA;AAChD,EAAA,IAAI,SAAA,GAAY,KAAA;AAChB,EAAA,IAAI,SAAA,GAAY,KAAA;AAEhB,EAAA,MAAM,SAAA,GAAY,CAAC,MAAA,KAAgC;AACjD,IAAA,aAAA,GAAgB,MAAA;AAChB,IAAA,IAAI,CAAC,WAAA,EAAa;AAChB,MAAA,WAAA,GAAc,IAAA;AACd,MAAA,IAAA,EAAK;AAAA,IACP;AAAA,EACF,CAAA;AAEA,EAAA,MAAM,qBAAqB,MAAY;AACrC,IAAA,IAAI,CAAC,4BAA4B,SAAA,EAAW;AAC5C,IAAA,SAAA,GAAY,IAAA;AACZ,IAAA,IAAI,OAAO,QAAA,KAAa,WAAA,EAAa,QAAA,CAAS,MAAA,EAAO;AAAA,EACvD,CAAA;AACA,EAAA,SAAA,CAAU,gBAAA,CAAiB,oBAAoB,kBAAkB,CAAA;AAEjE,EAAA,MAAM,eAAA,GAAkB,CAAC,YAAA,KAAkD;AACzE,IAAA,MAAM,aAAa,YAAA,CAAa,UAAA;AAChC,IAAA,IAAI,CAAC,UAAA,EAAY;AACjB,IAAA,UAAA,CAAW,gBAAA,CAAiB,eAAe,MAAM;AAI/C,MAAA,IAAI,UAAA,CAAW,KAAA,KAAU,WAAA,IAAe,SAAA,CAAU,UAAA,EAAY;AAC5D,QAAA,SAAA,CAAU,UAAU,CAAA;AAAA,MACtB;AAAA,IACF,CAAC,CAAA;AAAA,EACH,CAAA;AAEA,EAAA,MAAM,eAAA,GAAkB,CAAC,YAAA,KAAkD;AACzE,IAAA,IAAI,CAAC,aAAA,EAAe;AACpB,IAAA,MAAM,QAAQ,MAAY;AACxB,MAAA,YAAA,CAAa,MAAA,EAAO,CAAE,KAAA,CAAM,MAAM;AAAA,MAAC,CAAC,CAAA;AAAA,IACtC,CAAA;AACA,IAAA,MAAM,OAAA,GAAU,MAAY,KAAA,EAAM;AAClC,IAAA,MAAM,eAAe,MAAY;AAC/B,MAAA,IAAI,OAAO,QAAA,KAAa,WAAA,IAAe,QAAA,CAAS,oBAAoB,SAAA,EAAW;AAC7E,QAAA,KAAA,EAAM;AAAA,MACR;AAAA,IACF,CAAA;AACA,IAAA,IAAI,OAAO,MAAA,KAAW,WAAA,EAAa,MAAA,CAAO,gBAAA,CAAiB,SAAS,OAAO,CAAA;AAC3E,IAAA,IAAI,OAAO,aAAa,WAAA,EAAa;AACnC,MAAA,QAAA,CAAS,gBAAA,CAAiB,oBAAoB,YAAY,CAAA;AAAA,IAC5D;AACA,IAAA,oBAAA,GAAuB,MAAM;AAC3B,MAAA,IAAI,OAAO,MAAA,KAAW,WAAA,EAAa,MAAA,CAAO,mBAAA,CAAoB,SAAS,OAAO,CAAA;AAC9E,MAAA,IAAI,OAAO,aAAa,WAAA,EAAa;AACnC,QAAA,QAAA,CAAS,mBAAA,CAAoB,oBAAoB,YAAY,CAAA;AAAA,MAC/D;AAAA,IACF,CAAA;AAAA,EACF,CAAA;AAIA,EAAA,MAAM,sBAAsB,QAAA,GAAW,SAAA,CAAU,QAAA,CAAS,KAAK,IAAI,SAAA,CAAU,KAAA;AAE7E,EAAA,mBAAA,CACG,IAAA,CAAK,CAAC,YAAA,KAAiB;AACtB,IAAA,IAAI,SAAA,EAAW;AACf,IAAA,IAAI,YAAA,CAAa,OAAA,EAAS,SAAA,CAAU,YAAA,CAAa,OAAO,CAAA;AACxD,IAAA,YAAA,CAAa,gBAAA,CAAiB,aAAA,EAAe,MAAM,eAAA,CAAgB,YAAY,CAAC,CAAA;AAChF,IAAA,IAAI,iBAAiB,CAAA,EAAG;AACtB,MAAA,MAAA,GAAS,YAAY,MAAM;AACzB,QAAA,YAAA,CAAa,MAAA,EAAO,CAAE,KAAA,CAAM,MAAM;AAAA,QAElC,CAAC,CAAA;AAAA,MACH,GAAG,cAAc,CAAA;AAAA,IACnB;AACA,IAAA,eAAA,CAAgB,YAAY,CAAA;AAAA,EAC9B,CAAC,CAAA,CACA,KAAA,CAAM,MAAM;AAAA,EAEb,CAAC,CAAA;AAEH,EAAA,OAAO;AAAA,IACL,SAAA;AAAA,IACA,QAAA;AAAA,IACA,WAAA,GAAc;AACZ,MAAA,IAAI,aAAA,EAAe,aAAA,CAAc,WAAA,CAAY,oBAAoB,CAAA;AAAA,IACnE,CAAA;AAAA,IACA,OAAA,GAAU;AACR,MAAA,SAAA,GAAY,IAAA;AACZ,MAAA,IAAI,MAAA,KAAW,IAAA,EAAM,aAAA,CAAc,MAAM,CAAA;AACzC,MAAA,oBAAA,IAAuB;AACvB,MAAA,SAAA,CAAU,mBAAA,CAAoB,oBAAoB,kBAAkB,CAAA;AACpE,MAAA,SAAA,CAAU,KAAA,EAAM;AAAA,IAClB;AAAA,GACF;AACF","file":"index.js","sourcesContent":["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 const onControllerChange = (): void => {\n if (!reloadOnControllerChange || reloading) return;\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"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single source of the client↔service-worker "activate the waiting worker"
|
|
3
|
+
* message. Both the client controller (`applyUpdate`) and the SW helper
|
|
4
|
+
* (`listenForSkipWaiting`) import it so the contract can never drift.
|
|
5
|
+
*/
|
|
6
|
+
declare const SKIP_WAITING: "SKIP_WAITING";
|
|
7
|
+
declare const SKIP_WAITING_MESSAGE: {
|
|
8
|
+
readonly type: "SKIP_WAITING";
|
|
9
|
+
};
|
|
10
|
+
type SkipWaitingMessage = typeof SKIP_WAITING_MESSAGE;
|
|
11
|
+
|
|
12
|
+
export { SKIP_WAITING as S, SKIP_WAITING_MESSAGE as a, type SkipWaitingMessage as b };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single source of the client↔service-worker "activate the waiting worker"
|
|
3
|
+
* message. Both the client controller (`applyUpdate`) and the SW helper
|
|
4
|
+
* (`listenForSkipWaiting`) import it so the contract can never drift.
|
|
5
|
+
*/
|
|
6
|
+
declare const SKIP_WAITING: "SKIP_WAITING";
|
|
7
|
+
declare const SKIP_WAITING_MESSAGE: {
|
|
8
|
+
readonly type: "SKIP_WAITING";
|
|
9
|
+
};
|
|
10
|
+
type SkipWaitingMessage = typeof SKIP_WAITING_MESSAGE;
|
|
11
|
+
|
|
12
|
+
export { SKIP_WAITING as S, SKIP_WAITING_MESSAGE as a, type SkipWaitingMessage as b };
|