Files
skills/docs/api-stack/api/api-loader/src/cache-bust/cache-bust.md
2026-05-29 08:20:56 +01:00

3.4 KiB

bustSpecifier

(no submodules)

Purpose

Compute the cache-busted ESM specifier used by the API plugin host during a hot reload. Bun's ESM cache is keyed on the full specifier string (including query parameters), so file:///plugin/index.js and file:///plugin/index.js?v=2 are distinct cache entries even though both resolve to the same file on disk. bustSpecifier is the canonical helper the host calls on every reload to produce a fresh specifier — bumping the importing module's URL forces Bun to re-evaluate the entire transitive dependency subgraph without paying for an explicit cache invalidation API.

The function is pure and idempotent under monotonically-increasing version inputs: repeated calls replace the existing ?v=N segment rather than concatenate, so a long-lived host can bust the same plugin thousands of times without growing the specifier string. This is the canonical algorithm referenced by design §8.2.

Features

  • Replaces (never concatenates) the ?v=N query suffix.
  • Preserves unrelated query parameters and hash fragments.
  • Works with file://, http://, https://, and data: URLs.
  • Side-effect-free, allocation-light (one URL instance per call).
  • Strict input contract: throws synchronously on a malformed URL.

Usage: Basic

import { bustSpecifier } from "@suaveplan/api-loader/cache-bust";

const specifier = bustSpecifier("file:///opt/plugin/index.js", 1);
// → "file:///opt/plugin/index.js?v=1"
const next = bustSpecifier(specifier, 2);
// → "file:///opt/plugin/index.js?v=2" (NOT "?v=1?v=2")
const mod = await import(next);

Usage: Advanced

Within a reload loop, the host keeps a monotonically-increasing counter and busts the specifier on every flip. Other parameters (e.g. an integrity hash) survive the rewrite:

import { bustSpecifier } from "@suaveplan/api-loader/cache-bust";

let reload = 0;
const base = "file:///opt/plugin/index.js?integrity=abc";
function nextSpecifier(): string {
    reload += 1;
    return bustSpecifier(base, reload);
}
// 1st call → "file:///opt/plugin/index.js?integrity=abc&v=1"
// 2nd call → "file:///opt/plugin/index.js?integrity=abc&v=2"

API Reference

export function bustSpecifier(url: string, version: number): string;
  • url: an absolute URL (a file:// URL is typical inside the loader, but any URL supported by the WHATWG URL parser works).
  • version: a non-negative integer reload counter. Zero is permitted as a boundary case (used by the host on the initial load).

Returns the input URL with its v query parameter set to the string form of version. Other query parameters, the path, the fragment, and the authority are preserved verbatim.

Implementation Notes

The implementation uses the WHATWG URL API (URL and URLSearchParams) because it round-trips correctly: URLSearchParams.set("v", ...) replaces any existing v entry rather than appending a duplicate. Hand-rolled string concatenation (${url}?v=${version}) was rejected because it cannot detect an existing suffix — repeated busts under that approach would grow the specifier without bound (?v=1?v=2?v=3), and Bun's cache would treat each mutation as a distinct module forever, defeating the GC contract the rollback journal relies on.

The function throws TypeError from the WHATWG parser on a malformed URL input; the host's reload path catches and rewraps as LOADER.MODULE_IMPORT_FAILED.