A library for building modular, pluggable Service Workers.
Production-oriented: typed API, predictable plugin execution order, centralized error handling, built-in version/ping mechanisms, and ready-made activation scenarios let you use it safely in real-world frontend projects.
- Why this package?
- Installation
- Quick start
- Demo
- initServiceWorker(plugins, options)
- initServiceWorker options
- Plugins
- Plugin execution order
- Handler execution behaviour
- Primitives, presets, and ready-made service workers
- Developing a separate plugin package
- Plugins (ready-made)
- License
Service workers are powerful but easy to get wrong: many event handlers, error paths, race conditions, and browser quirks. Large frameworks help, but often bring their own routing model, strategy DSL, and a lot of concepts to keep in your head. This package focuses on a smaller surface: a typed plugin contract, predictable execution, and tools that stay close to the native Service Worker API.
- Plugins as building blocks — each plugin owns one concern (caching, auth, notifications, version checks).
- You compose a service worker from small pieces instead of one large script.
- Infrastructure code for events (
install,activate,fetch, …) lives in the library; your code focuses on behaviour.
- Plugins are sorted by
order(ascending, default0) before handlers are registered. - For
fetchthe chain is sequential: the first plugin that returns a non‑undefinedResponsewins. - For
pushall handlers run; for most other events (install,activate,message,sync,periodicsync, background fetch events and others) handlers run in parallel. - Event listeners are only registered when at least one plugin provides a handler for that event.
- One main concept:
Pluginwith optional hooks; no separate routing language or strategy objects. - Few moving parts: plugin, plugin factory,
initServiceWorker, options. - The
ServiceWorkerPlugintype acts as an executable contract and documentation at the same time.
- Minimal runtime with no bundled build system or large dependencies.
- Only the code you import is included in your bundle.
- Suitable for projects where bundle size and dependency graph are tightly controlled.
- You decide what to cache and how to update it.
- Plugin order, logging, and error handling are configured explicitly.
- If you need a non‑standard behaviour, you implement it directly in a plugin instead of working around a framework.
- A single
onErrorhook receives structured information about where and what failed. - Errors in one plugin do not break others; errors from global Service Worker events are handled in one place.
- Error types are typed so you can react differently to installation, activation, fetch, or background fetch failures.
- Pluggable logger with levels (
trace,debug,info,warn,error). - The same logger instance is passed into every plugin hook, which makes it easier to correlate logs across events.
- You can use your own logging infrastructure as long as it matches the expected interface.
- A set of ready‑to‑use plugins:
precache,cacheFirst,networkFirst,staleWhileRevalidate,skipWaiting,claim, and others. offlineFirstpreset that combines precache on install with cache‑first fetch behaviour.- Ready‑made service worker entry points:
activateOnSignal,activateImmediately,activateOnNextVisit. - Client utilities for registration, update detection, messaging, health checks, and Background Fetch — with a focus on predictable behaviour and minimal boilerplate.
npm install @budarin/pluggable-serviceworkeror
pnpm add @budarin/pluggable-serviceworker// precacheAndServePlugin.js
import type { Plugin } from '@budarin/pluggable-serviceworker';
export function precacheAndServePlugin(config: {
cacheName: string;
assets: string[];
}): Plugin {
const { cacheName, assets } = config;
return {
name: 'precache-and-serve',
install: async (_event, logger) => {
const cache = await caches.open(cacheName);
await cache.addAll(assets);
},
fetch: async (event, logger) => {
const cache = await caches.open(cacheName);
const asset = await cache.match(event.request);
if (!asset) {
logger.debug(
`precache-and-serve: asset ${event.request.url} not found in cache!`
);
}
return asset ?? undefined;
},
};
}// sw.ts
import { precacheAndServePlugin } from './precacheAndServePlugin';
import { initServiceWorker } from '@budarin/pluggable-serviceworker';
initServiceWorker(
[
precacheAndServePlugin({
cacheName: 'my-cache-v1',
assets: ['/', '/styles.css', '/script.js'],
}),
],
{
version: '1.8.0',
}
);The demo/ folder contains a React + Vite app with the offlineFirst preset and activateOnSignal SW. From repo root: pnpm start. See demo/README.md.
initServiceWorker is the entry point: it registers Service Worker event handlers (install, activate, fetch, …) and runs them through the plugin list. Only events that have at least one plugin handler are registered — if no plugin implements e.g. sync, the service worker will not listen for sync events.
plugins— array of plugin objects. Plugins with config come from factory calls at the call site (see "Plugin factory"). Entries that arenullorundefined(e.g. when a factory returnsundefinedbecause an API is unavailable) are ignored; no need to filter the array yourself.options— at leastversion(required), and optionalpingPath?,logger?,onError?. The logger (from options orconsole) is passed as the second argument to plugin handlers.
Example:
initServiceWorker(
[
precache({ cacheName: 'v1', assets: ['/'] }),
serveFromCache({ cacheName: 'v1' }),
],
{
version: '1.8.0',
logger: customLogger,
onError: handleError,
}
);The second parameter options is of type ServiceWorkerInitOptions: required version and optional pingPath?, logger?, onError?. Only logger is passed into plugin handlers (second argument); if omitted, console is used. onError is used only by the library, not passed to plugins.
PluginContext in the API is for typing (it has logger?); plugins do not receive a richer context.
interface PluginContext {
logger?: Logger; // default: console
}
interface ServiceWorkerInitOptions extends PluginContext {
/** Service worker / app version string (e.g. '1.8.0'). */
version: string;
/** Optional path for ping requests (default '/sw-ping'). */
pingPath?: string;
onError?: (error, event, errorType?) => void; // library only, not passed to plugins
}Version string for the service worker / app. Used by:
- the library's internal plugin that answers version requests (
getServiceWorkerVersion()on the client); - logging and debugging (you can log it in your
onErroror logger).
Recommend using the same string as your frontend app version (e.g. from package.json).
Example:
initServiceWorker(plugins, {
version: '1.8.0',
});Overrides the ping path handled by the library's internal ping plugin. Default is '/sw-ping' (constant SW_PING_PATH). This must match what you use on the client in pingServiceWorker({ path: ... }) if you change it.
Examples:
// Default — internal plugin handles GET /sw-ping
initServiceWorker(plugins, {
version: '1.8.0',
});
// Custom ping path (e.g. to avoid clashing with backend)
initServiceWorker(plugins, {
version: '1.8.0',
pingPath: '/internal/sw-ping',
});Logger object with info, warn, error, debug. Default is console. Any object implementing the Logger interface is accepted.
interface Logger {
trace: (...data: unknown[]) => void;
debug: (...data: unknown[]) => void;
info: (...data: unknown[]) => void;
warn: (...data: unknown[]) => void;
error: (...data: unknown[]) => void;
}Example:
const options = {
logger: customLogger,
// or
logger: {
trace: (...data) => customLogger('TRACE', ...data),
debug: (...data) => customLogger('DEBUG', ...data),
info: (...data) => customLogger('INFO', ...data),
warn: (...data) => customLogger('WARN', ...data),
error: (...data) => customLogger('ERROR', ...data),
},
};Single handler for all error types in the Service Worker. There is no default handler — if onError is not provided, errors are not handled.
Parameters:
error: Error | any— error objectevent: Event— event where the error occurrederrorType?: ServiceWorkerErrorType— error type (see "Error handling")
Important: If onError is not set, plugin and global errors are not handled. For production, always set onError for logging and monitoring.
Examples:
// Minimal: version only
initServiceWorker([cachePlugin], {
version: '1.8.0',
});
// With onError
initServiceWorker([cachePlugin], {
version: '1.8.0',
onError: (error, event, errorType) => {
console.error('Service Worker error:', error, errorType);
},
});The library lets you define one handler for all error types and handle each type as needed. It subscribes to global error, messageerror, unhandledrejection, rejectionhandled; an error in one plugin does not stop others. If onError throws, the exception is logged via options.logger.
import {
initServiceWorker,
serviceWorkerErrorTypes,
} from '@budarin/pluggable-serviceworker';
const logger = console; // or your own logger
const options = {
version: '1.8.0',
logger,
onError: (error, event, errorType) => {
logger.info(`Error type "${errorType}":`, error);
switch (errorType) {
case serviceWorkerErrorTypes.INSTALL_ERROR:
case serviceWorkerErrorTypes.ACTIVATE_ERROR:
case serviceWorkerErrorTypes.FETCH_ERROR:
case serviceWorkerErrorTypes.MESSAGE_ERROR:
case serviceWorkerErrorTypes.SYNC_ERROR:
case serviceWorkerErrorTypes.PERIODICSYNC_ERROR:
case serviceWorkerErrorTypes.PUSH_ERROR:
case serviceWorkerErrorTypes.BACKGROUNDFETCHSUCCESS_ERROR:
case serviceWorkerErrorTypes.BACKGROUNDFETCHFAIL_ERROR:
case serviceWorkerErrorTypes.BACKGROUNDFETCHABORT_ERROR:
case serviceWorkerErrorTypes.BACKGROUNDFETCHCLICK_ERROR:
logger.error(`Plugin error (${errorType}):`, error);
if (error instanceof Error && error.stack) {
logger.error('Plugin error Stack:', error.stack);
}
break;
case serviceWorkerErrorTypes.ERROR:
logger.error('JavaScript error:', error);
break;
case serviceWorkerErrorTypes.MESSAGE_ERROR_HANDLER:
logger.error('Message error:', error);
break;
case serviceWorkerErrorTypes.UNHANDLED_REJECTION:
logger.error('Unhandled promise rejection:', error);
break;
case serviceWorkerErrorTypes.REJECTION_HANDLED:
logger.info('Promise rejection handled:', error);
break;
default:
logger.error('Unknown error type:', error);
fetch('/api/errors', {
method: 'POST',
body: JSON.stringify({
error: error.message,
eventType: event.type,
url: event.request?.url,
timestamp: Date.now(),
}),
}).catch(() => {});
}
},
};
initServiceWorker(
[
/* your plugins */
],
options
);A plugin is an object with a name and optional handlers (install, fetch, activate, etc.). You pass such objects into initServiceWorker(plugins, options).
A plugin factory is a function that takes config and returns a plugin (e.g. precache(config), serveFromCache(config), or your own precacheAndServePlugin(config)). Config is set at the call site.
A plugin implements ServiceWorkerPlugin. Plugin-specific config is set when calling the factory. The _C type parameter (e.g. PluginContext) is for typing; the default context only has logger.
interface ServiceWorkerPlugin<_C extends PluginContext = PluginContext> {
name: string;
order?: number;
install?: (event: ExtendableEvent, logger: Logger) => Promise<void> | void;
activate?: (event: ExtendableEvent, logger: Logger) => Promise<void> | void;
fetch?: (
event: FetchEvent,
logger: Logger
) => Promise<Response | undefined> | Response | undefined;
message?: (event: SwMessageEvent, logger: Logger) => void;
sync?: (event: SyncEvent, logger: Logger) => Promise<void> | void;
push?: (
event: PushEvent,
logger: Logger
) =>
| Promise<PushNotificationPayload | void>
| PushNotificationPayload
| void;
periodicsync?: (
event: PeriodicSyncEvent,
logger: Logger
) => Promise<void> | void;
backgroundfetchsuccess?: (
event: BackgroundFetchUpdateUIEvent,
logger: Logger
) => Promise<void> | void;
backgroundfetchfail?: (
event: BackgroundFetchUpdateUIEvent,
logger: Logger
) => Promise<void> | void;
backgroundfetchabort?: (
event: BackgroundFetchEvent,
logger: Logger
) => Promise<void> | void;
backgroundfetchclick?: (
event: BackgroundFetchEvent,
logger: Logger
) => Promise<void> | void;
}| Method | Event | Returns | Description |
|---|---|---|---|
install |
install |
void |
Plugin init on SW install |
activate |
activate |
void |
Plugin activation on SW update |
fetch |
fetch |
Response | undefined |
Handle network requests |
message |
message |
void |
Handle messages from main thread |
sync |
sync |
void |
Background sync |
push |
push |
PushNotificationPayload | false | undefined |
Handle and show push notification |
periodicsync |
periodicsync |
void |
Periodic background tasks |
backgroundfetchsuccess |
backgroundfetchsuccess |
void |
Background Fetch: all fetches succeeded |
backgroundfetchfail |
backgroundfetchfail |
void |
Background Fetch: at least one fetch failed |
backgroundfetchabort |
backgroundfetchabort |
void |
Background Fetch: fetch aborted by user or app |
backgroundfetchclick |
backgroundfetchclick |
void |
Background Fetch: user clicked download UI |
How the package works:
nullandundefinedentries in the plugins array are ignored (e.g. when a factory returnsundefinedwhen an API is unavailable). No need to filter manually- Arrays are created for each event type: install, activate, fetch, message, sync, periodicsync, push, backgroundfetchsuccess, backgroundfetchfail, backgroundfetchabort, backgroundfetchclick
- Plugins are sorted by
order(ascending, default 0) - In that order, each plugin's handlers are pushed into the corresponding arrays
- Only event types that have at least one handler get a listener —
addEventListeneris called only for those - Background Fetch: listeners for
backgroundfetchsuccess,backgroundfetchfail,backgroundfetchabort,backgroundfetchclickare registered only when the browser supports the API ('backgroundFetch' in self.registration). If plugins registered BF handlers but the API is not supported, a warning is logged. - When an event fires in the service worker, handlers from the matching array are run
- Every method receives
eventas the first argument and logger as the second. fetch: returnResponseto end the chain orundefinedto pass to the next plugin. If all returnundefined, the framework callsfetch(event.request).push: may returnPushNotificationPayload(for Notification API),false(do not show), orundefined(library decides). Allpushhandlers run. For eachPushNotificationPayloadresult,showNotificationis called (multiple notifications are shown in parallel). No notification if all returnfalseor onlyundefined/falsewithout payload. The library shows one notification only when all plugins returnundefined(and there is payload to show).- Other handlers (
install,activate,message,sync,periodicsync,backgroundfetchsuccess,backgroundfetchfail,backgroundfetchabort,backgroundfetchclick): return value is ignored; the framework calls each plugin's method in order; the chain does not short-circuit. - All handlers are optional — implement only the events you need. If no plugin implements a given event, that event is not listened for in the service worker.
Plugins are sorted by order (ascending). If order is not specified, it defaults to 0.
Important: Order matters for:
fetch— handlers run sequentially; first plugin that returns aResponsestops the chainpush— handlers run sequentially
For other events (install, activate, message, sync, periodicsync, backgroundfetchsuccess, backgroundfetchfail, backgroundfetchabort, backgroundfetchclick), handlers run in parallel, so order is mainly for organizing your configuration.
import {
precache,
serveFromCache,
cacheFirst,
} from '@budarin/pluggable-serviceworker/plugins';
initServiceWorker(
[
precache({
cacheName: 'v1',
assets: ['/'],
order: -10, // Early
}),
serveFromCache({
cacheName: 'v1', // order defaults to 0
}),
cacheFirst({
cacheName: 'api',
order: 100, // Late
}),
],
{
version: '1.8.0',
}
);
// Execution order: precache (order -10) → serveFromCache (order 0) → cacheFirst (order 100)Recommendations for using order:
In most cases, you can do without explicitly specifying order — just place plugins in the array in the order you want them to execute. All plugins default to order = 0, so they will execute in registration order.
Explicit order is useful in edge cases when you need to:
- If you use presets with unknown pluggins order in it
- Use plugins from different sources and control their relative order
- Organize plugins into groups (early, regular, late)
Recommended order ranges:
-100…-1— Early plugins (logging, metrics, tracing)0— Regular plugins (default)1…100— Late plugins (fallbacks, final handlers)
Different Service Worker events are handled differently:
Events: install, activate, message, sync, periodicsync
All handlers run in parallel via Promise.all():
import {
precache,
skipWaiting,
precacheMissing,
} from '@budarin/pluggable-serviceworker/plugins';
import { customLogger } from '../customLogger';
import { initServiceWorker } from '@budarin/pluggable-serviceworker';
// All install handlers run in parallel
initServiceWorker(
[
precache({
cacheName: 'app-v1',
assets: ['/', '/main.js'],
}),
precacheMissing({
cacheName: 'ext-v1',
assets: ['/worker.js'],
}),
skipWaiting(),
],
{
version: '1.8.0',
logger: customLogger,
}
);Why parallel:
- install/activate: All plugins initialize independently
- message: All plugins receive the message
- sync: Independent sync tasks
- periodicsync: Independent periodic tasks
Events: fetch, push
Handlers run one after another:
fetch handlers are called in order. A plugin can return Response — then the chain stops and that response is used. Or return undefined — then the next plugin is tried. If all return undefined, the framework calls fetch(event.request).
Example factory that short-circuits for unauthorized access to protected paths:
import type { Plugin } from '@budarin/pluggable-serviceworker';
function authPlugin(config: {
protectedPaths: string[];
order?: number;
}): Plugin {
const { protectedPaths, order = 0 } = config;
return {
order,
name: 'auth',
fetch: async (event, logger) => {
const path = new URL(event.request.url).pathname;
if (protectedPaths.some((p) => path.startsWith(p))) {
if (needsAuth(event.request)) {
logger.warn('auth: unauthorized', event.request.url);
return new Response('Unauthorized', { status: 401 }); // Stops chain
}
}
return undefined; // Pass to next plugin
},
};
}
// using: authPlugin({ protectedPaths: ['/api/'] })Why sequential:
- fetch: Only one response per request; first non-undefined stops the chain. If none returns a response,
fetch(event.request)is used - push: Plugin can return
PushNotificationPayload,false, orundefined. The library callsshowNotificationfor each payload (in parallel). It shows one notification when all plugins returnundefined
| Event | Execution | Short-circuit | Reason |
|---|---|---|---|
install |
Parallel | No | Independent init |
activate |
Parallel | No | Independent activation |
fetch |
Sequential | Yes | Single response |
message |
Parallel | No | Independent handlers |
sync |
Parallel | No | Independent tasks |
periodicsync |
Parallel | No | Independent periodic |
push |
Sequential | No | Show all needed notifications |
backgroundfetchsuccess / backgroundfetchfail / backgroundfetchabort / backgroundfetchclick |
Parallel | No | Background Fetch API events |
One primitive = one operation. Import from @budarin/pluggable-serviceworker/plugins.
All primitives are plugin factories: config (if any) is passed at the call site; initServiceWorker options are only version (required), pingPath?, logger?, onError?. Use order in plugin config to control execution order.
| Name | Event | Description |
|---|---|---|
claim() |
activate |
Calls clients.claim(). |
claimAndReloadClients() |
activate |
claim + reloadClients in one plugin (order guaranteed). |
reloadClients() |
activate |
Reloads all client windows. |
pruneStaleCache(config) |
activate |
Removes cache entries whose URL is not in config.assets. |
cacheFirst(config) |
fetch |
Serve from cache config.cacheName; on miss, fetch and cache. |
networkFirst(config) |
fetch |
Fetch from network, on success cache. On error serve from cache. Otherwise undefined. |
restoreAssetToCache(config) |
fetch |
For URLs in config.assets: serve from cache or fetch and put in cache. Otherwise undefined. |
serveFromCache(config) |
fetch |
Serves from cache config.cacheName; if missing, returns undefined. |
staleWhileRevalidate(config) |
fetch |
Serve from cache, revalidate in background. |
precache(config) |
install |
Caches config.assets in cache config.cacheName. |
precacheWithNotification(config) |
install |
Same as precache, plus sends startInstallingMessage (default SW_MSG_START_INSTALLING) to clients, then caches, then installedMessage (default SW_MSG_INSTALLED). |
precacheMissing(config) |
install |
Adds to cache only assets from config.assets that are not yet cached. |
skipWaiting() |
install |
Calls skipWaiting(). |
skipWaitingOnMessage(config?) |
message |
Triggers on message with messageType (default SW_MSG_SKIP_WAITING). |
Handlers of the same type from different plugins run in parallel. For strict order (e.g. claim then reload clients), use one plugin that calls the primitives in sequence:
import { claim } from '@budarin/pluggable-serviceworker/plugins';
import { reloadClients } from '@budarin/pluggable-serviceworker/plugins';
const claimPlugin = claim();
const reloadPlugin = reloadClients();
activate: async (event, logger) => {
await claimPlugin.activate?.(event, logger);
await reloadPlugin.activate?.(event, logger);
},Example: custom cache and URL logic
Factory postsSwrPlugin(config) returns a plugin that applies stale-while-revalidate only to requests matching pathPattern:
// postsSwrPlugin.ts
import type { Plugin } from '@budarin/pluggable-serviceworker';
import { staleWhileRevalidate } from '@budarin/pluggable-serviceworker/plugins';
function postsSwrPlugin(config: {
cacheName: string;
pathPattern?: RegExp;
order?: number;
}): Plugin {
const { cacheName, pathPattern = /\/api\/posts(\/|$)/, order = 0 } = config;
const swrPlugin = staleWhileRevalidate({ cacheName });
return {
order,
name: 'postsSwr',
fetch: async (event, logger) => {
if (!pathPattern.test(new URL(event.request.url).pathname)) {
return undefined;
}
return swrPlugin.fetch!(event, logger);
},
};
}// sw.ts
const staticCache = 'static-v1';
const assets = ['/', '/main.js'];
initServiceWorker(
[
precache({
cacheName: staticCache,
assets,
}),
serveFromCache({
cacheName: staticCache,
}),
postsSwrPlugin({
cacheName: 'posts',
}),
],
{
version: '1.8.0',
logger: console,
}
);Combinations of primitives. Import from @budarin/pluggable-serviceworker/presets.
| Name | Contents | Purpose |
|---|---|---|
offlineFirst(config) |
precache(config) + serveFromCache(config) |
Serve from cache; on miss, fetch from network. |
Preset config: OfflineFirstConfig (cacheName, assets). Import from @budarin/pluggable-serviceworker/presets.
Strategies like networkFirst, staleWhileRevalidate are available as primitives — build your own SW from primitives and presets.
Pre-built entry points by activation moment (all with offline-first caching). Import from @budarin/pluggable-serviceworker/sw.
| Name | Description |
|---|---|
activateAndUpdateOnNextVisitSW |
Caching SW; activates and updates on next page visit (reload) after new SW is loaded. |
immediatelyActivateAndUpdateSW |
Caching SW; activates immediately on load and on update. |
immediatelyActivateUpdateOnSignalSW |
Caching SW: first install is immediate; on update, new version activates on signal from page (default message SW_MSG_SKIP_WAITING). |
Example:
// sw.js — your service worker entry
import { activateAndUpdateOnNextVisitSW } from '@budarin/pluggable-serviceworker/sw';
activateAndUpdateOnNextVisitSW({
version: '1.8.0',
cacheName: 'my-cache-v1',
assets: ['/', '/styles.css', '/script.js'],
onError: (err, event, type) => console.error(type, err),
});| Name | Use in | Description |
|---|---|---|
registerServiceWorkerWithClaimWorkaround(scriptURL, options?) |
client | Register SW when activate calls claim(); optional one-time reload on first load (workaround for browser bug). |
onNewServiceWorkerVersion(regOrHandler, onUpdate?) |
client | Subscribe to new SW version. Returns an unsubscribe function. Callback when new version is installed and there is an active controller (update, not first install). |
onServiceWorkerMessage(messageType, handler) |
client | Subscribe to messages from SW with given data.type. Returns an unsubscribe function. E.g. "new version available" banners. |
isServiceWorkerSupported() |
client | Check if Service Worker is supported. Useful for SSR/tests/old browsers. |
postMessageToServiceWorker(message, options?) |
client | Send message to active Service Worker. Returns Promise<boolean>. |
getServiceWorkerVersion(options?) |
client | Get active SW version (version from ServiceWorkerInitOptions). Returns Promise<string | null>. |
pingServiceWorker(options?) |
client | GET /sw-ping (handled by ping plugin). Wakes SW if sleeping, checks fetch availability. Returns 'ok' | 'no-sw' | 'error'. |
isBackgroundFetchSupported() |
client | Check if Background Fetch API is available. Returns Promise<boolean>. |
startBackgroundFetch(registration, id, requests, options?) |
client | Start a background fetch. Returns Promise<BackgroundFetchRegistration>. |
getBackgroundFetchRegistration(registration, id) |
client | Get background fetch registration by id. Returns Promise<BackgroundFetchRegistration | undefined>. |
abortBackgroundFetch(registration, id) |
client | Abort a background fetch. Returns Promise<boolean>. |
getBackgroundFetchIds(registration) |
client | List ids of active background fetches. Returns Promise<string[]>. |
normalizeUrl(url) |
SW | Normalize URL (relative → absolute by SW origin) for comparison. |
notifyClients(messageType, data?, includeUncontrolled = false) |
SW | Send { type: messageType } or { type: messageType, ...data } to all client windows controlled by this SW. If includeUncontrolled = true, also sends to uncontrolled windows in scope. |
Client subpaths (for smaller bundles): you can import from @budarin/pluggable-serviceworker/client/registration, .../client/messaging, .../client/health, or .../client/background-fetch instead of .../client to pull in only the utilities you need.
Client utilities — detailed docs (interface, purpose, examples): Registration (EN) | RU · Messaging (EN) | RU · Health (EN) | RU · Background Fetch (EN) | RU
Use registerServiceWorkerWithClaimWorkaround on the page so the SW takes control on first load when using claim() (workaround for browser bug). Without it, the page may have no controller until reload.
import {
isServiceWorkerSupported,
registerServiceWorkerWithClaimWorkaround,
onNewServiceWorkerVersion,
onServiceWorkerMessage,
postMessageToServiceWorker,
getServiceWorkerVersion,
pingServiceWorker,
} from '@budarin/pluggable-serviceworker/client';
if (isServiceWorkerSupported()) {
const reg = await registerServiceWorkerWithClaimWorkaround('/sw.js');
const unsubscribeUpdate = onNewServiceWorkerVersion(reg, () => {
// show "New version available" banner
});
const unsubscribeMsg = onServiceWorkerMessage(
'SW_MSG_NEW_VERSION_READY',
() => {
// show "New version installed, reload" banner
}
);
await postMessageToServiceWorker({ type: 'MY_MSG_PING' });
const swVersion = await getServiceWorkerVersion();
console.log('Service Worker version:', swVersion);
const pingResult = await pingServiceWorker();
console.log('Service Worker ping:', pingResult);
// later, when you no longer need the subscriptions:
unsubscribeUpdate();
unsubscribeMsg();
}On devices, the SW process can be suspended. After a long idle, the first interaction (e.g. messages) may fail until the worker wakes. To reduce issues:
- Call
pingServiceWorker()onfocus/visibilitychange:
import { pingServiceWorker } from '@budarin/pluggable-serviceworker/client';
window.addEventListener('focus', async () => {
await pingServiceWorker();
});- Optionally set the ping path via
pingPathininitServiceWorkerandpathinpingServiceWorkerto avoid clashing with existing routes.
registerServiceWorkerWithClaimWorkaround and related examples work around a Chrome bug reported in issue 482903583. Once the bug is fixed and widely shipped, consider simplifying or removing the workaround and updating the README and examples.
Plugin types are exported from this package. A separate plugin package does not publish its own types — it declares a dependency on @budarin/pluggable-serviceworker and imports types from it.
1. Plugin package dependencies
In your package's package.json:
{
"peerDependencies": {
"@budarin/pluggable-serviceworker": "^1.0.0"
},
"devDependencies": {
"@budarin/pluggable-serviceworker": "^1.5.5"
}
}peerDependencies so the plugin works with the user's library version; devDependencies for build and types.
2. Importing types in the plugin
Import type Plugin (alias for ServiceWorkerPlugin<PluginContext>); and if needed Logger, SwMessageEvent, PushNotificationPayload, etc.
import type { Plugin } from '@budarin/pluggable-serviceworker';
export interface MyPluginConfig {
cacheName: string;
order?: number;
}
export function myPlugin(config: MyPluginConfig): Plugin {
const { cacheName, order = 0 } = config;
return {
order,
name: 'my-plugin',
install: async (_event, logger) => {
logger.info('my-plugin: install');
const cache = await caches.open(cacheName);
await cache.add('/offline.html');
},
fetch: async (event) => {
const cached = await caches.match(event.request);
return cached ?? undefined;
},
};
}Ready-made plugins are installed as separate dependencies and passed into initServiceWorker along with the rest:
| Plugin | Purpose |
|---|---|
| @budarin/psw-plugin-serve-root-from-asset | Serves a chosen cached HTML asset for root (/) navigation — typical SPA setup. |
| @budarin/psw-plugin-serve-range-requests | Handles Range requests for cached files (video, audio, PDF): 206 responses, seeking and streaming from cache. |
| @budarin/psw-plugin-opfs-serve-range | Serves HTTP Range requests for files stored in the Origin Private File System (OPFS) — handy for offline storage and large media. |
Install and API details are in each plugin’s README on npm.
MIT © Vadim Budarin