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

@watchgold/resilience

v0.1.0

Published

Resilience primitives for data-heavy frontends: safe retries with backoff, request deduplication and coalescing, TTL caching, deploy-skew chunk recovery, and fail-closed data settling for ISR.

Readme

@watchgold/resilience

Resilience primitives for data-heavy frontends: safe retries with backoff, request deduplication and coalescing, TTL caching, deploy-skew chunk recovery, and fail-closed data settling for ISR.

Zero dependencies. Framework-free (works with Next.js, but nothing here imports it). Every helper is dependency-injected (now, reload, the underlying get, the loader) so all of it is testable without timers-in-the-sky or a real network.

Why this exists

Each module encodes a lesson a production market-data platform paid for:

The duplicate-POST retry. A response interceptor replayed any request that timed out or hit a 503 — POSTs included. Any metered AI question slower than the client's 30 s timeout was silently re-sent up to three times: each retry passed the daily-allowance check again, spent model tokens again, and persisted a duplicate record. retry.ts replays only RFC 9110 safe methods (GET/HEAD/OPTIONS); an unsafe request that may already have been processed surfaces its error and lets the user retry deliberately.

The empty render cached by ISR. A page fetched all of its panels with Promise.allSettled and rendered null for anything that rejected. Looks resilient — until the backend was down or cold-starting and every fetch rejected at once: the page rendered fully empty with a 200, and Next.js ISR cached that blank render for the whole revalidate window. Every visitor for the next minute got an empty page, even after the backend recovered. settle.ts keeps the partial tolerance but throws when everything (or anything you mark critical) failed, so a revalidation render fails and the framework keeps serving the last good page instead of caching the outage.

Deploy-skew crashes. After each deploy, tabs opened against the previous build still hold the old chunk manifest. Their next client-side navigation requests chunks whose names changed: the renamed chunk 404s (ChunkLoadError), or a lazy module resolves to undefined (minified React error #130), and the user lands on a dead-end "client-side exception" screen. The cure is one full reload — chunk-recovery.ts detects the skew-shaped errors and hard-reloads once, with a sessionStorage guard so a genuine bug can't cause a reload loop.

The remaining modules are the load-shedding layer that kept the same platform inside upstream rate limits: browser-side GET deduplication (dedup.ts), and the BFF/proxy response cache with per-prefix TTLs and single-flight coalescing (coalesced-cache.ts) that stopped 429s during traffic spikes.

Install

npm i @watchgold/resilience

Quick start

Safe retries with backoff (retry)

import axios from 'axios';
import { attachRetryInterceptor, isRetryableRequestError } from '@watchgold/resilience';

const api = axios.create({ baseURL: '/api', timeout: 30_000 });

// GET/HEAD/OPTIONS retried on network error / 503 / timeout with 2s, 4s, 8s
// backoff. POST/PUT/PATCH/DELETE are NEVER replayed.
const detach = attachRetryInterceptor(api);

// Customize:
attachRetryInterceptor(api, {
  maxRetries: 2,
  delayMs: (attempt) => attempt * 500,
  isRetryable: (err) => isRetryableRequestError(err, { retryStatuses: [503, 429] }),
  onRetry: (err, attempt) => console.warn(`retry #${attempt}`, err.code),
});

The client is typed structurally (AxiosLikeInstance: callable with a config, plus interceptors.response.use), so axios is a peer in spirit only — any compatible client, or a hand-rolled test double, works.

Request deduplication (dedup)

import { createDedupedGet } from '@watchgold/resilience';

const deduped = createDedupedGet((url) => api.get(url)); // or fetch-based

// A ticker, a chart, and a stats panel all asking for the same series
// during one render → exactly one network call.
const [a, b] = await Promise.all([
  deduped.get('/prices/latest'),
  deduped.get('/prices/latest'),
]);

deduped.get('/holdings/monthly', 5 * 60_000); // slow-moving data: longer TTL
deduped.invalidate('/prices/latest');         // e.g. after a relevant mutation

TTL cache with coalescing, for a BFF/proxy (coalesced-cache)

import {
  ttlRules, createCoalescedCache, etagMatches, swrCacheControl,
} from '@watchgold/resilience';

// Per-prefix TTLs in seconds, first match wins; 0 = per-caller, never cached.
const ttlFor = ttlRules(
  [
    ['api/portfolio', 0],   // must reflect writes immediately
    ['api/candles',  30],   // near-real-time series
    ['api/reference', 300], // slow-moving reference data
  ],
  15, // light default for everything else
);

const cache = createCoalescedCache<{ body: string; status: number; etag: string }>({
  ttlMsFor: (key) => ttlFor(key) * 1000,
});

// In a route handler:
const entry = await cache.fetchThrough(pathAndQuery, async () => {
  const res = await fetch(upstreamUrl);
  const body = await res.text();
  // 2xx-only caching: error responses are returned to waiters, never stored.
  return { value: { body, status: res.status, etag: makeEtag(body) }, cacheable: res.ok };
});

const cacheControl = swrCacheControl(ttlFor(pathAndQuery));
if (ifNoneMatch && etagMatches(ifNoneMatch, entry.etag)) return new Response(null, { status: 304 });

Semantics carried over from production:

  • Fresh hits are served without touching the upstream.
  • Concurrent misses coalesce: one loader runs; everyone shares its result. If it rejects, waiting callers fall through and make their own attempt instead of inheriting the failure.
  • TTL 0 bypasses read, write and coalescing — coalescing could answer a read-after-write with a fetch that started pre-write.
  • Only cacheable !== false results are stored; purging of expired entries is throttled (once per minute by default).
  • invalidatePrefix('api/reference') after a mutation drops the stale reads.

Deploy-skew chunk recovery (chunk-recovery)

import { isChunkLoadError, reloadForChunkErrorOnce } from '@watchgold/resilience';

// In a global error boundary / error page:
export default function GlobalError({ error }: { error: Error }) {
  useEffect(() => {
    if (isChunkLoadError(error) && reloadForChunkErrorOnce()) return; // reloading
  }, [error]);
  return <FallbackScreen />;
}

reloadForChunkErrorOnce reloads at most once per 15 s window (tracked in sessionStorage under chunk-recovery:last-reload-at), so a genuine bug can't trap the user in a reload loop. If storage is unavailable (private mode, quota), it reloads anyway — one reload still beats a dead-end error screen.

Fail-closed settling for ISR pages (settle)

import { settleOrThrow } from '@watchgold/resilience';

// In a server component / page with `revalidate`:
const data = await settleOrThrow(
  {
    prices: fetchPrices(),      // the page is meaningless without this
    holdings: fetchHoldings(),  // best-effort panel
    news: fetchNews(),          // best-effort panel
  },
  { critical: ['prices'], onError: (key, err) => console.error(`[${String(key)}]`, err) },
);
// data.holdings: Holdings | null — render partial pages happily.
// Backend fully down => AggregateError => ISR keeps serving the last good page.

Bounded fetches (fetch-timeout)

import { fetchWithTimeout } from '@watchgold/resilience';

const res = await fetchWithTimeout(url, { headers }, { timeoutMs: 10_000 });
// Times out with a `TimeoutError` DOMException; combines with your own
// AbortSignal (init.signal) — whichever aborts first wins.

API reference

retry

| Export | Description | | --- | --- | | isRetryableRequestError(error, opts?) | true only for safe methods (GET/HEAD/OPTIONS) failing transiently: no response, status in opts.retryStatuses (default [503]), or code === 'ECONNABORTED'. | | attachRetryInterceptor(client, opts?) | Registers a response interceptor on an AxiosLikeInstance; returns a detach function. Options: maxRetries (3), delayMs(attempt) (2 ** attempt * 1000), isRetryable, onRetry(error, attempt). Replay count is stamped on the request config as __retryCount. | | RetryableErrorLike, AxiosLikeInstance, RetryInterceptorOptions, IsRetryableOptions | Supporting types. |

dedup

| Export | Description | | --- | --- | | createDedupedGet(get, opts?) | Wraps a (url) => Promise<R>; returns { get(url, ttlMs?), invalidate(url?), size() }. Coalesces identical in-flight GETs; caches successes for ttlMs (default defaultTtlMs, 15 000 ms); failures clear the slot and propagate; now injectable. |

chunk-recovery

| Export | Description | | --- | --- | | DEFAULT_SKEW_PATTERNS | Case-insensitive fragments identifying stale-build errors (webpack chunk errors, dynamic-import failures, unexpected token '<', React #130). | | isChunkLoadError(error, extraPatterns?) | Matches error.name + error.message against the default plus extra patterns. | | reloadForChunkErrorOnce(opts?) | Guarded hard reload. Options: storageKey ('chunk-recovery:last-reload-at'), guardWindowMs (15 000), reload (defaults to window.location.reload). Returns whether a reload was triggered; false during SSR. |

coalesced-cache

| Export | Description | | --- | --- | | ttlRules(rules, defaultTtlSeconds) | Ordered [prefix, ttlSeconds] table → (key) => ttlSeconds, startsWith matching, first match wins. | | createCoalescedCache<V>({ ttlMsFor, now?, purgeIntervalMs? }) | get(key) (fresh-only), fetchThrough(key, loader), invalidatePrefix(prefix), delete(key), clear(), size(). Loader resolves { value, cacheable? }. | | etagMatches(ifNoneMatchHeader, etag) | RFC 9110 weak comparison; handles W/ prefixes and comma-separated lists. | | swrCacheControl(ttlSeconds, opts?) | 'private, no-store' for TTL 0 / noStore; otherwise public, max-age=N, stale-while-revalidate=N*factor (factor default 4). |

settle

| Export | Description | | --- | --- | | settleOrThrow(tasks, opts?) | Awaits a keyed map of promises; rejections become null. Throws AggregateError when every task rejected (requireAny, default true) or any critical key rejected. onError(key, reason) observes each rejection. |

fetch-timeout

| Export | Description | | --- | --- | | fetchWithTimeout(input, init?, opts?) | fetch bounded by AbortSignal.timeout(opts.timeoutMs ?? 10_000); a caller-supplied init.signal is combined (via AbortSignal.any where available, listener merge otherwise). |

SSR safety

  • reloadForChunkErrorOnce returns false when there is no window; isChunkLoadError is pure. Safe to import from server components.
  • createDedupedGet and createCoalescedCache hold plain module-level-style Maps: on a server they are per process (each serverless instance has its own copy — good for load shedding, not a source of truth), and in the browser they are per tab.
  • settleOrThrow and fetchWithTimeout are isomorphic. fetchWithTimeout needs AbortSignal.timeout (Node 18+, all modern browsers); AbortSignal.any is used when present (Node 20.3+) with a fallback otherwise.
  • Nothing here touches document, and no module has import-time side effects (sideEffects: false).

License

MIT


Extracted from the production codebase of WatchGold, a precious-metals market-data platform.