first commit
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
# `update-controller/update-controller.ts`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`UpdateController` coordinates a synchronised page reload across every tab the
|
||||
application is open in when a new service worker activates. It opens a single
|
||||
`BroadcastChannel`, listens for a typed `{ type: "RELOAD" }` envelope, and
|
||||
invokes `window.location.reload()` on receipt. The controller solves the
|
||||
"tab A is already running the new SW while tabs B/C are still serving the old
|
||||
one" problem that bites every multi-tab PWA without explicit cross-tab
|
||||
coordination.
|
||||
|
||||
## Features
|
||||
|
||||
- Single `BroadcastChannel` per controller instance, named
|
||||
`@suaveplan/pwa/update` by default.
|
||||
- Typed message envelope (`UpdateMessage`) so consumers cannot accidentally
|
||||
broadcast unrelated payloads on the same channel.
|
||||
- Injectable `BroadcastChannelLike` factory + `WindowLike` seam — production
|
||||
uses the platform `BroadcastChannel` + `window`, tests inject a shared
|
||||
in-memory hub that lets multiple controllers observe each other's messages.
|
||||
- `close()` removes listeners and closes the channel so the controller can be
|
||||
discarded after activation, preventing leaks in single-page apps.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { UpdateController } from "@suaveplan/pwa";
|
||||
|
||||
const updates = new UpdateController();
|
||||
swm.onStateChange((state) => {
|
||||
if (state === "activated") updates.signalReload();
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Test two controllers communicating through one shared in-memory channel hub:
|
||||
|
||||
```ts
|
||||
import { UpdateController } from "@suaveplan/pwa";
|
||||
|
||||
const hub = createSharedChannelHub();
|
||||
const reloaded = { a: 0, b: 0 };
|
||||
const a = new UpdateController({
|
||||
broadcastChannelFactory: (n) => hub.open(n),
|
||||
window: { location: { reload: () => reloaded.a++ } },
|
||||
});
|
||||
const b = new UpdateController({
|
||||
broadcastChannelFactory: (n) => hub.open(n),
|
||||
window: { location: { reload: () => reloaded.b++ } },
|
||||
});
|
||||
a.signalReload();
|
||||
// receivers run synchronously inside the hub
|
||||
expect(reloaded.b).toBe(1);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `new UpdateController(options?: UpdateControllerOptions)`.
|
||||
- `signalReload(): void`.
|
||||
- `close(): void`.
|
||||
- `UpdateMessage` — `{ type: "RELOAD" }`.
|
||||
- `BroadcastChannelLike`, `BroadcastChannelFactory`, `UpdateWindowLike` — seams.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The controller does not invoke `window.location.reload()` in response to its
|
||||
own broadcast — `BroadcastChannel` already excludes the sending context from
|
||||
receipt, so the originating tab is expected to reload via the SW's own
|
||||
`controllerchange` path. The injected `WindowLike` is optional so the
|
||||
controller can also be constructed inside a SW global scope (which has no
|
||||
`window`) for diagnostic purposes; in that mode reloads are no-ops.
|
||||
Reference in New Issue
Block a user