Hybrid SW + Client Model
Why Swoff splits responsibilities between service worker and client scopes.
Why a service worker + client hybrid?
Swoff splits responsibilities between two scopes:
Service worker (SW) scope — runs in a separate thread, survives page navigation, has access to Cache Storage API:
- Intercepts fetch requests and applies caching strategies
- Manages precached assets at install time
- Maintains persistent SSE/WebSocket connections for server push
- Handles push notification events
- Broadcasts invalidation across clients (cross-tab sync)
- Serves cached responses when offline
- Intelligently dispatches fallbacks: navigation requests get an HTML fallback chain (precache → runtime-html → per-route → global → 503), while subresources (JS, CSS, images, API) return a clean 502 — preventing the old behavior of serving HTML for non-HTML requests
SW generation
Swoff splits SW generation into two phases. The CLI reads swoff.config.json and produces supporting files in swoff/. The SW-specific template has config features baked in (strategies, auth, tags, push handlers) but still has two placeholders for build-time values: ASSETS_TO_CACHE, AUTO_SKIP_WAITING. A build step (run after your framework build) scans built assets, resolves the cache name, and writes the final SW to the output directory.
Versioning
SW updates are content-based. Every build produces a byte-different sw.js — if your built assets changed, the SW content changes, and the browser detects the update automatically. No version string to manage. Cache names are fixed: "precache", "swoff-runtime", "swoff-runtime-html". Additionally, every build appends a timestamp constant at the end of the SW. This constant is purely a byte-differ — it triggers browser update detection when the SW file changes. On activation, the SW compares this timestamp against an IndexedDB record (swoff-meta DB, cacheName key). On deploy mismatch, all three caches are deleted before the new SW takes over.
Client (window) scope — runs in the page, has access to DOM, IndexedDB, and framework adapters:
- Tracks mutation state per-operation
- Queues offline writes to IndexedDB (the SW could also write, but client-side queuing keeps mutation state accessible to UI components and avoids SW lifecycle complexity)
- Provides reactive hooks (
useSwoffFetch,useSwoffQueue) - Manages auth tokens in memory-only storage (never exposed to SW)
- Detects online/offline state and triggers mutation replay
The hybrid model exists because:
- Stale detection requires the SW to know about cached responses (SW scope)
- Background refresh requires the SW to initiate fetches without page involvement (SW scope)
- Mutation state tracking needs per-component reactivity (client scope)
- IndexedDB writes for offline queue need to survive page navigation (SW could work for reads, but client writes are simpler and more reliable)