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

@ovineko/spa-guard

v0.0.2-alpha-1

Published

Chunk load error handling for SPAs — core runtime, error handling, schema, i18n

Readme

@ovineko/spa-guard

npm license

Core runtime for spa-guard — chunk load error handling, version checking, spinner, i18n, and event schema for SPAs.

Install

npm install @ovineko/spa-guard

Usage

Call recommendedSetup once when your app boots. It dismisses the loading spinner, starts version checking, and returns a cleanup function.

import { recommendedSetup } from "@ovineko/spa-guard/runtime";

const cleanup = recommendedSetup();
// optionally call cleanup() to stop background checks

recommendedSetup is idempotent. Repeated calls return the same cleanup function and do not re-run setup side effects.

By default, recommendedSetup uses healthyBoot: "auto":

  • if retry params exist in the URL, it waits a dynamic grace period and calls markRetryHealthyBoot() only when the orchestrator phase is still idle.
  • if no retry params exist, it does nothing.

Auto grace period formula:

  • max(5000, max(reloadDelays)+1000, sum(lazyRetry.retryDelays)+1000)

If you want strict control, switch to manual healthy-boot mode and call markRetryHealthyBoot() yourself.

import { markRetryHealthyBoot } from "@ovineko/spa-guard";
import { recommendedSetup } from "@ovineko/spa-guard/runtime";

recommendedSetup();

await Promise.all([import("./routes/Home"), import("./routes/Checkout")]);

markRetryHealthyBoot();

Disable auto healthy boot:

recommendedSetup({ healthyBoot: "manual" });

Disable version checking:

const cleanup = recommendedSetup({ versionCheck: false });

Retry behavior and event flow

spa-guard uses a single retry orchestrator (retryOrchestrator.ts) as the sole owner of retry lifecycle. All reload scheduling, deduplication, and fallback transitions run through triggerRetry().

Retry state machine

The orchestrator maintains an explicit phase:

  • idle — no retry in progress
  • scheduled — a reload has been scheduled (timer running); concurrent triggers are deduplicated
  • fallback — retries exhausted, fallback UI is shown; further triggers are ignored

Retry progression

When a recoverable error occurs (chunk load error, unhandled rejection, vite:preloadError, or static asset 404):

  1. Event listener classifies the event and calls triggerRetry({ source, error }).
  2. Orchestrator checks phase — if already scheduled or fallback, returns immediately without scheduling another reload.
  3. On first trigger: reads RETRY_ATTEMPT_PARAM from URL to restore attempt count after a reload.
  4. If attempts remain: increments attempt, calls showLoadingUI(nextAttempt) to render the loading UI immediately (before the timer fires), sets a timer for reloadDelays[currentAttempt], encodes retryId and attempt count into the reload URL, then navigates. Requires options.html.loading.content to be configured; if absent, showLoadingUI returns silently and the retry still proceeds.
  5. If attempts are exhausted: transitions to fallback, calls setFallbackMode(), sends a beacon, and renders fallback UI.

URL params

The orchestrator serializes state into URL params for cross-reload continuity:

  • RETRY_ATTEMPT_PARAM — current attempt count (strict non-negative integer; malformed values are ignored)
  • RETRY_ID_PARAM — unique ID for this retry session
  • CACHE_BUST_PARAM — timestamp for cache busting (set when cacheBust: true is passed, e.g. for static asset errors)

Loading UI during retry delay

When options.html.loading.content is configured, showLoadingUI(attempt) is called before the reload timer fires. It injects the loading HTML into the target element (selector from options.html.fallback.selector, defaulting to body), reveals the data-spa-guard-section="retrying" element, fills attempt numbers into [data-spa-guard-content="attempt"] elements, applies i18n via applyI18n/getI18n, and optionally hides or replaces the spinner based on options.html.spinner. If loading content is not configured or the target element is not found, showLoadingUI returns silently — the retry still proceeds normally.

Default fallback/loading templates are theme-aware: they set color-scheme: light dark and apply neutral dark colors automatically via @media (prefers-color-scheme: dark).

Unhandledrejection serialization

serializeError handles PromiseRejectionEvent with strict redaction guardrails. For HTTP-like errors it extracts only safe metadata: status, statusText, url, method, response.type, and X-Request-ID from response headers when present. Response body, request/response payload, and the full headers object are never included in serialized output. For request wrappers (reason.request / reason.config) only method, url, and baseURL are extracted. Deep object traversal is bounded by MAX_DEPTH=4, MAX_KEYS=20, and MAX_STRING_LEN=500 to prevent oversized beacons. Circular references are handled via a WeakSet visited tracker. The output also includes isTrusted, timeStamp from the event, and runtime context (pageUrl, constructorName). This HTTP-like extraction also applies to Error subclasses that carry a response field (for example ResponseError), not only to plain objects.

Retry reset

If enough time has passed since the last reload (configurable via minTimeBetweenResets, default 5000 ms), the orchestrator resets the attempt counter and starts a fresh retry cycle instead of continuing to fallback. This prevents stale URL params from triggering fallback on a clean page load.

Healthy boot

After a successful app boot following a retry reload, markRetryHealthyBoot() clears retry URL params, cancels any pending timer, and resets orchestrator state.

Default behavior in recommendedSetup: auto healthy-boot after a grace period (healthyBoot: "auto"). Manual override: set healthyBoot: "manual" and call markRetryHealthyBoot() yourself once critical boot is confirmed.

Avoid false retry loops from app errors

By default, regular unhandledrejection events also trigger triggerRetry() (handleUnhandledRejections.retry: true). If your app has non-chunk unhandled rejections, this can look like retry looping even though chunks are healthy. In that case, disable reload-on-unhandled-rejection:

window.__SPA_GUARD_OPTIONS__ = {
  handleUnhandledRejections: {
    retry: false,
    sendBeacon: true,
  },
};

Static asset burst coalescing

Multiple 404'd asset errors within a short window (default 500 ms) are coalesced into a single triggerRetry({ cacheBust: true, source: "static-asset-error" }). The orchestrator's deduplication ensures only one reload is scheduled.

Retry ownership rule

Only retryOrchestrator.ts may schedule reloads, advance retry state, or transition to fallback. Listeners, renderers, and other modules must not schedule their own retry timers or set fallback state directly.

API

@ovineko/spa-guard (common)

  • listen — subscribe to spa-guard events
  • events — event type constants
  • options — runtime options helpers
  • disableDefaultRetry / enableDefaultRetry / isDefaultRetryEnabled — control default retry behaviour
  • isInFallbackMode — returns true when fallback UI is active (retries exhausted)
  • resetFallbackMode — clears the fallback flag; use in tests or programmatic recovery flows
  • BeaconError — error class for beacon failures
  • ForceRetryError — error class to force a retry

Retry orchestrator (single owner of retry lifecycle):

  • triggerRetry(input?) — trigger a retry from any source; returns TriggerResult:
    • { status: "accepted" } — reload scheduled
    • { status: "deduped", reason: string } — ignored because another retry is already scheduled or an internal error occurred
    • { status: "fallback" } — already in fallback mode; no retry scheduled
    • { status: "retry-disabled" } — retry is disabled via disableDefaultRetry()
  • markRetryHealthyBoot() — call after a successful boot following a retry reload; clears URL params, cancels timers, resets orchestrator state and fallback flag
  • getRetrySnapshot() — returns current orchestrator state: { phase, attempt, retryId, lastSource, lastTriggerTime }
  • resetRetryOrchestratorForTests() — resets all orchestrator state including fallback flag; use in test teardown

@ovineko/spa-guard/runtime

  • recommendedSetup(options?) — enable recommended runtime features; idempotent and returns cleanup function
  • RecommendedSetupOptions{ versionCheck?: boolean; healthyBoot?: "auto" | "manual" | "off" | false | { mode?: "auto"; graceMs?: number } }
    • healthyBoot.graceMs acts as a lower bound override for auto mode (max(computedGraceMs, graceMs))
  • startVersionCheck / stopVersionCheck — manual version check control
  • getState / subscribeToState — runtime state access
  • SpaGuardState — state type
  • showSpinner / dismissSpinner / getSpinnerHtml — spinner helpers
  • setTranslations — override i18n strings
  • ForceRetryError

@ovineko/spa-guard/schema

Schemas for spa-guard configuration.

@ovineko/spa-guard/i18n

Built-in translation strings.

@ovineko/spa-guard/runtime/debug

Debug helpers for testing error scenarios. Use createDebugger() to mount an in-page panel with buttons for each scenario.

Available error scenarios:

  • dispatchChunkLoadError — unhandled ChunkLoadError rejection
  • dispatchNetworkTimeout — unhandled network timeout after a delay
  • dispatchSyncRuntimeError — sync error thrown during React render
  • dispatchAsyncRuntimeError — uncaught error via setTimeout
  • dispatchFinallyError — unhandled rejection from Promise.finally
  • dispatchForceRetryError — ForceRetryError to exercise the force-retry path
  • dispatchUnhandledRejection — generic unhandled promise rejection
  • dispatchRetryExhausted — renders fallback UI directly (bypasses orchestrator; orchestrator phase remains idle)
  • dispatchStaticAsset404 — appends a hashed <script> that 404s, exercising the Resource Timing API-based static asset detection path

Related packages

License

MIT