# `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(decoders: Record, 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 `.` 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.