# 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 `fetch` shape — assignable to `@suaveplan/types.FetchLike` and to the plugin's `options.fetch` slot with no cast. - `file:` URL → `node:fs/promises.readFile` round trip with a real `Response` (status 200, `content-length`, `last-modified`). - `HEAD` short-circuit returns headers only (no buffer read). - `Range: bytes=-` honoured — returns `206 Partial Content` with `content-range`. Empty end (`bytes=10-`) is interpreted as "through end of file", matching RFC 7233. Out-of-range requests return `416 Range Not Satisfiable` with `content-range: bytes */`. - `init.signal` threaded into `fs.readFile({ signal })`. A pre-aborted signal short-circuits the read and rejects with an `AbortError` (or the signal's `reason` if provided) so the plugin's abort pipeline can rewrap it as `BUNDLE_URI.ABORTED`. - Non-`file:` URLs delegate to `globalThis.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 ```ts 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 ```ts 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` — `FetchLike`. For `file:` URLs reads the path through `node:fs/promises`; for any other scheme delegates to `globalThis.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.