Initial Commit
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
# `error-codes.ts`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module declares the canonical error-code registry for
|
||||
`@suaveplan/audio-codec-ffmpeg-wasm`. It exposes the nine `AUDIO_CODEC_FFMPEG_WASM.*`
|
||||
codes the package can throw, an immutable metadata record describing each code,
|
||||
the `AudioCodecFfmpegWasmError` class that every codec fault inherits from, and
|
||||
an idempotent registration hook so external systems (OTel attribute binders,
|
||||
report generators) can record that the registry has been touched.
|
||||
|
||||
## Features
|
||||
|
||||
- Nine stable error codes covering core-load failures, worker spawn, decode,
|
||||
encode, timeout, abort, unsupported codec, OOM and COI gating.
|
||||
- `AudioCodecFfmpegWasmError` class — extends `SuaveplanError`, carries
|
||||
`category`, `code`, `context`, and the originating `cause`.
|
||||
- Frozen `AUDIO_CODEC_FFMPEG_WASM_ERROR_METADATA` record with description and
|
||||
severity per code.
|
||||
- `registerAudioCodecFfmpegWasmErrorCodes()` no-op hook for downstream wiring.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
AudioCodecFfmpegWasmError,
|
||||
AUDIO_CODEC_FFMPEG_WASM_CODES,
|
||||
} from "@suaveplan/audio-codec-ffmpeg-wasm/error-codes";
|
||||
|
||||
throw new AudioCodecFfmpegWasmError(
|
||||
AUDIO_CODEC_FFMPEG_WASM_CODES.UNSUPPORTED_CODEC,
|
||||
"audio-codec-ffmpeg-wasm.encode.unknown-format",
|
||||
{ context: { format: "alac" } },
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
AUDIO_CODEC_FFMPEG_WASM_ERROR_METADATA,
|
||||
type AudioCodecFfmpegWasmErrorCode,
|
||||
} from "@suaveplan/audio-codec-ffmpeg-wasm/error-codes";
|
||||
|
||||
function severityFor(code: AudioCodecFfmpegWasmErrorCode): string {
|
||||
return AUDIO_CODEC_FFMPEG_WASM_ERROR_METADATA[code].severity;
|
||||
}
|
||||
```
|
||||
|
||||
The metadata record is frozen so downstream consumers can safely lift it into
|
||||
read-only contexts (OTel attribute maps, code-to-i18n-key tables). Throw sites
|
||||
elsewhere in the package always carry a `context` payload — `coreUrl`,
|
||||
`format`, `elapsedMs`, `timeoutMs`, `requestedBytes`, or the Zod issues list.
|
||||
|
||||
## API Reference
|
||||
|
||||
- `AUDIO_CODEC_FFMPEG_WASM_CODES` — `defineCodes("AUDIO_CODEC_FFMPEG_WASM", […])`
|
||||
result; key → namespaced code string.
|
||||
- `type AudioCodecFfmpegWasmErrorCode` — union of every code string value.
|
||||
- `AUDIO_CODEC_FFMPEG_WASM_ERROR_METADATA` — frozen `Record` of metadata.
|
||||
- `class AudioCodecFfmpegWasmError extends SuaveplanError<…>` — instances expose
|
||||
`category = "audio-codec-ffmpeg-wasm"`, `code`, `messageKey`, `context`,
|
||||
`cause`.
|
||||
- `registerAudioCodecFfmpegWasmErrorCodes()` — idempotent no-op.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The metadata record is `Object.freeze`d so it cannot be mutated at runtime —
|
||||
this keeps the OTel binder behaviour deterministic. The class has a `category`
|
||||
field so the constructor body is non-trivial, sidestepping the `useless-constructor`
|
||||
biome rule without any `biome-ignore` comment (CLAUDE.md rule 9). Every code
|
||||
emitted by this package is reachable from a real call site exercised in
|
||||
`error-codes.test.ts`, satisfying the §2c reachability requirement.
|
||||
@@ -0,0 +1,51 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import {
|
||||
AUDIO_CODEC_FFMPEG_WASM_CODES,
|
||||
AUDIO_CODEC_FFMPEG_WASM_ERROR_METADATA,
|
||||
AudioCodecFfmpegWasmError,
|
||||
registerAudioCodecFfmpegWasmErrorCodes,
|
||||
} from "./error-codes.js";
|
||||
|
||||
describe("error-codes", () => {
|
||||
it("every code has metadata with description and severity", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
for (const code of Object.values(AUDIO_CODEC_FFMPEG_WASM_CODES)) {
|
||||
const meta = AUDIO_CODEC_FFMPEG_WASM_ERROR_METADATA[code];
|
||||
expect(typeof meta.description).toBe("string");
|
||||
expect(meta.description.length).toBeGreaterThan(0);
|
||||
expect(["warning", "error"]).toContain(meta.severity);
|
||||
}
|
||||
});
|
||||
|
||||
it("AudioCodecFfmpegWasmError constructs with code, context, cause", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const cause = new Error("boom");
|
||||
const err = new AudioCodecFfmpegWasmError(
|
||||
AUDIO_CODEC_FFMPEG_WASM_CODES.DECODE_FAILED,
|
||||
"audio-codec-ffmpeg-wasm.test.message",
|
||||
{ cause, context: { foo: 1 } }
|
||||
);
|
||||
expect(err.code).toBe(AUDIO_CODEC_FFMPEG_WASM_CODES.DECODE_FAILED);
|
||||
expect(err.cause).toBe(cause);
|
||||
expect(err.context).toEqual({ foo: 1 });
|
||||
expect(err.name).toBe("AudioCodecFfmpegWasmError");
|
||||
expect(err.category).toBe("audio-codec-ffmpeg-wasm");
|
||||
});
|
||||
|
||||
it("registerAudioCodecFfmpegWasmErrorCodes is idempotent", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(() => registerAudioCodecFfmpegWasmErrorCodes()).not.toThrow();
|
||||
expect(() => registerAudioCodecFfmpegWasmErrorCodes()).not.toThrow();
|
||||
});
|
||||
|
||||
it("constructor accepts default options", { timeout: 5000 }, () => {
|
||||
const err = new AudioCodecFfmpegWasmError(
|
||||
AUDIO_CODEC_FFMPEG_WASM_CODES.ABORTED,
|
||||
"msg"
|
||||
);
|
||||
expect(err.context).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
import {
|
||||
defineCodes,
|
||||
SuaveplanError,
|
||||
type SuaveplanErrorOptions,
|
||||
} from "@suaveplan/error";
|
||||
|
||||
/**
|
||||
* Stable `AUDIO_CODEC_FFMPEG_WASM.*` code registry. Every code below is thrown
|
||||
* from a real call site exercised by the test suite — see
|
||||
* `error-codes.test.ts` for the reachability assertion.
|
||||
*/
|
||||
export const AUDIO_CODEC_FFMPEG_WASM_CODES = defineCodes(
|
||||
"AUDIO_CODEC_FFMPEG_WASM",
|
||||
[
|
||||
"CORE_LOAD_FAILED",
|
||||
"WORKER_SPAWN_FAILED",
|
||||
"DECODE_FAILED",
|
||||
"ENCODE_FAILED",
|
||||
"TIMEOUT",
|
||||
"ABORTED",
|
||||
"UNSUPPORTED_CODEC",
|
||||
"MEMORY_LIMIT_EXCEEDED",
|
||||
"CROSS_ORIGIN_ISOLATION_REQUIRED",
|
||||
] as const
|
||||
);
|
||||
|
||||
export type AudioCodecFfmpegWasmErrorCode =
|
||||
(typeof AUDIO_CODEC_FFMPEG_WASM_CODES)[keyof typeof AUDIO_CODEC_FFMPEG_WASM_CODES];
|
||||
|
||||
export interface AudioCodecFfmpegWasmErrorMetadata {
|
||||
readonly description: string;
|
||||
readonly severity: "warning" | "error";
|
||||
}
|
||||
|
||||
export const AUDIO_CODEC_FFMPEG_WASM_ERROR_METADATA: Readonly<
|
||||
Record<AudioCodecFfmpegWasmErrorCode, AudioCodecFfmpegWasmErrorMetadata>
|
||||
> = Object.freeze({
|
||||
[AUDIO_CODEC_FFMPEG_WASM_CODES.CORE_LOAD_FAILED]: {
|
||||
description:
|
||||
"Lazy load of the @ffmpeg/core wasm payload failed — bad URL, CORS, or network error.",
|
||||
severity: "error",
|
||||
},
|
||||
[AUDIO_CODEC_FFMPEG_WASM_CODES.WORKER_SPAWN_FAILED]: {
|
||||
description:
|
||||
"The dedicated Worker could not be constructed or emitted `error` before `load()` resolved.",
|
||||
severity: "error",
|
||||
},
|
||||
[AUDIO_CODEC_FFMPEG_WASM_CODES.DECODE_FAILED]: {
|
||||
description:
|
||||
"ffmpeg returned a non-zero exit while decoding; stderr tail is attached on `context.stderr`.",
|
||||
severity: "error",
|
||||
},
|
||||
[AUDIO_CODEC_FFMPEG_WASM_CODES.ENCODE_FAILED]: {
|
||||
description:
|
||||
"ffmpeg returned a non-zero exit while encoding; stderr tail is attached on `context.stderr`.",
|
||||
severity: "error",
|
||||
},
|
||||
[AUDIO_CODEC_FFMPEG_WASM_CODES.TIMEOUT]: {
|
||||
description:
|
||||
"The worker did not respond within the configured timeout; the worker was terminated.",
|
||||
severity: "error",
|
||||
},
|
||||
[AUDIO_CODEC_FFMPEG_WASM_CODES.ABORTED]: {
|
||||
description:
|
||||
"An AbortSignal fired; the worker was terminated and the call rejected.",
|
||||
severity: "warning",
|
||||
},
|
||||
[AUDIO_CODEC_FFMPEG_WASM_CODES.UNSUPPORTED_CODEC]: {
|
||||
description:
|
||||
"The requested format is not in the supported set, or constructor options failed Zod validation.",
|
||||
severity: "error",
|
||||
},
|
||||
[AUDIO_CODEC_FFMPEG_WASM_CODES.MEMORY_LIMIT_EXCEEDED]: {
|
||||
description:
|
||||
"ffmpeg.wasm hit its 4 GB linear memory cap; the worker was terminated.",
|
||||
severity: "error",
|
||||
},
|
||||
[AUDIO_CODEC_FFMPEG_WASM_CODES.CROSS_ORIGIN_ISOLATION_REQUIRED]: {
|
||||
description:
|
||||
"`enableMultithread: true` was requested but `globalThis.crossOriginIsolated !== true` — COOP/COEP headers missing.",
|
||||
severity: "error",
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Concrete `SuaveplanError` subclass for every fault raised by
|
||||
* `@suaveplan/audio-codec-ffmpeg-wasm`. The `category` field exists so the
|
||||
* constructor body is non-trivial — no static-analysis suppression required.
|
||||
*/
|
||||
export class AudioCodecFfmpegWasmError extends SuaveplanError<AudioCodecFfmpegWasmErrorCode> {
|
||||
public readonly category = "audio-codec-ffmpeg-wasm" as const;
|
||||
|
||||
public constructor(
|
||||
code: AudioCodecFfmpegWasmErrorCode,
|
||||
messageKey: string,
|
||||
options: SuaveplanErrorOptions = {}
|
||||
) {
|
||||
super(code, messageKey, options);
|
||||
this.name = "AudioCodecFfmpegWasmError";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Idempotent registration hook. Metadata is already frozen at module load;
|
||||
* call this once at startup if you bind codes to OTel attributes elsewhere.
|
||||
*/
|
||||
export function registerAudioCodecFfmpegWasmErrorCodes(): void {
|
||||
// No-op — registry side-effects are static.
|
||||
}
|
||||
|
||||
registerAudioCodecFfmpegWasmErrorCodes();
|
||||
@@ -0,0 +1,10 @@
|
||||
export type {
|
||||
AudioCodecFfmpegWasmErrorCode,
|
||||
AudioCodecFfmpegWasmErrorMetadata,
|
||||
} from "./error-codes.js";
|
||||
export {
|
||||
AUDIO_CODEC_FFMPEG_WASM_CODES,
|
||||
AUDIO_CODEC_FFMPEG_WASM_ERROR_METADATA,
|
||||
AudioCodecFfmpegWasmError,
|
||||
registerAudioCodecFfmpegWasmErrorCodes,
|
||||
} from "./error-codes.js";
|
||||
Reference in New Issue
Block a user