-
Notifications
You must be signed in to change notification settings - Fork 0
The opt-in page half of double-meh's Service-Worker contract. It pairs with the double-meh-sw sibling (or any SW that speaks the same message contract): the page library keeps the semantics — inspectors, errors, options, promises — while a controlling SW amplifies with what only it can do: a cache tier shared across tabs and navigations, cross-tab wire coalescing, and bundling for pages that never heard of double-meh. Everything here is a performance adornment: with no SW present the app behaves identically.
import io from 'double-meh';
import {installSW} from 'double-meh/sw.js';
installSW(io);installSW(io, options?) registers the sw transport, announces the library to a controlling
SW, and maintains io.sw. Options: library (the name announced, default 'double-meh'),
serviceWorker (injectable container, default navigator.serviceWorker), and helloTimeout
(ms to wait for a hello reply, default 500).
When a controlling SW is detected, the module announces io:hello {library}. A contract-speaking
SW answers with its contract version, its own version, and its capabilities — and marks the page
as a library client: the SW passes that page's traffic through instead of bundling it itself
(client-wins — the page has the context: named bundles, explicit flushes). The announce repeats on
controllerchange, because a new controller starts with an empty client registry.
The negotiated state lives on io.sw:
io.sw.supported; // Service Workers exist in this environment
io.sw.connected; // a SW answered the handshake
io.sw.contract; // the contract version it answered with
io.sw.version; // the SW's own version string
io.sw.capabilities; // e.g. ['cache', 'coalesce', 'bundle', 'invalidate', 'version', 'transport']
await io.sw.hello(); // re-announce manually: the state on reply, null on timeout or no SWEvery connect and disconnect also emits the sw event:
io.on('sw', state => console.log(state.connected ? 'SW attached' : 'SW detached'));A hello that nobody answers (no SW, a foreign SW) resolves null after helloTimeout and leaves
io.sw.connected false — the app works exactly as before.
{transport: 'sw'} routes a request as a message to the SW, which fetches upstream and replies
with the transferred body — instead of going through the page's own fetch(). This is the
transport for the prefetch/adopt class, because it owns two things interception structurally
cannot:
-
Uncontrolled pages — it reaches the SW registration before the SW controls the page
(first visit included), where intercepting
fetch()cannot engage yet. - Navigation-surviving requests — the SW completes the fetch even if the page navigates away, and lands the result in the shared cache tier: fire on page A, adopt on page B.
// prefetch: the result lands in the page cache AND the SW's shared tier
await io.get('/api/dashboard', null, {transport: 'sw'});Constraints, by construction: request bodies do not cross the channel (writes throw FailedIO —
interactive traffic belongs on the default fetch transport), abort signals and
progress events do not cross either, and DevTools network attribution moves to the SW. Responses
re-mint from the transferred bytes through the normal pipeline — decode, cache, track, and
inspectors all apply.
The SW keeps its cache in the Cache API under the name io-shared (the contract's default).
Two page-side writers can feed it:
-
io.bundle.writeThrough = true— unpacked bundle parts are written through under their real URLs, so the SW serves them to unadornedfetch()calls and to other tabs. - The
swtransport — the SW itself stores every GET it fetches for the page.
A custom name (writeThrough: 'my-cache') must match the SW's configured cache tier; prefer the
default unless the SW side is reconfigured too. The page-side storage backend
cacheApiStorage is a different animal — it stores decoded envelopes under synthetic keys and
does not collide with the shared tier.
installChannel(io) closes the invalidation loop: invalidate /users/42 in one tab and every
tab drops it — with a service worker as the hub, or with a bare BroadcastChannel('io') when
there is none.
import io from 'double-meh';
import {installChannel} from 'double-meh/sw.js';
installChannel(io);
// this now evicts in every tab (and a controlling SW's shared tier):
await io.cache.remove('https://api.example.com/users/42');How a removal travels:
-
String patterns propagate — an exact URL or a trailing-
*prefix becomes a URL-prefix invalidation on the wire. A controlling SW receivesio:invalidate, evicts its shared tier, and — when connected — broadcastsio:invalidatedto every tab; with no connected SW the page broadcasts directly, so the cross-tab story works in the no-SW quadrant too. -
Key-space
RegExpand predicate removals stay local — they match stored keys, not URLs, and don't translate to the wire. So doesio.cache.clear(). - Incoming invalidations evict quietly — the local eviction covers all accept variants of the matched URLs and never re-broadcasts, so there are no loops. Note the receiving side treats an exact URL as a prefix: possible over-eviction is just a cache miss.
io.channel reports {name, active} and close() detaches everything (restores
io.cache.remove, stops listening, closes the channel). Options: name (the channel, default
'io') and serviceWorker (injectable, as in installSW). On CLIs the channel is unrefed —
it never holds the process open. installChannel composes with installSW but does not require
it.
-
cache —
removepatterns and invalidation semantics. -
bundle — client-side bundling and
writeThrough. -
Events — the
swevent rides the standard event surface. - Cookbook: deduping & prefetch — the prefetch/adopt recipes.
- The fetch transport — the default transport and custom registration.
Guides
Concepts
Services
Modules
- fetch transport
- keys & URLs
- helpers
- records
- sse
- storage backends
- compression encoders
- Service Worker integration
Cookbook