Files
2026-05-27 23:47:53 +01:00

91 lines
3.3 KiB
Markdown

# `extension.ts` — URI → file-extension extraction
## Purpose
The loader dispatches decoders by file extension. This module owns the
canonicalisation step: given a URI string (which may be `bundle://`,
`https://`, `drive://`, plain path-based, or `fallback://?u=…&u=…`),
return the lowercase extension that decides which `AudioDecoder` runs.
Cross-format `fallback://` URIs are explicitly rejected with
`DECODER_UNAVAILABLE` — a single load must pick a single decoder.
## Features
- Universal across `bundle://`, `https://`, `drive://`, and arbitrary
path-based URIs.
- Recursive handling for `fallback://` URIs — every candidate is
re-extracted and the result must agree across candidates.
- Query and fragment components are stripped before extension extraction.
- Throws `AUDIO_IR_LOADER.DECODER_UNAVAILABLE` on every failure mode
(missing extension, parse error, candidate disagreement).
## Usage: Basic
```ts
import { extractExtension } from "@suaveplan/audio-ir-loader";
extractExtension("bundle://x/foo.wav"); // → "wav"
extractExtension("https://cdn.example.com/path/clip.WAV?v=2"); // → "wav"
extractExtension("fallback://?u=bundle%3A%2F%2Fx%2Fa.wav&u=https%3A%2F%2Fh%2Fa.wav");
// → "wav"
```
## Usage: Advanced
The loader uses this helper at the start of every `load(id)` to pick the
right decoder before any I/O happens:
```ts
import {
AUDIO_IR_LOADER_CODES,
AudioIrLoaderError,
extractExtension,
} from "@suaveplan/audio-ir-loader";
function pickDecoder<D>(decoders: Record<string, D>, uri: string, id: string): D {
const ext = extractExtension(uri, id);
const decoder = decoders[ext];
if (decoder === undefined) {
throw new AudioIrLoaderError(
AUDIO_IR_LOADER_CODES.DECODER_UNAVAILABLE,
`no decoder registered for '.${ext}'`,
{ id, extension: ext }
);
}
return decoder;
}
```
Cross-format fallback URIs surface as a single `DECODER_UNAVAILABLE`
throw with the first extension preserved on the error's `extension`
discriminant, which makes the diagnostic easy to act on in the manifest
authoring step.
## API Reference
### `extractExtension(uri: string, id?: string): string`
Returns the lowercase extension. The optional `id` is forwarded to the
`AudioIrLoaderError.id` discriminant on every throw so observers can
filter by IR identifier.
Throws `AUDIO_IR_LOADER.DECODER_UNAVAILABLE` when:
- the URI carries no `.<ext>` segment after the last `/`,
- the URI is `fallback://` and is not parseable as a URL,
- the URI is `fallback://` with zero `u=` candidates,
- the URI is `fallback://` and candidates disagree on extension.
## Implementation Notes
The `bundle://x/y.wav` URI form is technically not a valid RFC 3986
authority-less URI but every consumer of `audio-ir-loader` already deals
with it through `storage-uri-plugin-bundle`'s own parser; the
path-stripping in this module is intentionally permissive about authority
shape so callers get a consistent dispatch result regardless of whether
the path-component is `//host/key.wav` or `:/path/key.wav`. The
`fallback://` walk delegates to `extractExtension` recursively, so any
future URI scheme that itself wraps candidates (e.g. a hypothetical
`mirror://`) would only need to extend `FALLBACK_PREFIX` matching and
fall through to the same recursion.