4.4 KiB
nodeFileFetch — Node file:// adapter
(no submodules)
Purpose
nodeFileFetch is a server-tier FetchLike that closes a real gap in
Node's global fetch: undici (the engine backing globalThis.fetch in
every Node release through 24) refuses file: URLs outright. Consumers
that point bundleUriPlugin at a local-disk rootUrl on Node therefore
cannot use the default plugin wiring — the first get rejects with a
transport error. This adapter is the supported fix.
The function is exposed at the dedicated ./node-fetch subpath so the
package's default export stays universal-pure (no node:* imports
from src/index.ts). Node consumers opt into the adapter with one
explicit import, browser/Bun/edge consumers continue to use the default
globalThis.fetch wiring untouched.
Features
- WHATWG
fetchshape — assignable to@suaveplan/types.FetchLikeand to the plugin'soptions.fetchslot with no cast. file:URL →node:fs/promises.readFileround trip with a realResponse(status 200,content-length,last-modified).HEADshort-circuit returns headers only (no buffer read).Range: bytes=<start>-<end>honoured — returns206 Partial Contentwithcontent-range. Empty end (bytes=10-) is interpreted as "through end of file", matching RFC 7233. Out-of-range requests return416 Range Not Satisfiablewithcontent-range: bytes */<size>.init.signalthreaded intofs.readFile({ signal }). A pre-aborted signal short-circuits the read and rejects with anAbortError(or the signal'sreasonif provided) so the plugin's abort pipeline can rewrap it asBUNDLE_URI.ABORTED.- Non-
file:URLs delegate toglobalThis.fetch, so a single adapter instance handles both local-disk and CDN roots without branching at the call site. - Missing path or non-file inode (directory, symlink loop) returns
404 Not Found, matching the rest of the plugin's NOT_FOUND mapping.
Usage: Basic
import { bundleUriPlugin } from "@suaveplan/storage-uri-plugin-bundle";
import { nodeFileFetch } from "@suaveplan/storage-uri-plugin-bundle/node-fetch";
const plugin = bundleUriPlugin({
packageSlug: "audio-ir-room-plugin-club",
rootUrl: new URL("./assets/", import.meta.url),
fetch: nodeFileFetch,
});
const bytes = await plugin.adapter.get("club.wav");
Usage: Advanced
import { bundleUriPlugin } from "@suaveplan/storage-uri-plugin-bundle";
import { nodeFileFetch } from "@suaveplan/storage-uri-plugin-bundle/node-fetch";
// Range read — plugin issues "Range: bytes=10-19", adapter answers 206
// with the 10-byte slice, plugin returns it verbatim.
const plugin = bundleUriPlugin({
packageSlug: "x",
rootUrl: new URL("file:///pkgs/x/assets/"),
fetch: nodeFileFetch,
});
const controller = new AbortController();
const slice = await plugin.adapter.getPartial(
"club.wav",
10,
10,
controller.signal
);
// controller.abort() before resolution → BUNDLE_URI.ABORTED.
API Reference
nodeFileFetch(input, init?): Promise<Response>—FetchLike. Forfile:URLs reads the path throughnode:fs/promises; for any other scheme delegates toglobalThis.fetch.
Implementation Notes
The adapter never throws for HTTP-shaped problems — missing files become
404, range overflows become 416, etc. — because the calling plugin
already owns the status → BUNDLE_URI.* mapping and expects a real
Response shape. The one exception is AbortSignal: a pre-aborted
signal throws synchronously (before the stat round trip) so the
plugin's ensureNotAborted upstream check is symmetric with the
adapter's downstream check. Mid-flight aborts propagate as the native
AbortError that fs.readFile({ signal }) raises; the plugin's
wrapAbortError catches that and rewrites it to BUNDLE_URI.ABORTED.
Range parsing tolerates both bytes=10-19 (closed interval) and
bytes=10- (open through EOF) per RFC 7233 §2.1. End values past EOF
are clamped to size-1 so a caller asking for bytes=10- against a
20-byte file gets a 10-byte slice + content-range: bytes 10-19/20. A
single regex match runs per call — no allocation past the Headers
construction the calling code already paid for.
node:fs/promises is the only node:* import. The module sits at the
./node-fetch subpath so universal consumers never pull it through the
default barrel; tree-shaking and tier checks both stay green.