npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@curiouslearning/sw

v1.0.0

Published

Service worker update-notification lifecycle fix plus registration/precaching boilerplate shared across Curious Learning apps.

Readme

@curiouslearning/sw

Service worker update-notification lifecycle fix, plus the closely related registration and precaching boilerplate that FeedTheMonsterJS and assessment-survey-js already duplicate independently. This package defines that logic once so it can be imported everywhere instead of drifting again (see the original fix: FTM src/sw-src.js, commit 9e1493da, 2026-05-15).

Why

The old pattern broadcast "update available" on the native updatefound event, before the new worker had actually activated — a user-triggered reload could still be served stale content. The fix here computes isUpdate = !!self.registration.active at evaluation time and only broadcasts once self.clients.claim() has resolved.

Install

npm install @curiouslearning/sw

workbox-core, workbox-precaching, and workbox-routing (^7.4.1) are peer dependencies — install them alongside this package if your service worker doesn't already depend on them directly.

Usage

registerUpdateNotifier (worker-side)

Call synchronously, at the top level of the service worker script, after precaching/routing setup, exactly once:

// sw-src.ts
import { precacheAndRoute } from 'workbox-precaching';
import { registerUpdateNotifier } from '@curiouslearning/sw';

precacheAndRoute(self.__WB_MANIFEST);
registerUpdateNotifier(); // uses DEFAULT_CHANNEL_NAME / DEFAULT_READY_MESSAGE

Throws a TypeError if called outside a service worker global scope (i.e. self.registration is undefined).

registerServiceWorkerUpdates (client-side)

Call once per page load, wherever the app currently calls navigator.serviceWorker.register:

import { registerServiceWorkerUpdates } from '@curiouslearning/sw';

await registerServiceWorkerUpdates({
  swUrl: './sw.js',
  mode: 'confirm', // default; omit unless overriding
});

channelName/readyMessage must match whatever was passed to registerUpdateNotifier() on the worker side (both default to the shared constants below, so in most cases you don't need to pass either).

UpdateMode

| Mode | Behavior | | --- | --- | | 'confirm' (default) | Shows a blocking confirm() dialog when an update is ready; reloads the page on acceptance. Matches both FTM's and assessment-survey-js's current UX, so adopting this package doesn't change behavior. | | 'silent' | No dialog. The new worker activates and takes over on the next natural navigation — use this when you don't want to interrupt the user at all. | | 'custom' | Does nothing but invoke onUpdateAvailable; the app owns 100% of the UX (e.g. a toast/snackbar instead of a native dialog). onUpdateAvailable is required in this mode — omitting it throws a TypeError synchronously, before any registration work happens. |

onUpdateAvailable is optional for 'confirm' and 'silent' too — when provided, it fires as a side hook (e.g. analytics/logging) alongside the built-in behavior for those modes.

Shared constants and types

import {
  DEFAULT_CHANNEL_NAME, // 'sw-update-channel'
  DEFAULT_READY_MESSAGE, // 'UpdateReady'
  CACHE_BUST_PARAM, // 'cache-bust'
} from '@curiouslearning/sw';
import type { SwMessageType, UpdateMode } from '@curiouslearning/sw';

Import these instead of redeclaring your own string literals — this is what keeps the worker and client sides of the lifecycle in sync across apps.

cacheUrlsWithProgress

import { cacheUrlsWithProgress } from '@curiouslearning/sw';

const cache = await caches.open('app-assets-v1');
await cacheUrlsWithProgress(cache, assetUrls, {
  timeoutMs: 8000,
  batchSize: 10,
  delayBetweenBatchesMs: 800,
  onProgress: (pct) => postMessage({ type: 'Loading', percent: pct }),
  onItemError: (url, error) => console.warn('Failed to cache', url, error),
});

Individual item failures (fetch or cache.put()) are tolerated — they're reported via onItemError and still counted toward progress, but never reject the overall promise.

createInjectManifestOptions

// webpack.config.js
const { InjectManifest } = require('workbox-webpack-plugin');
const { createInjectManifestOptions } = require('@curiouslearning/sw');

new InjectManifest(createInjectManifestOptions({
  globIgnores: ['**/audio/*.{mp3,wav}'],
}));

Pure and synchronous — merges the shared defaults (globDirectory: 'build/', a 10 MiB per-file precache ceiling, and standard swSrc/swDest conventions) with whatever overrides you pass.

isCacheBustRequest

import { isCacheBustRequest } from '@curiouslearning/sw';

self.addEventListener('fetch', (event) => {
  if (isCacheBustRequest(event.request.url)) return; // let the browser handle it
  event.respondWith(/* normal cache strategy */);
});

registerNavigationFallback

Call after precacheAndRoute() has run, synchronously during service worker script evaluation:

import { precacheAndRoute } from 'workbox-precaching';
import { registerNavigationFallback } from '@curiouslearning/sw';

precacheAndRoute(self.__WB_MANIFEST);
registerNavigationFallback(); // default '/index.html', enabled: true

Pass { enabled: false } to skip registering the fallback route entirely (e.g. for apps that don't do client-side routing).

Development

npm install
npm run typecheck   # tsc --noEmit
npm test            # jest — colocated *.spec.ts files under src/
npm run build       # vite build -> dist/sw.mjs, dist/sw.cjs, dist/sw.d.ts

Spec files are colocated beside the source file they test (*.spec.ts), not in a separate test/ directory — moving or renaming a feature folder carries its tests with it. Shared test infrastructure (the BroadcastChannel and service-worker-global-scope mocks) lives in src/test-utils/mocks.ts.

Build output

The package builds dual ESM + CJS output via Vite library mode:

  • dist/sw.mjs — ESM build (import)
  • dist/sw.cjs — CJS build (require)
  • dist/sw.d.ts — rolled-up type declarations

Both are covered by a dual-build smoke test (src/index.spec.ts) that require()s and dynamically import()s the built dist/ output directly, catching exports map misconfiguration before publish.