Files
skills/docs/foundation/data/pwa/src/update-controller/update-controller.md
2026-05-29 08:20:56 +01:00

2.7 KiB

update-controller/update-controller.ts

(no submodules)

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

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:

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.