Initial Commit
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
# nodeFileFetch — Node `file://` adapter
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## 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=<start>-<end>` 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 */<size>`.
|
||||
- `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<Response>` — `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.
|
||||
Reference in New Issue
Block a user