# `loader.ts` — `createIrLoader` pipeline orchestration _(no submodules)_ ## Purpose This module is the entry point for the package. `createIrLoader(options)` returns a typed `IrLoader` that composes the IR family pipeline: `IrRegistry.get(id)` → `cache.get(id)` (on hit, fast path) → `StorageUriResolver.read(uri)` → `decoders[ext](bytes)` → `validateAndFinalize(entry, decoded)` → `cache.put` → return frozen `ImpulseResponse`. Every layer's errors pass through verbatim — the loader only owns the six `AUDIO_IR_LOADER.*` codes from its own contract. ## Features - Zod-validated options at construction (`createIrLoader` throws `ZodError` on misuse). - Decoder keys normalised to lowercase so callers can write `{ WAV: decodeWav }` and have it work. - Optional in-process LRU cache controlled by `options.cache.maxBytes`. - `AbortSignal` threading: short-circuits a pre-aborted signal, passes the signal through to both `resolver.read` and the decoder. - Real OpenTelemetry instrumentation: span + duration histogram + bytes / frames / cache / error counters. - Bounded-concurrency `preload(ids, { concurrency })` with aggregate failure reporting (delegated to `preload.ts`). ## Usage: Basic ```ts import { createStorageUriResolver } from "@suaveplan/storage-uri"; import { bundleUriPlugin } from "@suaveplan/storage-uri-plugin-bundle"; import { decodeWav } from "@suaveplan/audio-decode-wav"; import { createIrLoader } from "@suaveplan/audio-ir-loader"; const resolver = createStorageUriResolver([ bundleUriPlugin({ packageSlug: "club", rootUrl: new URL("file:///opt/ir/club/") }), ]); const loader = createIrLoader({ registry, resolver, decoders: { wav: decodeWav }, }); const ir = await loader.load("club-club1"); ``` ## Usage: Advanced The loader composes well with an LRU cache and with bounded-concurrency preload for app bootstrap: ```ts const loader = createIrLoader({ registry, resolver, decoders: { wav: decodeWav, flac: decodeFlac }, cache: { maxBytes: 64 * 1024 * 1024 }, }); const controller = new AbortController(); const [a, b, c] = await loader.preload( ["club-a", "club-b", "club-c"], { signal: controller.signal, concurrency: 2 } ); // Later, free a specific IR's slot: loader.evict("club-c"); console.log(loader.cacheStats()); ``` ## API Reference ### `createIrLoader(options): IrLoader` - `options.registry: IrRegistry` - `options.resolver: StorageUriResolver` - `options.decoders: Record` — must be non-empty; keys are case-insensitive. - `options.cache?: { maxBytes: number }` — positive integer; enables LRU. ### `IrLoader` - `load(id, { signal? }?): Promise` - `preload(ids, { signal?, concurrency? }?): Promise` - `evict(id): boolean` - `cacheStats(): { entries, bytes, hits, misses }` ### `AudioDecoder` `(bytes: Uint8Array, opts?: { signal?: AbortSignal }) => Promise`. ## Implementation Notes The OTel span for every `load` is started before the cache hit-check so even cache hits emit a span; the `outcome=cache_hit` histogram label distinguishes those from the `outcome=success` "did real work" path. The `SuaveplanError`-cast on errors lets the loader's `errors_total` counter attribute the throw to its originating namespace (`AUDIO_IR.*`, `STORAGE_URI.*`, `BUNDLE_URI.*`, `FALLBACK_URI.*`, `AUDIO_DECODE..*`, or `AUDIO_IR_LOADER.*`) rather than collapsing every miss into a single opaque bucket. The pre-abort check throws synchronously before any registry / resolver / decoder call, satisfying the spec's "never invoked when signal is already aborted" requirement.