Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,102 @@
# `error-codes.ts` — `AUDIO_IR_LOADER.*` registry
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module is the single source of truth for every error code raised by
`@suaveplan/audio-ir-loader`. It uses `defineCodes` from `@suaveplan/error`
to register an immutable, byte-stable mapping of short keys (`MANIFEST_MISMATCH`)
to fully-qualified strings (`AUDIO_IR_LOADER.MANIFEST_MISMATCH`). It also
declares the `AudioIrLoaderError` class, which exposes every dimension an
operator might filter on (`id`, `extension`, `field`, `manifestValue`,
`decodedValue`, `maxBytes`, `decodedBytes`, `errors`) as typed read-only
properties so dashboards and routing layers never have to parse message
text to know what happened.
## Features
- Six codes — `DECODER_UNAVAILABLE`, `MANIFEST_MISMATCH`, `ABORTED`,
`CACHE_OVERFLOW`, `DECODE_FAILED`, `RESOLVE_FAILED`.
- Frozen registry object — `Object.isFrozen(AUDIO_IR_LOADER_CODES)` is true.
- Typed error class with discriminants pre-typed on the class.
- Aggregate `errors[]` for the `DECODE_FAILED` partial-failure path from
`preload`.
## Usage: Basic
```ts
import {
AUDIO_IR_LOADER_CODES,
AudioIrLoaderError,
} from "@suaveplan/audio-ir-loader";
throw new AudioIrLoaderError(
AUDIO_IR_LOADER_CODES.DECODER_UNAVAILABLE,
"no decoder registered for .flac",
{ id: "club-club1", extension: "flac" }
);
```
## Usage: Advanced
The aggregate `errors` discriminant powers the `preload` partial-failure
contract:
```ts
import { AudioIrLoaderError, AUDIO_IR_LOADER_CODES } from "@suaveplan/audio-ir-loader";
try {
await loader.preload(["a", "b", "c"]);
} catch (err) {
if (
err instanceof AudioIrLoaderError &&
err.code === AUDIO_IR_LOADER_CODES.DECODE_FAILED
) {
for (const { id, error } of err.errors ?? []) {
console.error(`load ${id} failed:`, error.code);
}
}
}
```
Operators routing on the structured discriminants can ship dashboards that
break down failures by `extension` for `DECODER_UNAVAILABLE`, by `field` for
`MANIFEST_MISMATCH`, and by the size pair (`maxBytes` / `decodedBytes`) for
`CACHE_OVERFLOW`, with zero message-text regexes.
## API Reference
### `AUDIO_IR_LOADER_CODES`
Frozen `Readonly<Record<Key, "AUDIO_IR_LOADER.<Key>">>` with keys
`DECODER_UNAVAILABLE`, `MANIFEST_MISMATCH`, `ABORTED`, `CACHE_OVERFLOW`,
`DECODE_FAILED`, `RESOLVE_FAILED`.
### `AudioIrLoaderError extends SuaveplanError`
Constructor: `(code, message, options?)`. Discriminants on the instance:
`id`, `extension`, `field`, `manifestValue`, `decodedValue`, `maxBytes`,
`decodedBytes`, `errors`. `name === "AudioIrLoaderError"`.
### `AudioIrLoaderErrorOptions`
Extends `SuaveplanErrorOptions` with the seven optional discriminants
listed on the class plus `errors: readonly AggregatedLoadFailure[]`.
### `AggregatedLoadFailure`
`{ id: string; error: SuaveplanError }` — one entry per failed id in a
`preload` aggregate.
## Implementation Notes
`defineCodes` returns a frozen object; the registry guarantees byte-stable
code strings across builds. The discriminants on `AudioIrLoaderError` are
declared as `T | undefined` rather than via `exactOptionalPropertyTypes`
gymnastics so callers reading the property always get an unambiguous
"absent" sentinel. Every code in this registry is exercised by a real
call site under test (see `error-codes.test.ts`) — the exhaustion test
guarantees we cannot register a code that has no production thrower.

View File

@@ -0,0 +1,331 @@
import type { IrManifestEntry } from "@suaveplan/audio-ir-core";
import { createIrRegistry } from "@suaveplan/audio-ir-core";
import {
createStorageUriResolver,
type UriPlugin,
} from "@suaveplan/storage-uri";
import { describe, expect, it } from "@suaveplan/testing/runner";
import { ImpulseResponseCache } from "../cache/cache.js";
import { type AudioDecoder, createIrLoader } from "../loader/loader.js";
import { runPreload } from "../loader/preload.js";
import { resetInstruments } from "../otel/otel.js";
import {
AUDIO_IR_LOADER_CODES,
AudioIrLoaderError,
type AudioIrLoaderErrorCode,
} from "./error-codes.js";
const ENTRY: IrManifestEntry = Object.freeze({
id: "club-club1",
family: "room",
uri: "bundle://x/club1.wav",
channels: 1,
sampleRate: 44100,
gainDb: 0,
license: "MIT",
source: "demo",
}) as IrManifestEntry;
const FLAC_ENTRY: IrManifestEntry = Object.freeze({
...ENTRY,
id: "club-club2",
uri: "bundle://x/club2.flac",
}) as IrManifestEntry;
function buildResolver(reads: Record<string, Uint8Array>): {
resolver: ReturnType<typeof createStorageUriResolver>;
plugin: UriPlugin;
} {
const plugin: UriPlugin = {
scheme: "bundle",
adapter: {
async get(key: string) {
return reads[key];
},
async set() {
return;
},
async delete() {
return true;
},
async exists(key) {
return key in reads;
},
async list() {
return { keys: [], total: 0, hasMore: false };
},
async clear() {
return;
},
},
parse(uri: string) {
const noScheme = uri.replace(/^bundle:\/\//, "");
return { key: noScheme };
},
};
return { resolver: createStorageUriResolver([plugin]), plugin };
}
const noopWavBytes = new Uint8Array([1, 2, 3]);
function decoderThrowing(code: string): AudioDecoder {
return async () => {
const err = new Error(`fixture decoder threw ${code}`);
throw err;
};
}
async function exerciseDecoderUnavailable(): Promise<
AudioIrLoaderError | undefined
> {
resetInstruments();
const registry = createIrRegistry();
registry.register(FLAC_ENTRY, {
manifestSlug: "club",
manifestFamily: "room",
});
const { resolver } = buildResolver({ "x/club2.flac": noopWavBytes });
const loader = createIrLoader({
registry,
resolver,
decoders: { wav: decoderThrowing("nope") },
});
try {
await loader.load("club-club2");
} catch (err) {
if (err instanceof AudioIrLoaderError) return err;
}
return undefined;
}
async function exerciseManifestMismatch(): Promise<
AudioIrLoaderError | undefined
> {
resetInstruments();
const registry = createIrRegistry();
registry.register(ENTRY, {
manifestSlug: "club",
manifestFamily: "room",
});
const { resolver } = buildResolver({ "x/club1.wav": noopWavBytes });
const decoder: AudioDecoder = async () => ({
channelData: [new Float32Array([0.1])],
channels: 1,
sampleRate: 22050,
});
const loader = createIrLoader({
registry,
resolver,
decoders: { wav: decoder },
});
try {
await loader.load("club-club1");
} catch (err) {
if (err instanceof AudioIrLoaderError) return err;
}
return undefined;
}
async function exerciseAborted(): Promise<AudioIrLoaderError | undefined> {
resetInstruments();
const registry = createIrRegistry();
registry.register(ENTRY, {
manifestSlug: "club",
manifestFamily: "room",
});
const { resolver } = buildResolver({ "x/club1.wav": noopWavBytes });
const decoder: AudioDecoder = async () => ({
channelData: [new Float32Array([0])],
channels: 1,
sampleRate: 44100,
});
const loader = createIrLoader({
registry,
resolver,
decoders: { wav: decoder },
});
const controller = new AbortController();
controller.abort(new Error("preflight"));
try {
await loader.load("club-club1", { signal: controller.signal });
} catch (err) {
if (err instanceof AudioIrLoaderError) return err;
}
return undefined;
}
async function exerciseCacheOverflow(): Promise<
AudioIrLoaderError | undefined
> {
const cache = new ImpulseResponseCache(64);
const big = Object.freeze({
id: "big",
channelData: Object.freeze([new Float32Array(1024)]),
channels: 1,
sampleRate: 44100,
lengthFrames: 1024,
durationSec: 1024 / 44100,
gainDb: 0,
family: "room" as const,
manifest: ENTRY,
});
try {
cache.put("big", big as unknown as Parameters<typeof cache.put>[1]);
} catch (err) {
if (err instanceof AudioIrLoaderError) return err;
}
return undefined;
}
async function exerciseDecodeFailed(): Promise<AudioIrLoaderError | undefined> {
resetInstruments();
const registry = createIrRegistry();
registry.register(ENTRY, {
manifestSlug: "club",
manifestFamily: "room",
});
const { resolver } = buildResolver({ "x/club1.wav": noopWavBytes });
const decoder: AudioDecoder = async () => ({
channelData: [new Float32Array([0])],
channels: 99,
sampleRate: 44100,
});
const loader = createIrLoader({
registry,
resolver,
decoders: { wav: decoder },
});
try {
await runPreload(
(id, opts) => loader.load(id, opts),
["club-club1"],
undefined
);
} catch (err) {
if (err instanceof AudioIrLoaderError) return err;
}
return undefined;
}
async function exerciseResolveFailed(): Promise<
AudioIrLoaderError | undefined
> {
resetInstruments();
const registry = createIrRegistry();
registry.register(ENTRY, {
manifestSlug: "club",
manifestFamily: "room",
});
const { resolver } = buildResolver({ "x/club1.wav": noopWavBytes });
const decoder: AudioDecoder = async () => ({
channelData: [new Float32Array([0])],
channels: 1,
sampleRate: 44100,
});
const loader = createIrLoader({
registry,
resolver,
decoders: { wav: decoder },
});
// Build a fake load fn that throws a non-SuaveplanError to trigger
// the RESOLVE_FAILED wrapper inside runPreload.
const badLoader = async () => {
throw new TypeError("plain error");
};
try {
await runPreload(
badLoader as unknown as (id: string) => Promise<never>,
["x"],
undefined
);
} catch (err) {
// top-level err is DECODE_FAILED whose `errors[0].error` is the RESOLVE_FAILED
if (err instanceof AudioIrLoaderError && err.errors !== undefined) {
const first = err.errors[0]?.error;
if (first instanceof AudioIrLoaderError) return first;
}
}
// touch loader to avoid unused warning
void loader;
return undefined;
}
function siteFor(
code: AudioIrLoaderErrorCode
): () => Promise<AudioIrLoaderError | undefined> {
if (code === AUDIO_IR_LOADER_CODES.DECODER_UNAVAILABLE)
return exerciseDecoderUnavailable;
if (code === AUDIO_IR_LOADER_CODES.MANIFEST_MISMATCH)
return exerciseManifestMismatch;
if (code === AUDIO_IR_LOADER_CODES.ABORTED) return exerciseAborted;
if (code === AUDIO_IR_LOADER_CODES.CACHE_OVERFLOW)
return exerciseCacheOverflow;
if (code === AUDIO_IR_LOADER_CODES.DECODE_FAILED)
return exerciseDecodeFailed;
if (code === AUDIO_IR_LOADER_CODES.RESOLVE_FAILED)
return exerciseResolveFailed;
return async () => undefined;
}
describe("AUDIO_IR_LOADER_CODES exhaustion", () => {
it("every AUDIO_IR_LOADER code reachable from a real call site", {
timeout: 30000,
}, async () => {
const all = Object.values(AUDIO_IR_LOADER_CODES);
expect(all.length).toBe(6);
for (const code of all) {
const err = await siteFor(code)();
expect(err, `no real call site exercises ${code}`).toBeDefined();
expect(err?.code).toBe(code);
}
});
it("registry exposes exactly six codes", { timeout: 5000 }, () => {
const names = Object.keys(AUDIO_IR_LOADER_CODES).sort();
expect(names).toEqual([
"ABORTED",
"CACHE_OVERFLOW",
"DECODER_UNAVAILABLE",
"DECODE_FAILED",
"MANIFEST_MISMATCH",
"RESOLVE_FAILED",
]);
});
it("AUDIO_IR_LOADER_CODES registry is frozen", { timeout: 5000 }, () => {
expect(Object.isFrozen(AUDIO_IR_LOADER_CODES)).toBe(true);
});
it("AudioIrLoaderError surfaces typed discriminants", {
timeout: 5000,
}, () => {
const err = new AudioIrLoaderError(
AUDIO_IR_LOADER_CODES.MANIFEST_MISMATCH,
"x",
{
id: "club-a",
field: "channels",
manifestValue: 2,
decodedValue: 1,
}
);
expect(err.name).toBe("AudioIrLoaderError");
expect(err.id).toBe("club-a");
expect(err.field).toBe("channels");
expect(err.manifestValue).toBe(2);
expect(err.decodedValue).toBe(1);
});
it("AudioIrLoaderError with no options leaves discriminants undefined", {
timeout: 5000,
}, () => {
const err = new AudioIrLoaderError(AUDIO_IR_LOADER_CODES.ABORTED, "x");
expect(err.id).toBeUndefined();
expect(err.extension).toBeUndefined();
expect(err.field).toBeUndefined();
expect(err.manifestValue).toBeUndefined();
expect(err.decodedValue).toBeUndefined();
expect(err.maxBytes).toBeUndefined();
expect(err.decodedBytes).toBeUndefined();
expect(err.errors).toBeUndefined();
});
});

View File

@@ -0,0 +1,87 @@
import {
defineCodes,
SuaveplanError,
type SuaveplanErrorOptions,
} from "@suaveplan/error";
/**
* Frozen registry of every error code raised by `@suaveplan/audio-ir-loader`.
*
* Six codes total — `DECODER_UNAVAILABLE`, `MANIFEST_MISMATCH`, `ABORTED`,
* `CACHE_OVERFLOW`, `DECODE_FAILED`, `RESOLVE_FAILED`. Every code is
* reachable from a real production call site exercised by the test suite.
*
* Errors from upstream packages (`AUDIO_IR.*`, `STORAGE_URI.*`,
* `BUNDLE_URI.*`, `FALLBACK_URI.*`, `DRIVE_URI.*`, `AUDIO_DECODE.<FMT>.*`)
* pass through verbatim — the loader does NOT re-wrap them.
*/
export const AUDIO_IR_LOADER_CODES = defineCodes("AUDIO_IR_LOADER", [
"DECODER_UNAVAILABLE",
"MANIFEST_MISMATCH",
"ABORTED",
"CACHE_OVERFLOW",
"DECODE_FAILED",
"RESOLVE_FAILED",
] as const);
export type AudioIrLoaderErrorCode =
(typeof AUDIO_IR_LOADER_CODES)[keyof typeof AUDIO_IR_LOADER_CODES];
/** Per-id failure entry carried by aggregate `DECODE_FAILED` errors. */
export interface AggregatedLoadFailure {
readonly id: string;
readonly error: SuaveplanError;
}
export interface AudioIrLoaderErrorOptions extends SuaveplanErrorOptions {
/** The IR identifier the operation targeted. */
id?: string | undefined;
/** File extension extracted from the URI for DECODER_UNAVAILABLE. */
extension?: string | undefined;
/** Field on which the manifest diverged from the decoded audio. */
field?: "channels" | "sampleRate" | undefined;
/** Manifest-declared value for MANIFEST_MISMATCH. */
manifestValue?: number | undefined;
/** Decoded value for MANIFEST_MISMATCH. */
decodedValue?: number | undefined;
/** Configured cache budget for CACHE_OVERFLOW. */
maxBytes?: number | undefined;
/** Decoded byte total for CACHE_OVERFLOW. */
decodedBytes?: number | undefined;
/** Per-id error list for aggregate DECODE_FAILED throws. */
errors?: readonly AggregatedLoadFailure[] | undefined;
}
/**
* Structured error for every `AUDIO_IR_LOADER.*` throw.
*
* Every dimension an operator filters on is a typed discriminant — never
* parsed from message text. All fields are exposed as readonly properties.
*/
export class AudioIrLoaderError extends SuaveplanError<AudioIrLoaderErrorCode> {
public readonly id: string | undefined;
public readonly extension: string | undefined;
public readonly field: "channels" | "sampleRate" | undefined;
public readonly manifestValue: number | undefined;
public readonly decodedValue: number | undefined;
public readonly maxBytes: number | undefined;
public readonly decodedBytes: number | undefined;
public readonly errors: readonly AggregatedLoadFailure[] | undefined;
public constructor(
code: AudioIrLoaderErrorCode,
message: string,
options?: AudioIrLoaderErrorOptions
) {
super(code, message, options);
this.id = options?.id;
this.extension = options?.extension;
this.field = options?.field;
this.manifestValue = options?.manifestValue;
this.decodedValue = options?.decodedValue;
this.maxBytes = options?.maxBytes;
this.decodedBytes = options?.decodedBytes;
this.errors = options?.errors;
this.name = "AudioIrLoaderError";
}
}

View File

@@ -0,0 +1,9 @@
export type {
AggregatedLoadFailure,
AudioIrLoaderErrorCode,
AudioIrLoaderErrorOptions,
} from "./error-codes.js";
export {
AUDIO_IR_LOADER_CODES,
AudioIrLoaderError,
} from "./error-codes.js";