91 lines
3.3 KiB
Markdown
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.
|