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

3.2 KiB

service-worker/service-worker.ts

(no submodules)

Purpose

ServiceWorkerManager wraps the browser ServiceWorkerContainer API behind an injectable seam so production code reaches navigator.serviceWorker exactly once (via the constructor default) while tests pass an in-process fake container that drives every lifecycle transition deterministically. The manager handles registration, update polling, skipWaiting coordination, state-change emission, and OTel counter updates without any vi.stubGlobal shenanigans.

Features

  • Registration with scope, custom scriptUrl, onUpdate / onSuccess / onError callbacks, and configurable update-poll interval.
  • Lifecycle state machine — installing → installed → activating → activated → redundant plus unsupported for runtimes lacking ServiceWorkerContainer.
  • skipWaiting() posts { type: "SKIP_WAITING" } to the waiting worker.
  • unregister() clears the update timer and tears down the registration.
  • checkForUpdate() invokes registration.update() and returns success.
  • Typed errors via PWA_CODES.SW_UNSUPPORTED / PWA_CODES.SW_REGISTRATION_FAILED.
  • OTel counters for pwa_sw_installed_total, pwa_sw_activated_total, pwa_sw_updated_total.

Usage: Basic

import { ServiceWorkerManager } from "@suaveplan/pwa";

const swm = new ServiceWorkerManager();
const result = await swm.register({ scriptUrl: "/sw.js" });
if (!result.success && result.error !== undefined) {
    console.error(result.error.code, result.error.message);
}

Usage: Advanced

Inject a fake container + interval scheduler for deterministic tests:

import { ServiceWorkerManager } from "@suaveplan/pwa";

const swm = new ServiceWorkerManager({
    container: fakeContainer,
    intervals: {
        setInterval: (cb, ms) => fakeTimers.schedule(cb, ms),
        clearInterval: (h) => fakeTimers.cancel(h),
    },
    observability: testObs,
});
await swm.register({ scriptUrl: "/sw.js", updateInterval: 100 });
fakeTimers.tick(100);
expect(fakeContainer.lastRegistration?.update).toHaveBeenCalled();

API Reference

  • new ServiceWorkerManager(options?: ServiceWorkerManagerOptions).
  • state: SwState — current lifecycle state (readonly).
  • registration: ServiceWorkerRegistration | null.
  • isSupported: boolean.
  • register(options): Promise<SwRegistrationResult>.
  • unregister(): Promise<boolean>.
  • checkForUpdate(): Promise<boolean>.
  • skipWaiting(): void.
  • onStateChange(listener): () => void.

Implementation Notes

The manager never reaches globalThis.navigator from the body of any method — this._container is captured in the constructor and threaded through every subsequent call so an injected fake completely replaces the real container. The interval scheduler is a separate seam so tests can advance time without hooking the global timer, which under happy-dom + Vitest's fork pool can race the next test file. Errors surfaced from container.register() are wrapped in a PwaError with the original thrown error preserved as cause, so callers can instanceof PwaError + error.code === "PWA.SW_REGISTRATION_FAILED" and still recover the underlying TypeError / SecurityError via error.cause.