Files
suaveplan/packages/media/audio-ir/audio-ir-loader/src/loader/loader.md
2026-05-27 23:47:53 +01:00

3.6 KiB

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

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:

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<string, AudioDecoder> — must be non-empty; keys are case-insensitive.
  • options.cache?: { maxBytes: number } — positive integer; enables LRU.

IrLoader

  • load(id, { signal? }?): Promise<ImpulseResponse>
  • preload(ids, { signal?, concurrency? }?): Promise<readonly ImpulseResponse[]>
  • evict(id): boolean
  • cacheStats(): { entries, bytes, hits, misses }

AudioDecoder

(bytes: Uint8Array, opts?: { signal?: AbortSignal }) => Promise<DecodedAudioLike>.

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.<FMT>.*, 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.