From bd318ecda12ab1a55567b3b4a1920852f470ac06 Mon Sep 17 00:00:00 2001 From: Milosz Filimowski Date: Thu, 30 Jul 2026 17:01:21 +0200 Subject: [PATCH 1/2] FCE-3580: define tsunami error model and add FishjamError base Document the error-model decision in packages/tsunami/AGENTS.md: every failing imperative method rejects with a typed FishjamError subclass and the same object is mirrored into the matching ClientState field, applied to device acquisition, addTrack (TrackTypeError becomes a rejection), and connect() (typed rejections consistent with peerStatus). Land the base primitive: FishjamError (cause + recoverability) with ClientDisposedError refactored onto it under src/errors/. Concrete error classes land with their first consumer in the follow-up tickets. Bump the base tsconfig lib to es2022 for Error.cause typing (tsunami is the only package extending it). --- packages/tsunami/AGENTS.md | 29 ++++++++++++++ packages/tsunami/src/ClientResourceScope.ts | 2 +- packages/tsunami/src/FishjamClient.test.ts | 2 +- packages/tsunami/src/errors.ts | 10 ----- packages/tsunami/src/errors/FishjamError.ts | 38 +++++++++++++++++++ .../src/errors/lifecycleErrors.test.ts | 18 +++++++++ .../tsunami/src/errors/lifecycleErrors.ts | 17 +++++++++ packages/tsunami/src/index.ts | 3 +- tsconfig.base.json | 2 +- 9 files changed, 107 insertions(+), 14 deletions(-) create mode 100644 packages/tsunami/AGENTS.md delete mode 100644 packages/tsunami/src/errors.ts create mode 100644 packages/tsunami/src/errors/FishjamError.ts create mode 100644 packages/tsunami/src/errors/lifecycleErrors.test.ts create mode 100644 packages/tsunami/src/errors/lifecycleErrors.ts diff --git a/packages/tsunami/AGENTS.md b/packages/tsunami/AGENTS.md new file mode 100644 index 000000000..a6983e992 --- /dev/null +++ b/packages/tsunami/AGENTS.md @@ -0,0 +1,29 @@ +# tsunami — implementation guide + +Error model, decided in FCE-3580. Follow it exactly; do not invent new error +mechanisms. + +## The rule + +**Both surfaces, one error object.** A failing imperative method rejects with +a typed `FishjamError` subclass, and the same object is written to the +matching `ClientState` field. Success on the same resource clears the field. + +| Path | Contract | +| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Device acquisition | Rejects with a `DeviceError` subclass; same object in `cameraError` / `microphoneError`. Exception: `initializeDevices` _resolves_ with a result (degraded init is a supported outcome). | +| `addTrack`, room refuses track kind | Rejects with `TrackTypeError` — never a synchronous throw. Keep-local recovery is the adapter's policy. | +| `connect()` | Rejects with `AuthError` / `JoinError` / `SocketError` / `ConnectionError` (never `undefined`, never hangs) and sets `peerStatus: "error"` — one shared event list for both surfaces. | + +## Rules + +1. Per-failure classes in domain files (`devices/errors.ts`, …): extend + `FishjamError`, explicit `this.name` literal (minification-safe), set + `recoverability`, original failure as `cause`. No `{ name }` objects, + codes, or reason unions. +2. Land error classes with the code that first throws them (YAGNI). +3. Misuse errors (`ClientDisposedError`) are the only sync throws; never in state. +4. Platform error shapes stay behind `IDeviceManager` — DOMException + classification belongs in `WebDeviceManager`, never in core. +5. Every error path ships both tests: typed rejection + state transition. +6. `AbortError` is cancellation, not failure — never mapped or stored. diff --git a/packages/tsunami/src/ClientResourceScope.ts b/packages/tsunami/src/ClientResourceScope.ts index a65288aa7..8d7624bbe 100644 --- a/packages/tsunami/src/ClientResourceScope.ts +++ b/packages/tsunami/src/ClientResourceScope.ts @@ -1,4 +1,4 @@ -import { ClientDisposedError } from "./errors"; +import { ClientDisposedError } from "./errors/lifecycleErrors"; type Cleanup = () => void; diff --git a/packages/tsunami/src/FishjamClient.test.ts b/packages/tsunami/src/FishjamClient.test.ts index af7913739..f6edf2ad6 100644 --- a/packages/tsunami/src/FishjamClient.test.ts +++ b/packages/tsunami/src/FishjamClient.test.ts @@ -2,7 +2,7 @@ import { type ConnectConfig, FishjamClient as TsClient, type GenericMetadata } f import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { ClientResourceScope } from "./ClientResourceScope"; -import { ClientDisposedError } from "./errors"; +import { ClientDisposedError } from "./errors/lifecycleErrors"; import { FishjamClient } from "./FishjamClient"; const mediaStreamConstructor = vi.fn(); diff --git a/packages/tsunami/src/errors.ts b/packages/tsunami/src/errors.ts deleted file mode 100644 index c1ae586e7..000000000 --- a/packages/tsunami/src/errors.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Thrown when an operation is attempted on a client that has reached the end - * of its lifecycle. - */ -export class ClientDisposedError extends Error { - public constructor() { - super("FishjamClient has been disposed and cannot be used again"); - this.name = "ClientDisposedError"; - } -} diff --git a/packages/tsunami/src/errors/FishjamError.ts b/packages/tsunami/src/errors/FishjamError.ts new file mode 100644 index 000000000..b1cd37a70 --- /dev/null +++ b/packages/tsunami/src/errors/FishjamError.ts @@ -0,0 +1,38 @@ +/** Possible values of {@link FishjamError.recoverability}. */ +export type ErrorRecoverability = "retry" | "user_action" | "fatal"; + +/** + * Base class of every error thrown by the SDK. + * + * Use it to tell SDK failures apart from other errors, then narrow to a + * specific error class when a case needs dedicated handling: + * + * ```typescript + * try { + * await client.connect(config); + * } catch (error) { + * if (error instanceof FishjamError) { + * showErrorBanner(error.message); + * } else { + * throw error; + * } + * } + * ``` + * + * When the SDK wraps a platform failure (for example a `DOMException` from the + * browser), the original error is available on `cause`. + */ +export abstract class FishjamError extends Error { + /** + * Hints how your application can respond to this error: + * - `"retry"` — the same operation may succeed if attempted again. + * - `"user_action"` — ask the user to fix something first, for example grant + * a permission, free up the device, or sign in again. + * - `"fatal"` — the operation cannot succeed; do not retry. + */ + public abstract readonly recoverability: ErrorRecoverability; + + protected constructor(message: string, options?: { cause?: unknown }) { + super(message, options); + } +} diff --git a/packages/tsunami/src/errors/lifecycleErrors.test.ts b/packages/tsunami/src/errors/lifecycleErrors.test.ts new file mode 100644 index 000000000..58ad1c398 --- /dev/null +++ b/packages/tsunami/src/errors/lifecycleErrors.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from "vitest"; + +import { FishjamError } from "./FishjamError"; +import { ClientDisposedError } from "./lifecycleErrors"; + +describe("ClientDisposedError", () => { + it("keeps the message the lifecycle machinery and its tests rely on", () => { + expect(new ClientDisposedError().message).toBe("FishjamClient has been disposed and cannot be used again"); + }); + + it("is a fatal FishjamError with a stable name", () => { + const error = new ClientDisposedError(); + + expect(error).toBeInstanceOf(FishjamError); + expect(error.name).toBe("ClientDisposedError"); + expect(error.recoverability).toBe("fatal"); + }); +}); diff --git a/packages/tsunami/src/errors/lifecycleErrors.ts b/packages/tsunami/src/errors/lifecycleErrors.ts new file mode 100644 index 000000000..0842fffda --- /dev/null +++ b/packages/tsunami/src/errors/lifecycleErrors.ts @@ -0,0 +1,17 @@ +import { type ErrorRecoverability, FishjamError } from "./FishjamError"; + +/** + * Thrown when a method is called on a client after `dispose()`. + * + * A disposed client is permanently unusable — create a new `FishjamClient` + * instance instead of retrying. You may also see this error as the rejection + * of an operation that was still in flight when the client was disposed. + */ +export class ClientDisposedError extends FishjamError { + public readonly recoverability: ErrorRecoverability = "fatal"; + + public constructor() { + super("FishjamClient has been disposed and cannot be used again"); + this.name = "ClientDisposedError"; + } +} diff --git a/packages/tsunami/src/index.ts b/packages/tsunami/src/index.ts index 1b258723a..d5222e1db 100644 --- a/packages/tsunami/src/index.ts +++ b/packages/tsunami/src/index.ts @@ -6,7 +6,8 @@ export type { DeviceItem, DeviceType, IDeviceManager, IDevicePersistence } from "./devices/deviceManager"; export { LocalStorageDevicePersistence } from "./devices/LocalStorageDevicePersistence"; export { WebDeviceManager, type WebDeviceManagerOptions } from "./devices/WebDeviceManager"; -export { ClientDisposedError } from "./errors"; +export { type ErrorRecoverability, FishjamError } from "./errors/FishjamError"; +export { ClientDisposedError } from "./errors/lifecycleErrors"; export { FishjamClient } from "./FishjamClient"; export * from "@fishjam-cloud/ts-client"; diff --git a/tsconfig.base.json b/tsconfig.base.json index 0fb4e06b8..8f4741967 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -11,7 +11,7 @@ "skipLibCheck": true, "strict": true, "target": "ESNext", - "lib": ["es2020", "DOM", "DOM.Iterable"], + "lib": ["es2022", "DOM", "DOM.Iterable"], "jsx": "react-jsx" } } From add1f83fe7649175711d3fb91e15c5201ac4a66e Mon Sep 17 00:00:00 2001 From: Milosz Filimowski Date: Thu, 30 Jul 2026 17:08:00 +0200 Subject: [PATCH 2/2] remove excessive comment --- packages/tsunami/src/errors/FishjamError.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/packages/tsunami/src/errors/FishjamError.ts b/packages/tsunami/src/errors/FishjamError.ts index b1cd37a70..7fb693a81 100644 --- a/packages/tsunami/src/errors/FishjamError.ts +++ b/packages/tsunami/src/errors/FishjamError.ts @@ -4,20 +4,6 @@ export type ErrorRecoverability = "retry" | "user_action" | "fatal"; /** * Base class of every error thrown by the SDK. * - * Use it to tell SDK failures apart from other errors, then narrow to a - * specific error class when a case needs dedicated handling: - * - * ```typescript - * try { - * await client.connect(config); - * } catch (error) { - * if (error instanceof FishjamError) { - * showErrorBanner(error.message); - * } else { - * throw error; - * } - * } - * ``` * * When the SDK wraps a platform failure (for example a `DOMException` from the * browser), the original error is available on `cause`.