diff --git a/packages/tsunami/AGENTS.md b/packages/tsunami/AGENTS.md new file mode 100644 index 00000000..a6983e99 --- /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 a65288aa..8d7624bb 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 af791373..f6edf2ad 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 c1ae586e..00000000 --- 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 00000000..7fb693a8 --- /dev/null +++ b/packages/tsunami/src/errors/FishjamError.ts @@ -0,0 +1,24 @@ +/** Possible values of {@link FishjamError.recoverability}. */ +export type ErrorRecoverability = "retry" | "user_action" | "fatal"; + +/** + * Base class of every error thrown by the SDK. + * + * + * 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 00000000..58ad1c39 --- /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 00000000..0842fffd --- /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 1b258723..d5222e1d 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 0fb4e06b..8f474196 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" } }