first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,167 @@
# @suaveplan/storage-uri-plugin-bundle - Per-package bundle:// URI scheme plugin for @suaveplan/storage-uri. Universal; resolves bundle://<package-slug>/<rel-path> against an injectable rootUrl via fetch on every read. Read-only StorageLike shim, HTTP Range support with 200/206/416 disambiguation, AbortSignal propagation, optional in-memory LRU, full OpenTelemetry surface.
Per-package `bundle://` URI scheme for `@suaveplan/storage-uri`.
`bundle://<package-slug>/<rel-path>` lets a consuming package ship binary
assets — impulse responses, sample fonts, reference images, fixture
bundles — alongside its npm tarball and resolve them through the existing
`storage-uri` ecosystem. The plugin is universal: zero `node:fs`,
zero browser-only branches, every read goes through an injectable
`FetchLike` against `new URL(encodedRelPath, rootUrl)`. `file:`, `https:`,
`http:`, `data:`, `blob:`, and bundler-virtual schemes all flow through
the same fetch surface.
<!-- begin:auto-docs -->
## Documentation tree
- [abort](./src/abort/abort.md) — `abort.ts` declares the two helpers every read path consumes to honour an `AbortSignal`: `ensureNotAborted` short-circu…
- [abort](./src/abort/abort.md) — `abort.ts` declares the two helpers every read path consumes to honour an `AbortSignal`: `ensureNotAborted` short-circu…
- [cache](./src/cache/cache.md) — `cache.ts` declares the optional in-memory LRU used by the storage shim when a caller passes `options.cache = { maxByte…
- [cache](./src/cache/cache.md) — `cache.ts` declares the optional in-memory LRU used by the storage shim when a caller passes `options.cache = { maxByte…
- [error-codes](./src/error-codes/error-codes.md) — `error-codes.ts` declares the canonical registry of every error code raised by `@suaveplan/storage-uri-plugin-bundle` a…
- [error-codes](./src/error-codes/error-codes.md) — `error-codes.ts` declares the canonical registry of every error code raised by `@suaveplan/storage-uri-plugin-bundle` a…
- [factory](./src/factory/factory.md) — `factory.ts` is the public entry point for `@suaveplan/storage-uri-plugin-bundle`. `bundleUriPlugin(options)` validates…
- [factory](./src/factory/factory.md) — `factory.ts` is the public entry point for `@suaveplan/storage-uri-plugin-bundle`. `bundleUriPlugin(options)` validates…
- [node-fetch](./src/node-fetch/node-fetch.md) — `nodeFileFetch` is a server-tier `FetchLike` that closes a real gap in Node's global `fetch`: undici (the engine backin…
- [node-fetch](./src/node-fetch/node-fetch.md) — `nodeFileFetch` is a server-tier `FetchLike` that closes a real gap in Node's global `fetch`: undici (the engine backin…
- [otel](./src/otel/otel.md) — `otel.ts` exposes the tracer, meter, and instrument bag that every read path in `@suaveplan/storage-uri-plugin-bundle`…
- [otel](./src/otel/otel.md) — `otel.ts` exposes the tracer, meter, and instrument bag that every read path in `@suaveplan/storage-uri-plugin-bundle`…
- [parse](./src/parse/parse.md) — `parse.ts` enforces the `bundle://<package-slug>/<rel-path>` grammar declared in `design.md §1` and rejects every malfo…
- [parse](./src/parse/parse.md) — `parse.ts` enforces the `bundle://<package-slug>/<rel-path>` grammar declared in `design.md §1` and rejects every malfo…
- [range](./src/range/range.md) — `range.ts` implements the building blocks the storage shim uses to issue HTTP `Range` requests and disambiguate the thr…
- [range](./src/range/range.md) — `range.ts` implements the building blocks the storage shim uses to issue HTTP `Range` requests and disambiguate the thr…
- [storage-shim](./src/storage-shim/storage-shim.md) — `storage-shim.ts` declares `BundleStorageShim`, the `StorageLike` adapter the `bundle://` URI plugin exposes. The class…
- [storage-shim.fetch](./src/storage-shim/storage-shim.fetch.md) — `storage-shim.fetch.ts` carries the two cross-cutting helpers every storage-shim method uses: `safeFetch` dispatches an…
- [storage-shim.head](./src/storage-shim/storage-shim.head.md) — `storage-shim.head.ts` implements the two HEAD-routed methods of the bundle storage shim. `performExists` returns a boo…
- [storage-shim](./src/storage-shim/storage-shim.md) — `storage-shim.ts` declares `BundleStorageShim`, the `StorageLike` adapter the `bundle://` URI plugin exposes. The class…
- [storage-shim.range](./src/storage-shim/storage-shim.range.md) — `storage-shim.range.ts` carries the full `getPartial` pipeline: pre-fetch offset/length validation, the `Range: bytes=……
- [streaming](./src/streaming/streaming.md) — `streaming.ts` exposes the single helper the storage shim consumes from its `createReadStream` implementation: given a…
- [streaming](./src/streaming/streaming.md) — `streaming.ts` exposes the single helper the storage shim consumes from its `createReadStream` implementation: given a…
- [types](./src/types/plugin-options.schemas.md) — `plugin-options.schemas.ts` declares the Zod schema that validates `BundleUriPluginOptions` at construction time. The s…
- [plugin-options.schemas](./src/types/plugin-options.schemas.md) — `plugin-options.schemas.ts` declares the Zod schema that validates `BundleUriPluginOptions` at construction time. The s…
<!-- end:auto-docs -->
## Modules
- `./error-codes/error-codes` — `BUNDLE_URI_CODES` (9 codes) + `BundleUriError`.
- `./factory/factory` — `bundleUriPlugin(options)` entry point.
- `./parse/parse` — `parse` + `build` with three-pass traversal prevention.
- `./storage-shim/storage-shim` — `BundleStorageShim` (`StorageLike`).
- `./storage-shim/storage-shim.fetch` — `safeFetch` + `assertSuccess`.
- `./storage-shim/storage-shim.head` — HEAD-backed `exists` / `getMetadata`.
- `./storage-shim/storage-shim.range` — Range pipeline with 206/200/416 disambiguation.
- `./range/range` — `validateRange`, `buildRangeHeader`, `sliceForRange`.
- `./streaming/streaming` — `toReadableStream`.
- `./abort/abort` — `ensureNotAborted` + `wrapAbortError`.
- `./cache/cache` — `UriLruCache`.
- `./otel/otel` — meter + tracer wiring, `recordOp`, fallback counters.
- `./node-fetch` — **server-tier** opt-in `FetchLike` for Node consumers
whose `rootUrl` is a `file://` URL (see [Node users + `file://` roots](#node-users--file-roots)).
## Quick start — register one plugin per consuming package
```ts
import { createStorageUriResolver } from "@suaveplan/storage-uri";
import { bundleUriPlugin } from "@suaveplan/storage-uri-plugin-bundle";
const resolver = createStorageUriResolver({});
// In each consuming package's `register()`:
resolver.register(
bundleUriPlugin({
packageSlug: "audio-ir-room-plugin-club",
rootUrl: new URL("./", import.meta.url),
})
);
const bytes = await resolver.read(
"bundle://audio-ir-room-plugin-club/assets/club.wav"
);
```
## Bundler composition — flatten assets to a CDN URL
Vite, esbuild, and webpack all rewrite `new URL("./assets/x.wav", import.meta.url)`
at build time. Register the plugin with a `rootUrl` pointing at the
flattened asset directory and the runtime fetch transparently lands on
the bundler-produced URL:
```ts
// Vite / esbuild / webpack
import assetsRoot from "./assets/?url"; // → flattened bundle URL
bundleUriPlugin({
packageSlug: "audio-ir-room-plugin-club",
rootUrl: assetsRoot, // bundler-rewritten string is fine
});
```
In production this becomes a real HTTPS URL — `bundle://` still resolves
the relative path inside that root through `fetch`, and the OTel surface
classifies the root as `root_kind="http"`.
## Node users + `file://` roots
Node's global `fetch` (undici, all releases through Node 24) does NOT
support `file:` URLs. If your `rootUrl` is a `file://` URL and you are
running on Node, the default `globalThis.fetch` wiring will reject on
the first `get`. The package ships a dedicated server-tier adapter at
the `./node-fetch` subpath that closes the gap:
```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, // ← required on Node when rootUrl is file://
});
```
`nodeFileFetch` reads `file:` URLs via `node:fs/promises`, returns a
real `Response` with `content-length` + `last-modified`, honours `HEAD`
and `Range: bytes=<start>-<end>` (including open-ended `bytes=10-`), and
threads `init.signal` into the underlying read. Any non-`file:` URL
falls through to `globalThis.fetch`, so a single adapter handles both
local-disk and CDN roots without branching at the call site.
Bun's native `fetch` already supports `file:` URLs — Bun consumers can
keep using the default `globalThis.fetch` wiring. The subpath import
exists so the package's default export stays universal-pure (no
`node:*` imports leak into browser, Cloudflare Workers, Deno, or React
Native builds).
## Composition — front the plugin with `storage-plugin-cache`
```ts
import { cachedStorage } from "@suaveplan/storage-plugin-cache";
import { MemoryCache } from "@suaveplan/cache";
const cache = new MemoryCache<Uint8Array>();
const cached = cachedStorage(plugin.adapter, { cache, namespace: "bundle" });
```
## Error codes
`BUNDLE_URI.PACKAGE_MISMATCH`, `PATH_TRAVERSAL`, `READ_ONLY`,
`NOT_FOUND`, `INVALID_URI`, `FETCH_FAILED`, `RANGE_INVALID`,
`RANGE_UNSUPPORTED`, `ABORTED` — all reachable from a real call site
exercised by the test suite.
## OpenTelemetry
- `bundle_uri.op_duration_ms{op, root_kind, outcome}` histogram.
- `bundle_uri.reads_total{root_kind, outcome}` counter.
- `bundle_uri.errors_total{root_kind, code}` counter.
- `bundle_uri.range_fallback_total{root_kind}` counter.
- `bundle_uri.cache_hits_total{root_kind}` /
`bundle_uri.cache_misses_total{root_kind}` counters.
- Per-op span on the `@suaveplan/storage-uri-plugin-bundle` tracer with
`bundle.package`, `bundle.rel_path` (query-stripped + 256-char clip),
`bundle.root_kind` (`file | http | data | blob | other`),
`bundle.outcome` attributes.
Inject a `MeterProvider` with `setBundleUriMeterProvider(provider)` then
`resetInstruments()` to rebind.

View File

@@ -0,0 +1,77 @@
# AbortSignal helpers
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`abort.ts` declares the two helpers every read path consumes to honour an
`AbortSignal`: `ensureNotAborted` short-circuits an already-aborted signal
into a `BUNDLE_URI.ABORTED` rejection *before* any fetch is dispatched,
and `wrapAbortError` converts a mid-flight `AbortError` into a structured
`BundleUriError` carrying the original error on `cause` so retry and
circuit-breaker classifiers can distinguish caller-initiated cancellation
from transport failure.
The helpers are intentionally stateless and side-effect free — they accept
the per-call diagnostic context (URI, slug, rel-path, root-kind) as a
plain object so the same helper handles every operation (`get`, `exists`,
`getMetadata`, `getPartial`, `createReadStream`) without per-op branches.
## Features
- Pre-fetch short-circuit when `signal.aborted === true`.
- Mid-flight `AbortError` detection by name (works against both
`DOMException("AbortError")` and `Error{name:"AbortError"}` shapes).
- Original error preserved on `cause` for the structured-error chain.
- Zero allocation when the signal is absent or not aborted.
## Usage: Basic
```ts
import {
ensureNotAborted,
wrapAbortError,
} from "@suaveplan/storage-uri-plugin-bundle";
ensureNotAborted(signal, {
uri: "bundle://x/a",
packageSlug: "x",
relPath: "a",
rootKind: "http",
});
```
## Usage: Advanced
```ts
try {
return await fetchImpl(url, { signal });
} catch (cause) {
const wrapped = wrapAbortError(cause, {
uri,
packageSlug,
relPath,
rootKind,
});
if (wrapped) throw wrapped;
throw cause;
}
```
## API Reference
- `AbortContext` — `{ uri, packageSlug, relPath, rootKind }`.
- `ensureNotAborted(signal, ctx)` — throws if `signal.aborted`.
- `wrapAbortError(cause, ctx)` — returns a `BundleUriError` for abort
causes, `undefined` otherwise.
## Implementation Notes
`isAbortError` does a `typeof object` plus `name === "AbortError"` check
rather than `instanceof DOMException` so the helper works under Node, Bun,
and browsers without a platform-specific dependency. The `signal.reason`
is preserved on `cause` for the pre-aborted path so consumers can surface
why the caller cancelled (`signal.reason` may be any value per the
WHATWG spec).

View File

@@ -0,0 +1,78 @@
# UriLruCache
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`cache.ts` declares the optional in-memory LRU used by the storage shim
when a caller passes `options.cache = { maxBytes }`. The cache is keyed
by the full URI string, holds raw `Uint8Array` bodies, and evicts oldest
entries first until the cumulative byte total is at or below the configured
ceiling. The implementation is intentionally minimal — just enough surface
for the `get` / `set` / `has` triad the shim needs.
The shim defaults to OFF. When the LRU is enabled, every successful `get`
result is inserted; subsequent `get`s of the same URI return the cached
bytes without invoking the injected `FetchLike`. Writes are not relevant
(bundles are immutable, every write throws `READ_ONLY` upstream of the
cache).
## Features
- O(1) `get`/`set`/`has` via the underlying `Map`.
- LRU recency refresh on `get` (delete + re-insert at tail).
- Byte-bounded eviction policy — oldest-first until
`currentBytes <= maxBytes`.
- Single-entry oversize is rejected silently (the entry is not inserted
and no eviction loop fires) so a runaway 500 MB payload cannot flush
the entire cache.
- `bytes` / `size` accessors for diagnostic and test inspection.
## Usage: Basic
```ts
import { UriLruCache } from "@suaveplan/storage-uri-plugin-bundle";
const cache = new UriLruCache(1024);
cache.set("bundle://x/a", new Uint8Array(512));
cache.set("bundle://x/b", new Uint8Array(512));
cache.get("bundle://x/a"); // → Uint8Array (and refresh recency)
cache.set("bundle://x/c", new Uint8Array(512));
cache.has("bundle://x/b"); // → false (evicted as oldest)
```
## Usage: Advanced
```ts
import { UriLruCache } from "@suaveplan/storage-uri-plugin-bundle";
const cache = new UriLruCache(100);
cache.set("k1", new Uint8Array(50));
cache.set("k2", new Uint8Array(50));
cache.set("k3", new Uint8Array(50));
console.log(cache.size, cache.bytes); // → 2 100
console.log(cache.has("k1")); // → false (oldest evicted)
```
## API Reference
- `new UriLruCache(maxBytes)` — construct an empty cache with the
byte ceiling.
- `get(key): Uint8Array | undefined` — refresh recency and return.
- `set(key, value): void` — insert and evict to satisfy the ceiling.
- `has(key): boolean` — non-mutating presence check.
- `size: number` — entry count.
- `bytes: number` — cumulative byte total.
## Implementation Notes
The class delegates everything to a plain `Map`. JavaScript `Map`
preserves insertion order, so `map.keys().next().value` returns the
oldest key — exactly the LRU candidate. The recency refresh on `get`
is a delete + re-insert; the cost is O(1). Cached buffers are shared
references — callers must NOT mutate a returned `Uint8Array` in place
or future cache hits will surface the mutation. The shim guarantees
immutability by treating cache results as read-only, which is the same
invariant the entire `bundle://` contract enforces upstream.

View File

@@ -0,0 +1,91 @@
# BUNDLE_URI error codes
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`error-codes.ts` declares the canonical registry of every error code raised by
`@suaveplan/storage-uri-plugin-bundle` and the `BundleUriError` class that
carries those codes through the structured-error pipeline. Every code listed
here is reachable from a real call site in the package and exercised by an
explicit `it(...)` in the test suite — unused codes are a CI failure per
`AGENTS.md §2c`.
The class extends `SuaveplanError` so that traces, redaction, span events,
and the `walkCauses` chain work uniformly with the rest of the project. The
discriminants (`uri`, `scheme`, `packageSlug`, `relPath`, `rootKind`, `op`,
`actual`, `httpStatus`, `offset`, `length`) surface the dimensions that
operators most commonly filter dashboards on without needing to parse free
text out of `message`.
## Features
- Frozen `BUNDLE_URI_CODES` record produced by `defineCodes` — type-narrowed
values, exhaustive registry inferred by TypeScript.
- `BundleUriError` subclass with a typed `code` plus nine optional structured
discriminants for granular diagnostics.
- All nine codes (`PACKAGE_MISMATCH`, `PATH_TRAVERSAL`, `READ_ONLY`,
`NOT_FOUND`, `INVALID_URI`, `FETCH_FAILED`, `RANGE_INVALID`,
`RANGE_UNSUPPORTED`, `ABORTED`) reachable from real call sites.
- `BundleUriRootKind` enum aligned with the resolution-matrix `root_kind`
span attribute (`file` | `http` | `data` | `blob` | `other`).
## Usage: Basic
```ts
import {
BUNDLE_URI_CODES,
BundleUriError,
} from "@suaveplan/storage-uri-plugin-bundle/error-codes/error-codes";
throw new BundleUriError(BUNDLE_URI_CODES.NOT_FOUND, "asset missing", {
uri: "bundle://my-pkg/assets/x.wav",
packageSlug: "my-pkg",
relPath: "assets/x.wav",
rootKind: "http",
});
```
## Usage: Advanced
```ts
import {
BUNDLE_URI_CODES,
BundleUriError,
} from "@suaveplan/storage-uri-plugin-bundle/error-codes/error-codes";
try {
// …read attempt…
} catch (cause) {
throw new BundleUriError(BUNDLE_URI_CODES.FETCH_FAILED, "upstream 502", {
uri: "bundle://my-pkg/assets/x.wav",
packageSlug: "my-pkg",
rootKind: "http",
httpStatus: 502,
cause,
});
}
```
## API Reference
- `BUNDLE_URI_CODES` — frozen record `{ <REASON>: "BUNDLE_URI.<REASON>" }`.
- `BundleUriErrorCode` — literal-typed union of `BUNDLE_URI.*` strings.
- `BundleUriRootKind` — `"file" | "http" | "data" | "blob" | "other"`.
- `BundleUriErrorOptions` — `SuaveplanErrorOptions` plus optional `uri`,
`scheme`, `packageSlug`, `relPath`, `rootKind`, `op`, `actual`,
`httpStatus`, `offset`, `length`.
- `BundleUriError` — `SuaveplanError<BundleUriErrorCode>` with every option
re-surfaced as a typed public-readonly field.
## Implementation Notes
The class name is captured in `Error.captureStackTrace` so the stack frame
points at the throw site, not at the base-class constructor. Discriminants
are assigned individually (rather than spread) so that
`exactOptionalPropertyTypes` is satisfied without leaking `undefined` keys
into JSON serialisation. The codes registry is constructed once at module
load through `Object.freeze`, matching the convention in every other
`defineCodes` consumer.

View File

@@ -0,0 +1,71 @@
# bundleUriPlugin
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`factory.ts` is the public entry point for `@suaveplan/storage-uri-plugin-bundle`.
`bundleUriPlugin(options)` validates the supplied option bag through
`BundleUriPluginOptionsSchema`, normalises `rootUrl` to a trailing-slash
`URL` instance, defaults `fetch` to `globalThis.fetch.bind(globalThis)`
and `clock` to `Date.now`, then constructs a `UriPlugin` whose `scheme`
is `"bundle"`, whose `adapter` is a `BundleStorageShim`, and whose
`parse` / `build` are bound to the validated options.
Each consuming package calls the factory once from its own `register()`
hook and contributes its plugin to a shared `StorageUriResolver`. The
slug parameter scopes one plugin to one package — calling the resolver
with `bundle://<other-pkg>/x` surfaces `PACKAGE_MISMATCH` at `parse`
rather than producing a silent mis-route.
## Features
- Zod-validated option bag — strict; unknown keys rejected.
- Slug regex enforced at construction (`^@?[a-z0-9][a-z0-9._/-]{0,127}$`).
- `rootUrl` normalised to `URL`; trailing-slash pathname required.
- Default `fetch` resolution via `globalThis.fetch` with a clear error
when no global is available (Node ≤ 17 / hostile sandboxes).
- Returned plugin satisfies `runUriPluginConformance`.
## Usage: Basic
```ts
import { bundleUriPlugin } from "@suaveplan/storage-uri-plugin-bundle";
const plugin = bundleUriPlugin({
packageSlug: "audio-ir-room-plugin-club",
rootUrl: new URL("./", import.meta.url),
});
resolver.register(plugin);
```
## Usage: Advanced
```ts
import { bundleUriPlugin } from "@suaveplan/storage-uri-plugin-bundle";
const plugin = bundleUriPlugin({
packageSlug: "@suaveplan/font-subset",
rootUrl: "https://cdn.example/font-subset/",
fetch: signedFetch,
cache: { maxBytes: 5_000_000 },
});
```
## API Reference
- `bundleUriPlugin(options): UriPlugin` — entry point.
- `validateOptions(options): ResolvedBundleUriPluginOptions` — exported
for tests; throws `BundleUriError(BUNDLE_URI.INVALID_URI)` on failure.
## Implementation Notes
`validateOptions` is exported separately so the test suite can exercise
the validation branches without round-tripping through the full plugin
construction. The trailing-slash check is performed after `URL`
normalisation so a `string` input like `"file:///pkgs/x/assets"`
surfaces as `INVALID_URI` rather than being silently amended — caller
mistakes are loud. `resolveGlobalFetch` is the only place that touches
`globalThis.fetch` so injecting a stub in tests is a one-line change.

View File

@@ -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.

View File

@@ -0,0 +1,91 @@
# OpenTelemetry wiring
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`otel.ts` exposes the tracer, meter, and instrument bag that every read
path in `@suaveplan/storage-uri-plugin-bundle` records against. It mirrors
the convention used by `@suaveplan/storage-uri/otel` and
`@suaveplan/storage-plugin-cache/otel`: lazy instrument acquisition,
test-injectable `MeterProvider`, and a single `recordOp` entry point that
emits the histogram, the success/error counter, and the per-op span in one
call so the call site never forgets one of the three.
The module also carries the small helpers that the rest of the package
relies on for span-attribute hygiene: `sanitiseRelPathAttr` strips
query strings and truncates to 256 chars, and `classifyRootKind` maps
a `URL` to the `file | http | data | blob | other` label used on every
span and counter.
## Features
- `bundle_uri.op_duration_ms{op, root_kind, outcome}` histogram.
- `bundle_uri.reads_total{root_kind, outcome}` counter.
- `bundle_uri.errors_total{root_kind, code}` counter.
- `bundle_uri.range_fallback_total{root_kind}` counter.
- `bundle_uri.cache_hits_total{root_kind}` and
`bundle_uri.cache_misses_total{root_kind}` counters.
- Per-op span on the `@suaveplan/storage-uri-plugin-bundle` tracer with
`bundle.package`, `bundle.rel_path`, `bundle.root_kind`,
`bundle.outcome` attributes.
- `setBundleUriMeterProvider(provider)` + `resetInstruments()` for
deterministic test injection.
## Usage: Basic
```ts
import { recordOp } from "@suaveplan/storage-uri-plugin-bundle";
recordOp({
op: "get",
rootKind: "http",
outcome: "success",
durationMs: 12,
packageSlug: "my-pkg",
relPath: "assets/x.wav",
});
```
## Usage: Advanced
```ts
import {
setBundleUriMeterProvider,
resetInstruments,
} from "@suaveplan/storage-uri-plugin-bundle";
import { MeterProvider } from "@opentelemetry/sdk-metrics";
const provider = new MeterProvider({});
setBundleUriMeterProvider(provider);
resetInstruments();
// …run instrumented code, then drain `provider` to read observations.
```
## API Reference
- `getInstruments(force?)` — lazy-build the bag; `force=true` rebuilds.
- `setBundleUriMeterProvider(provider | undefined)` — test injection.
- `resetInstruments()` — drop the cached bag.
- `recordOp(input)` — one-call emission of histogram + counter + span.
- `recordRangeFallback(rootKind)` — increment fallback counter.
- `recordCacheHit(rootKind)` / `recordCacheMiss(rootKind)` —
cache-path counters.
- `sanitiseRelPathAttr(relPath)` — strip query + truncate to 256 chars.
- `classifyRootKind(rootUrl)` — `URL` → `BundleUriRootKind`.
- `BundleUriOp`, `BundleUriOutcome`, `BundleUriInstruments`,
`RecordOpInput` — supporting types.
## Implementation Notes
The instrument bag is held in a module-private `let` so the same
instruments persist across calls. `resetInstruments` is the test-only
escape hatch that drops the cache so the next `getInstruments` rebinds
against the freshly-installed `MeterProvider`. The 256-char clip is
applied *after* the query strip — query-string lengths are not counted
against the attribute size budget. `classifyRootKind` is a `switch` over
`URL.protocol` (which always ends with `:`); the default case covers
bundler-virtual schemes (`virtual:`, `~asset:`, `\0…`) so coverage
includes the `"other"` branch.

View File

@@ -0,0 +1,80 @@
# parse / build
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`parse.ts` enforces the `bundle://<package-slug>/<rel-path>` grammar
declared in `design.md §1` and rejects every malformed or escaping input
with a typed `BundleUriError`. It delegates RFC 3986 component splitting
to `parseUri` from `@suaveplan/storage-uri` (so length, NUL, whitespace,
and scheme-syntax checks are shared with every other URI plugin) and
layers per-plugin slug enforcement and three-pass path-traversal
prevention on top.
The inverse `build(key, options, meta?)` re-serialises a key plus optional
meta into a canonical `bundle://` URI. The pair satisfies the
round-trip contract `parse(build(key, opts)) === { key, meta }`.
## Features
- Lowercased scheme match per RFC 3986 §3.1 (delegated to `parseUri`).
- Slug enforcement at the boundary — mis-routed URIs surface as
`PACKAGE_MISMATCH` before any I/O.
- Three-pass traversal prevention: raw `..` rejection, post-decode
`..` rejection, defensive URL-resolve `pathname.startsWith` check.
- Pass 3 is skipped for non-hierarchical `data:` / `blob:` roots, where
URL resolution is meaningless (single-resource model per
`design.md §2`).
- `build` round-trips through `parse` for every key the plugin accepts.
## Usage: Basic
```ts
import { parse, build } from "@suaveplan/storage-uri-plugin-bundle";
const opts = {
packageSlug: "my-pkg",
rootUrl: new URL("file:///pkgs/my-pkg/"),
fetch: globalThis.fetch,
clock: () => Date.now(),
};
const parsed = parse("bundle://my-pkg/assets/x.wav", opts);
// → { key: "assets/x.wav", meta: { packageSlug: "my-pkg" } }
const uri = build("assets/x.wav", opts);
// → "bundle://my-pkg/assets/x.wav"
```
## Usage: Advanced
```ts
import { parse } from "@suaveplan/storage-uri-plugin-bundle";
try {
parse("bundle://my-pkg/%2E%2E/etc/passwd", opts);
} catch (err) {
// err.code === "BUNDLE_URI.PATH_TRAVERSAL"
// err.relPath === "%2E%2E/etc/passwd"
}
```
## API Reference
- `parse(uri, options): BundleParsed` — `{ key, meta: { packageSlug } }`
on success; `BundleUriError` on failure.
- `build(key, options, meta?): string` — canonical `bundle://` URI.
- `BundleParsed` — `{ key, meta: { packageSlug } }`.
## Implementation Notes
`parseUri` is consumed via the package barrel to keep the dependency
graph shallow. Pass 1 uses a tight regex `/(^|\/)\.\.(\/|$)/` rather
than splitting on `/` and scanning segments — same semantics, no array
allocation. Pass 2 catches `decodeURIComponent` throwing (malformed
percent-encoding such as `%2`) and rethrows as `INVALID_URI` with the
underlying `URIError` on `cause`. Pass 3 anchors the resolved
pathname against `rootUrl.pathname` (which always ends with `/` by the
factory's normalisation), so a hypothetical input that slips both
earlier passes still cannot escape.

View File

@@ -0,0 +1,92 @@
# Range read primitives
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`range.ts` implements the building blocks the storage shim uses to issue
HTTP `Range` requests and disambiguate the three possible upstream
responses: `206 Partial Content` (range honoured, return body verbatim),
`200 OK` (server ignored the header — slice client-side and increment
the fallback counter), and `416 Range Not Satisfiable` (raise
`BUNDLE_URI.RANGE_INVALID`). Offset/length validation happens before
any fetch so a malformed call is cheap and the OTel counter for failed
ranges is not polluted with caller-side mistakes.
The module is intentionally fetch-agnostic — `sliceForRange` accepts a
status code plus a buffered body, so the call site decides how the
fetch is dispatched and how the `Range` header is composed. That keeps
the file under 300 lines and lets the shim unit-test the slicing
matrix without spinning a server.
## Features
- `validateRange(offset, length, ctx)` — integer + bounds check,
throws `RANGE_INVALID` on violation without dispatching fetch.
- `buildRangeHeader(offset, length)` — canonical
`bytes=<offset>-<offset+length-1>` value.
- `sliceForRange(status, body, offset, length, ctx, rootKind)` —
three-branch disambiguation, returns `{ bytes, fallback }`.
- Sum-overflow guard on `offset + length` against
`Number.MAX_SAFE_INTEGER`.
## Usage: Basic
```ts
import {
validateRange,
buildRangeHeader,
sliceForRange,
} from "@suaveplan/storage-uri-plugin-bundle";
validateRange(0, 10, { uri, relPath });
const header = buildRangeHeader(0, 10); // "bytes=0-9"
const { bytes, fallback } = sliceForRange(
206,
body,
0,
10,
{ uri, relPath },
"http"
);
```
## Usage: Advanced
```ts
import { sliceForRange } from "@suaveplan/storage-uri-plugin-bundle";
// Server ignored the Range header and returned 200 — slice locally
// and increment the fallback counter.
const { bytes, fallback } = sliceForRange(
200,
fullBody,
10,
10,
{ uri, relPath },
"http"
);
if (fallback) recordRangeFallback("http");
```
## API Reference
- `RangeValidationCtx` — `{ uri, relPath }`.
- `RangeOutcome` — `{ bytes, fallback }`.
- `validateRange(offset, length, ctx)` — throws on bad input.
- `buildRangeHeader(offset, length): string`.
- `sliceForRange(status, body, offset, length, ctx, rootKind): RangeOutcome`.
## Implementation Notes
`validateRange` rejects non-integer offsets and lengths, negative
offsets, zero / negative lengths, and the sum overflow that would
otherwise wrap around past `Number.MAX_SAFE_INTEGER`. The 200-fallback
slice is clamped to `body.byteLength` so a caller asking for a slice
beyond EOF receives an undersized (or empty) `Uint8Array` rather than a
throw — matching the WHATWG `Response.arrayBuffer` then `Uint8Array#slice`
behaviour every fetch consumer already expects. 416 is the only
non-2xx status the slicer rewraps; other non-2xx statuses are the
shim's responsibility (`NOT_FOUND`, `FETCH_FAILED`).

View File

@@ -0,0 +1,62 @@
# Storage-shim fetch helpers
## Purpose
`storage-shim.fetch.ts` carries the two cross-cutting helpers every
storage-shim method uses: `safeFetch` dispatches an injected `FetchLike`
and rewraps a thrown cause into the right `BundleUriError`
(`ABORTED` for `AbortError`, `FETCH_FAILED` for anything else), and
`assertSuccess` maps a non-2xx response status to a typed error
(`NOT_FOUND` for 404/410, `FETCH_FAILED` for everything else).
Splitting these out keeps `storage-shim.ts` under the 300-line ceiling
without introducing test-only mocking seams — both helpers are exercised
through real fetch fixtures from the main shim tests.
## Features
- `safeFetch` — single try/catch wrapping the fetch dispatch.
- `assertSuccess` — three-branch status mapping (2xx, 404/410, other).
- `cleanRelPathForAttr` — strip query string for span attributes.
- `FetchContext` value type carrying every discriminant the helpers
surface on thrown errors.
## Usage: Basic
```ts
import {
safeFetch,
assertSuccess,
} from "@suaveplan/storage-uri-plugin-bundle";
const res = await safeFetch(fetchImpl, url, {}, ctx, "GET");
assertSuccess(res, ctx, "GET");
const body = new Uint8Array(await res.arrayBuffer());
```
## Usage: Advanced
```ts
const init: RequestInit = signal ? { signal, method: "HEAD" } : { method: "HEAD" };
const res = await safeFetch(fetchImpl, url, init, ctx, "HEAD");
if (res.status === 404) return false;
assertSuccess(res, ctx, "HEAD");
```
## API Reference
- `safeFetch(fetchImpl, url, init, ctx, description): Promise<Response>`.
- `assertSuccess(res, ctx, description): void` — throws on non-2xx.
- `cleanRelPathForAttr(key): string` — strips query suffix.
- `FetchContext` — `{ uri, packageSlug, relPath, rootKind }`.
## Implementation Notes
`safeFetch` is intentionally async so the awaited rejection is captured
by the surrounding `try`/`catch`; a sync wrapper would let the rejection
bubble synchronously and skip the `wrapAbortError` branch. The
`description` parameter is the operation name embedded in thrown error
messages (`"GET fetch rejected"`, `"HEAD fetch rejected"`,
`"RANGE fetch rejected"`) so operators can grep diagnostics for the
originating call site. `cleanRelPathForAttr` is shared by every shim
method to keep span attributes free of query-string secrets.

View File

@@ -0,0 +1,64 @@
# HEAD-backed exists and getMetadata
## Purpose
`storage-shim.head.ts` implements the two HEAD-routed methods of the
bundle storage shim. `performExists` returns a boolean by mapping
2xx → true, 404/410 → false, anything else → `FETCH_FAILED`.
`performGetMetadata` parses the four headers the `StatResult` contract
recognises (`content-length`, `content-type`, `etag`,
`last-modified`) and returns a sparse object that only carries the keys
the upstream actually provided.
The split keeps the main shim under the 300-line limit and lets the
HEAD pipeline be tested in isolation against a real `node:http`
fixture server.
## Features
- 2xx / 404-410 / other-status disambiguation for `exists`.
- Strict numeric parse on `content-length` (regex
`^\d+$` — rejects negative or floating-point garbage).
- Date-validity check on `last-modified` — invalid `Date` is dropped
rather than surfaced as `Invalid Date`.
- Sparse `StatResult` — only present headers appear on the result so
`exactOptionalPropertyTypes` consumers never see `undefined` keys.
## Usage: Basic
```ts
import {
performExists,
performGetMetadata,
} from "@suaveplan/storage-uri-plugin-bundle";
const ok = await performExists({ options, url, signal, ctx });
const meta = await performGetMetadata({ options, url, signal, ctx });
```
## Usage: Advanced
```ts
const meta = await performGetMetadata({ options, url, signal: undefined, ctx });
if (meta.size !== undefined && meta.size > 10_000_000) {
throw new Error("asset too large");
}
if (meta.lastModified !== undefined) cache.refreshKey(meta.lastModified);
```
## API Reference
- `HeadInput` — `{ options, url, signal, ctx }`.
- `performExists(input): Promise<boolean>`.
- `performGetMetadata(input): Promise<StatResult>`.
## Implementation Notes
`StatResult` is imported from `@suaveplan/storage-uri` (the canonical
shape consumed by the resolver) so the metadata returned by the bundle
shim is plug-compatible with every other URI plugin's response. The
`last-modified` parser uses `new Date(rawHeader)` and discards values
where `getTime()` returns `NaN`; that is the same gate every other
HTTP-stat parser uses. `content-length` is parsed via
`parseInt(_, 10)` rather than `Number(_)` so leading/trailing whitespace
is tolerated but exponents and decimals are rejected.

View File

@@ -0,0 +1,88 @@
# BundleStorageShim
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`storage-shim.ts` declares `BundleStorageShim`, the `StorageLike` adapter
the `bundle://` URI plugin exposes. The class wraps an injected
`FetchLike` and dispatches every read against
`new URL(encodedRelPath, rootUrl)`, surfaces the optional LRU cache, and
threads `AbortSignal` plus OTel instrumentation through every method. The
class deliberately implements the immutable-bundles contract — `set`,
`setPartial`, `delete`, `clear`, and `list` all reject synchronously with
`BUNDLE_URI.READ_ONLY` carrying the offending operation name on the `op`
discriminant for dashboard correlation.
The shim is split across four files in the same feature directory to keep
each under the 300-line ceiling: this file is the class itself,
`storage-shim.fetch.ts` provides `safeFetch` / `assertSuccess`,
`storage-shim.head.ts` provides the HEAD-backed `exists` /
`getMetadata`, and `storage-shim.range.ts` provides the Range pipeline.
## Features
- `get` / `exists` / `getMetadata` / `getPartial` / `createReadStream`
on top of `FetchLike`.
- AbortSignal pre-check + mid-flight rewrap via the abort helpers.
- Optional in-memory LRU front (off unless `options.cache` supplied).
- HTTP `Range` header with 206/200/416 disambiguation; `data:`/`blob:`
roots always slow-pathed.
- Streaming via `Response.body` direct forwarding with one-chunk
fallback when `body` is null.
- Read-only enforcement on every write surface — `READ_ONLY` carries
`op` for filter-friendly diagnostics.
## Usage: Basic
```ts
import { BundleStorageShim } from "@suaveplan/storage-uri-plugin-bundle";
const shim = new BundleStorageShim({
packageSlug: "my-pkg",
rootUrl: new URL("https://cdn.example/my-pkg/"),
fetch: globalThis.fetch.bind(globalThis),
clock: () => performance.now(),
});
const bytes = await shim.get("assets/x.wav");
```
## Usage: Advanced
```ts
import { BundleStorageShim } from "@suaveplan/storage-uri-plugin-bundle";
const shim = new BundleStorageShim({
packageSlug: "my-pkg",
rootUrl: new URL("file:///pkgs/my-pkg/"),
fetch: globalThis.fetch.bind(globalThis),
clock: () => Date.now(),
cache: { maxBytes: 1_000_000 },
});
const partial = await shim.getPartial("assets/big.bin", 0, 1024);
```
## API Reference
- `new BundleStorageShim(resolvedOptions)` — construct.
- `get(key, signal?): Promise<Uint8Array | undefined>`.
- `exists(key, signal?): Promise<boolean>`.
- `getMetadata(key, signal?): Promise<StatResult | undefined>`.
- `getPartial(key, offset, length, signal?): Promise<Uint8Array>`.
- `createReadStream(key, signal?): Promise<ReadableStream<Uint8Array>>`.
- `set` / `setPartial` / `delete` / `clear` / `list` — all reject with
`BUNDLE_URI.READ_ONLY`.
- Public `rootKind` field exposes the resolved root-kind classification
for diagnostics.
## Implementation Notes
`urlOf(key)` returns `rootUrl` directly for `data:`/`blob:` roots since
those schemes are single-resource and `new URL(rel, root)` is either
undefined-behaviour or throws. `ctxFor` builds the per-operation context
once (`uri`, `packageSlug`, `relPath`, `rootKind`) so every helper that
needs the diagnostic fields receives the same shape. `outcomeOf` is a
small private classifier that maps the thrown `BundleUriError` code to
the OTel `outcome` label (`not_found` / `aborted` / `error`).

View File

@@ -0,0 +1,75 @@
# Range read pipeline
## Purpose
`storage-shim.range.ts` carries the full `getPartial` pipeline: pre-fetch
offset/length validation, the `Range: bytes=…` header build, the 206 /
200 / 416 status-code disambiguation, and the
`data:` / `blob:` single-resource fast-fallback path. Extracting the
pipeline keeps the main shim under the 300-line ceiling and lets the
range pipeline be unit-tested in isolation.
The pipeline is fetch-agnostic only in the sense that the dispatch
goes through the injected `FetchLike`; everything else
(slicing, counter increments, error-code resolution) is shared with the
canonical helpers in `range/range.ts`.
## Features
- Pre-fetch `validateRange` so caller-side mistakes never hit the wire.
- HTTP `Range` header with 206/200/416 disambiguation.
- `data:` / `blob:` roots short-circuited to a full-body fetch +
client-side slice + counter increment (single-resource model per
`design.md §2`).
- 416 → `RANGE_INVALID` carrying `httpStatus: 416`, `offset`, `length`.
- 200 fallback path increments `bundle_uri.range_fallback_total`.
## Usage: Basic
```ts
import { performRangeRead } from "@suaveplan/storage-uri-plugin-bundle";
const bytes = await performRangeRead({
options,
url,
key: "assets/x.bin",
offset: 0,
length: 1024,
signal: undefined,
ctx,
rootKind: "http",
});
```
## Usage: Advanced
```ts
const bytes = await performRangeRead({
options,
url: new URL("data:application/octet-stream;base64,AAAA"),
key: "ignored",
offset: 0,
length: 2,
signal: undefined,
ctx,
rootKind: "data",
});
// Range header dropped; full body fetched and sliced; fallback counter ++.
```
## API Reference
- `RangeReadInput` — `{ options, url, key, offset, length, signal,
ctx, rootKind }`.
- `performRangeRead(input): Promise<Uint8Array>`.
## Implementation Notes
The 416 branch drains the response body via
`res.arrayBuffer().catch(() => undefined)` so the underlying connection
is released — some HTTP servers stall the next request on the same
keep-alive connection if the prior body is left hanging. The `200` and
`206` branches share the same `arrayBuffer + sliceForRange` path; the
slicer's `{ bytes, fallback }` tuple is what surfaces the counter-
increment decision. The `data:` / `blob:` short-circuit fetches with
`new URL(rel, root)` ignored because both schemes are single-resource.

View File

@@ -0,0 +1,67 @@
# toReadableStream
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`streaming.ts` exposes the single helper the storage shim consumes from
its `createReadStream` implementation: given a fetch `Response`, return a
`ReadableStream<Uint8Array>` that the caller can pipe, transform, or
consume. The helper is the smallest possible surface that still covers
both the happy path (modern `fetch` returns a non-null `body`) and the
fallback path some polyfills exhibit where `body` is null and the caller
must buffer through `arrayBuffer()`.
The fallback intentionally produces a one-chunk stream rather than
streaming bytes through a TransformStream — once the body has been fully
buffered there is no benefit to chunking, and a single `enqueue` keeps
the downstream consumer's `read()` loop trivially terminating.
## Features
- Direct forwarding of `Response.body` when present — zero copy, zero
buffering.
- One-chunk fallback wrapping the buffered `arrayBuffer()` bytes when
`body` is null.
- Always returns a `ReadableStream<Uint8Array>` (matches the
`StreamingStorageExtensions.createReadStream` contract in
`@suaveplan/storage-uri`).
## Usage: Basic
```ts
import { toReadableStream } from "@suaveplan/storage-uri-plugin-bundle";
const res = await fetch(url);
const stream = await toReadableStream(res);
```
## Usage: Advanced
```ts
import { toReadableStream } from "@suaveplan/storage-uri-plugin-bundle";
const res = await fetchImpl(url, { signal });
const stream = await toReadableStream(res);
for await (const chunk of stream as unknown as AsyncIterable<Uint8Array>) {
consume(chunk);
}
```
## API Reference
- `toReadableStream(response): Promise<ReadableStream<Uint8Array>>` —
forwards `response.body` when non-null, otherwise wraps
`arrayBuffer()` in a one-chunk stream.
## Implementation Notes
The fallback explicitly constructs the `Uint8Array` from the
`ArrayBuffer` returned by `arrayBuffer()`. Using `arrayBuffer()` then
re-wrapping is intentional — some polyfills return a `Response` whose
`body` is null but whose `arrayBuffer()` resolves to the full payload,
and the shim needs to honour the streaming contract regardless. The
returned stream is owned by the caller — once it is consumed (or
cancelled), the underlying response is released.

View File

@@ -0,0 +1,69 @@
# BundleUriPluginOptionsSchema
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`plugin-options.schemas.ts` declares the Zod schema that validates
`BundleUriPluginOptions` at construction time. The schema enforces the
`packageSlug` regex from `design.md §1`, accepts `rootUrl` as either a
string or `URL` instance, and rejects unknown keys via `.strict()` so
caller typos surface rather than silently being ignored.
The schema is consumed by `factory.ts`, which converts Zod validation
failures into `BundleUriError(BUNDLE_URI.INVALID_URI)` carrying the
underlying `ZodError` on `cause` — preserving every issue for diagnostic
serialisation without polluting the public error API with Zod types.
## Features
- Slug regex `^@?[a-z0-9][a-z0-9._/-]{0,127}$` matching the npm-package
name subset declared in the ABNF grammar.
- `rootUrl` accepts both `string` (URL-validated via `z.string().url()`)
and `URL` instance (via `z.instanceof(URL)`).
- `.strict()` rejects unknown keys so option-bag typos are loud.
- Optional `cache.maxBytes` is a positive integer; the LRU layer rejects
non-positive values up-front.
## Usage: Basic
```ts
import { BundleUriPluginOptionsSchema } from "@suaveplan/storage-uri-plugin-bundle";
const parsed = BundleUriPluginOptionsSchema.parse({
packageSlug: "my-pkg",
rootUrl: "https://cdn.example/my-pkg/",
});
```
## Usage: Advanced
```ts
import { BundleUriPluginOptionsSchema } from "@suaveplan/storage-uri-plugin-bundle";
const result = BundleUriPluginOptionsSchema.safeParse({
packageSlug: "Bad Slug",
rootUrl: new URL("file:///x/"),
});
if (!result.success) {
console.error(result.error.issues);
}
```
## API Reference
- `BundleUriPluginOptionsSchema` — Zod object schema with `.strict()`.
- `BundleUriPluginOptionsInput` — `z.input<typeof schema>`.
## Implementation Notes
The schema does NOT normalise `rootUrl` to a `URL` instance — that
happens in `factory.ts` once a value is in hand — because Zod transforms
make the output type harder to consume from the surrounding TypeScript.
`z.custom` is used for `fetch` and `clock` rather than a typed function
schema because Zod's `z.function()` produces a wrapper that is not byte-
for-byte compatible with `FetchLike`/`Clock` under
`exactOptionalPropertyTypes`. The default `import z from "zod"` form is
mandated by the project memory for Bun-safe parsing.

View File

@@ -0,0 +1,73 @@
# BundleUriPluginOptions
## Purpose
`plugin-options.types.ts` declares the public option bag accepted by
`bundleUriPlugin(...)` and the internal `ResolvedBundleUriPluginOptions`
shape produced after Zod validation has normalised every input.
The split exists because callers pass a permissive surface — `rootUrl` may
be a string or `URL`, `fetch`/`logger`/`clock` are all optional — while
the rest of the package consumes the post-validation shape where `rootUrl`
is always a `URL`, `fetch` always defined, and `clock` always present.
## Features
- Required `packageSlug` and `rootUrl`, optional dependencies.
- Structural typing — no runtime classes — keeps the types-only file
importable from anywhere without pulling runtime code.
- `ResolvedBundleUriPluginOptions` matches the contract of `factory.ts`
so the rest of the codebase consumes a single canonical shape.
## Usage: Basic
```ts
import type { BundleUriPluginOptions } from "@suaveplan/storage-uri-plugin-bundle";
const opts: BundleUriPluginOptions = {
packageSlug: "audio-ir-room-plugin-club",
rootUrl: new URL("./", import.meta.url),
};
```
## Usage: Advanced
```ts
import type {
BundleUriPluginOptions,
ResolvedBundleUriPluginOptions,
} from "@suaveplan/storage-uri-plugin-bundle";
declare function bundleUriPlugin(o: BundleUriPluginOptions): {
readonly resolved: ResolvedBundleUriPluginOptions;
};
const out = bundleUriPlugin({
packageSlug: "@suaveplan/font-subset",
rootUrl: "https://cdn.example/font/",
cache: { maxBytes: 1_000_000 },
fetch: globalThis.fetch.bind(globalThis),
});
```
## API Reference
- `BundleUriPluginOptions` — public surface (everything but `packageSlug`
and `rootUrl` is optional).
- `ResolvedBundleUriPluginOptions` — post-validation shape consumed by the
rest of the package.
## Implementation Notes
`FetchLike`, `LoggerLike`, and `Clock` are imported from `@suaveplan/types`
rather than redeclared so the plugin transparently composes with every
other package that consumes the same canonical contracts.
The file deliberately carries no runtime exports — it is excluded from
coverage by the default `*.types.ts` rule in `createTestConfig`. The
public shape uses `readonly` modifiers throughout because every
downstream consumer treats the option bag as a value object, never
mutating it; rebinding via spread is the supported pattern when an
override is needed at runtime. The resolved shape preserves the
`logger` and `cache` optional-keys so `exactOptionalPropertyTypes`
consumers can detect "intentionally absent" versus "explicitly
undefined" without ambiguity.