Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions packages/tsunami/AGENTS.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion packages/tsunami/src/ClientResourceScope.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ClientDisposedError } from "./errors";
import { ClientDisposedError } from "./errors/lifecycleErrors";

type Cleanup = () => void;

Expand Down
2 changes: 1 addition & 1 deletion packages/tsunami/src/FishjamClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
10 changes: 0 additions & 10 deletions packages/tsunami/src/errors.ts

This file was deleted.

24 changes: 24 additions & 0 deletions packages/tsunami/src/errors/FishjamError.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
18 changes: 18 additions & 0 deletions packages/tsunami/src/errors/lifecycleErrors.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
17 changes: 17 additions & 0 deletions packages/tsunami/src/errors/lifecycleErrors.ts
Original file line number Diff line number Diff line change
@@ -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";
}
}
3 changes: 2 additions & 1 deletion packages/tsunami/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
2 changes: 1 addition & 1 deletion tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"skipLibCheck": true,
"strict": true,
"target": "ESNext",
"lib": ["es2020", "DOM", "DOM.Iterable"],
"lib": ["es2022", "DOM", "DOM.Iterable"],
"jsx": "react-jsx"
}
}