Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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).
|
||||
78
plugins/document-navigator/docs/persistence/storage/storage-uri-plugin-bundle/src/cache/cache.md
vendored
Normal file
78
plugins/document-navigator/docs/persistence/storage/storage-uri-plugin-bundle/src/cache/cache.md
vendored
Normal 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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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`).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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`).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user