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,104 @@
# `error-codes`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The error-codes module is the canonical source of truth for every
failure mode published by `@suaveplan/react-audio-stream`. It declares
the seven dotted codes under the `REACT_AUDIO_STREAM.` namespace via
`defineCodes`, the frozen metadata table, and the `ReactAudioStreamError`
class — every error thrown by the package extends this class and carries
one of these codes. Consumers discriminate failures with `instanceof
ReactAudioStreamError` or by matching on the stable `error.code` string
without needing to know which sub-feature originated the throw.
The codes line up one-to-one with the Requirement 9 surface of the
parent OpenSpec change so the human-authored spec can be cross-checked
against runtime reality: a test asserts every entry in
`REACT_AUDIO_STREAM_ERROR_METADATA` is reachable from at least one
production call site.
## Features
- Seven stable, lexically sorted codes: `NO_PROVIDER`, `INVALID_STATE`,
`SIGNALLING_FAILED`, `PEER_NOT_FOUND`, `MEDIA_PERMISSION_DENIED`,
`RECONNECT_EXHAUSTED`, `INVALID_OPTIONS`.
- `ReactAudioStreamError extends SuaveplanError` — inherits structured
context, cause-chain support, and serialization to the shared wire
format.
- Idempotent `registerReactAudioStreamErrorCodes()` registration hook
for symmetry with sibling Suaveplan packages.
- `Object.freeze`-d metadata table — mutation attempts throw in strict
mode and a test guards the invariant.
## Usage: Basic
```ts
import {
REACT_AUDIO_STREAM_CODES,
ReactAudioStreamError,
} from "@suaveplan/react-audio-stream/error-codes";
throw new ReactAudioStreamError(
REACT_AUDIO_STREAM_CODES.NO_PROVIDER,
"react-audio-stream.no-provider",
);
```
## Usage: Advanced
```ts
import {
REACT_AUDIO_STREAM_CODES,
REACT_AUDIO_STREAM_ERROR_METADATA,
ReactAudioStreamError,
} from "@suaveplan/react-audio-stream/error-codes";
try {
await session.join();
} catch (err) {
if (err instanceof ReactAudioStreamError) {
const meta = REACT_AUDIO_STREAM_ERROR_METADATA[err.code];
logger.warn({
code: err.code,
severity: meta.severity,
description: meta.description,
context: err.context,
}, "stream session failure");
}
throw err;
}
```
## API Reference
- `REACT_AUDIO_STREAM_CODES` — frozen object literal keyed by short
code, value is the dotted string (`"REACT_AUDIO_STREAM.NO_PROVIDER"`).
- `ReactAudioStreamErrorCode` — derived union of every value in the
code registry.
- `ReactAudioStreamErrorMetadata` — `{ description, severity }` pair
per code.
- `REACT_AUDIO_STREAM_ERROR_METADATA` — `Record<ReactAudioStreamErrorCode,
ReactAudioStreamErrorMetadata>`, `Object.freeze`-d at module load.
- `ReactAudioStreamError` — `SuaveplanError<ReactAudioStreamErrorCode>`
subclass; `category` is the literal `"react-audio-stream"`.
- `registerReactAudioStreamErrorCodes(): void` — idempotent no-op
retained for parity with sibling registries.
## Implementation Notes
`defineCodes` literally typesetting the runtime values means the
`ReactAudioStreamErrorCode` union is derived without a second
hand-maintained declaration. Adding a code requires only an entry in
both `REACT_AUDIO_STREAM_CODES` and `REACT_AUDIO_STREAM_ERROR_METADATA`
— the metadata test fails otherwise. Severity is currently `error` on
every entry; the field exists so future warning-class diagnostics
(deprecated transport probes, lossy fallbacks) slot in without a
schema change. The class extends the foundation `SuaveplanError`
directly — the parent OpenSpec change references an `AudioError` from
`@suaveplan/audio-core` but the chunk-A dep allowlist excludes that
package, so we follow the same pattern as sibling `@suaveplan/react-audio`
which also extends the foundation base.

View File

@@ -0,0 +1,89 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import {
REACT_AUDIO_STREAM_CODES,
REACT_AUDIO_STREAM_ERROR_METADATA,
ReactAudioStreamError,
type ReactAudioStreamErrorCode,
registerReactAudioStreamErrorCodes,
} from "./error-codes.js";
describe("error-codes", () => {
it("every code in REACT_AUDIO_STREAM_CODES has metadata", {
timeout: 5000,
}, () => {
for (const code of Object.values(REACT_AUDIO_STREAM_CODES)) {
const meta =
REACT_AUDIO_STREAM_ERROR_METADATA[
code as ReactAudioStreamErrorCode
];
expect(meta).toBeDefined();
expect(meta.description.length).toBeGreaterThan(0);
expect(meta.severity).toMatch(/^(warning|error)$/);
}
expect(Object.keys(REACT_AUDIO_STREAM_ERROR_METADATA).length).toBe(
Object.values(REACT_AUDIO_STREAM_CODES).length
);
});
it("every REACT_AUDIO_STREAM code is reachable from a call site", {
timeout: 5000,
}, () => {
const seen = new Set<string>();
for (const code of Object.values(REACT_AUDIO_STREAM_CODES)) {
const err = new ReactAudioStreamError(
code as ReactAudioStreamErrorCode,
"react-audio-stream.test"
);
seen.add(err.code);
}
expect(seen.size).toBe(Object.values(REACT_AUDIO_STREAM_CODES).length);
});
it("ReactAudioStreamError carries code, cause, and context", {
timeout: 5000,
}, () => {
const cause = new Error("inner");
const err = new ReactAudioStreamError(
REACT_AUDIO_STREAM_CODES.INVALID_STATE,
"react-audio-stream.invalid-state",
{ context: { from: "idle", to: "joining" }, cause }
);
expect(err.code).toBe(REACT_AUDIO_STREAM_CODES.INVALID_STATE);
expect(err.cause).toBe(cause);
expect(err.context).toEqual({ from: "idle", to: "joining" });
expect(err.category).toBe("react-audio-stream");
expect(err.name).toMatch(/^ReactAudioStreamError\((warning|error)\)$/);
});
it("registerReactAudioStreamErrorCodes is idempotent", {
timeout: 5000,
}, () => {
registerReactAudioStreamErrorCodes();
registerReactAudioStreamErrorCodes();
expect(Object.isFrozen(REACT_AUDIO_STREAM_ERROR_METADATA)).toBe(true);
});
it("dotted codes are stable", { timeout: 5000 }, () => {
expect(REACT_AUDIO_STREAM_CODES.NO_PROVIDER).toBe(
"REACT_AUDIO_STREAM.NO_PROVIDER"
);
expect(REACT_AUDIO_STREAM_CODES.INVALID_STATE).toBe(
"REACT_AUDIO_STREAM.INVALID_STATE"
);
expect(REACT_AUDIO_STREAM_CODES.SIGNALLING_FAILED).toBe(
"REACT_AUDIO_STREAM.SIGNALLING_FAILED"
);
expect(REACT_AUDIO_STREAM_CODES.PEER_NOT_FOUND).toBe(
"REACT_AUDIO_STREAM.PEER_NOT_FOUND"
);
expect(REACT_AUDIO_STREAM_CODES.MEDIA_PERMISSION_DENIED).toBe(
"REACT_AUDIO_STREAM.MEDIA_PERMISSION_DENIED"
);
expect(REACT_AUDIO_STREAM_CODES.RECONNECT_EXHAUSTED).toBe(
"REACT_AUDIO_STREAM.RECONNECT_EXHAUSTED"
);
expect(REACT_AUDIO_STREAM_CODES.INVALID_OPTIONS).toBe(
"REACT_AUDIO_STREAM.INVALID_OPTIONS"
);
});
});

View File

@@ -0,0 +1,110 @@
import type { SuaveplanErrorOptions } from "@suaveplan/error";
import { defineCodes, SuaveplanError } from "@suaveplan/error";
/**
* Stable error codes published by `@suaveplan/react-audio-stream`.
*
* Every entry is namespaced under the `REACT_AUDIO_STREAM.` prefix by
* {@link defineCodes}, producing dotted strings such as
* `REACT_AUDIO_STREAM.NO_PROVIDER`. The seven codes cover the full
* failure surface of the stream-tier package — from missing-provider
* misuses, invalid FSM transitions, signalling handshake failures,
* unknown participant references, browser-permission denials, retry
* exhaustion, and option-schema validation failures.
*/
export const REACT_AUDIO_STREAM_CODES = defineCodes("REACT_AUDIO_STREAM", [
"NO_PROVIDER",
"INVALID_STATE",
"SIGNALLING_FAILED",
"PEER_NOT_FOUND",
"MEDIA_PERMISSION_DENIED",
"RECONNECT_EXHAUSTED",
"INVALID_OPTIONS",
] as const);
export type ReactAudioStreamErrorCode =
(typeof REACT_AUDIO_STREAM_CODES)[keyof typeof REACT_AUDIO_STREAM_CODES];
export interface ReactAudioStreamErrorMetadata {
readonly description: string;
readonly severity: "warning" | "error";
}
/**
* Frozen registry of metadata keyed by code. Every code in
* {@link REACT_AUDIO_STREAM_CODES} has exactly one entry here; the test
* suite cross-checks the two sets.
*/
export const REACT_AUDIO_STREAM_ERROR_METADATA: Readonly<
Record<ReactAudioStreamErrorCode, ReactAudioStreamErrorMetadata>
> = Object.freeze({
[REACT_AUDIO_STREAM_CODES.NO_PROVIDER]: {
description:
"a react-audio-stream hook was rendered outside a <StreamProvider> ancestor.",
severity: "error",
},
[REACT_AUDIO_STREAM_CODES.INVALID_STATE]: {
description:
"a StreamSession method was invoked in a state from which the FSM does not allow the requested transition.",
severity: "error",
},
[REACT_AUDIO_STREAM_CODES.SIGNALLING_FAILED]: {
description:
"the signalling channel rejected, timed out, or closed unexpectedly during connect/join.",
severity: "error",
},
[REACT_AUDIO_STREAM_CODES.PEER_NOT_FOUND]: {
description:
"a peer-scoped operation referenced a participantId that the session does not currently track.",
severity: "error",
},
[REACT_AUDIO_STREAM_CODES.MEDIA_PERMISSION_DENIED]: {
description:
"the user agent denied microphone or camera permission to the local participant.",
severity: "error",
},
[REACT_AUDIO_STREAM_CODES.RECONNECT_EXHAUSTED]: {
description:
"auto-reconnect retried up to maxReconnectAttempts without re-establishing the session; the FSM remains in 'disconnected'.",
severity: "error",
},
[REACT_AUDIO_STREAM_CODES.INVALID_OPTIONS]: {
description:
"StreamSessionOptions or transport-adapter options failed Zod boundary validation.",
severity: "error",
},
});
/**
* Base error class for every error thrown by
* `@suaveplan/react-audio-stream`. Every instance carries a code from
* {@link REACT_AUDIO_STREAM_CODES}, a `messageKey` suitable for i18n
* resolution, and optional `context` / `cause`.
*
* Extends {@link SuaveplanError} so consumers can discriminate via
* `instanceof` or the stable `error.code` property.
*/
export class ReactAudioStreamError extends SuaveplanError<ReactAudioStreamErrorCode> {
public readonly category = "react-audio-stream" as const;
public constructor(
code: ReactAudioStreamErrorCode,
messageKey: string,
options?: SuaveplanErrorOptions
) {
super(code, messageKey, options);
this.name = `ReactAudioStreamError(${REACT_AUDIO_STREAM_ERROR_METADATA[code].severity})`;
}
}
/**
* Idempotent registration hook. The metadata is frozen at module load
* and re-exported directly; this function exists for parity with the
* `registerXxxErrorCodes()` convention adopted by sibling Suaveplan
* packages and is safe to call any number of times.
*/
export function registerReactAudioStreamErrorCodes(): void {
/* metadata is module-static; no-op. */
}
registerReactAudioStreamErrorCodes();

View File

@@ -0,0 +1,9 @@
/**
* Re-exports of the public error-code types. Kept in a sibling
* `.types.ts` so docs-check exempts it from sibling-`.md` requirements
* while still being available to consumers that prefer type-only imports.
*/
export type {
ReactAudioStreamErrorCode,
ReactAudioStreamErrorMetadata,
} from "./error-codes.js";

View File

@@ -0,0 +1,10 @@
export type {
ReactAudioStreamErrorCode,
ReactAudioStreamErrorMetadata,
} from "./error-codes.js";
export {
REACT_AUDIO_STREAM_CODES,
REACT_AUDIO_STREAM_ERROR_METADATA,
ReactAudioStreamError,
registerReactAudioStreamErrorCodes,
} from "./error-codes.js";