@tribe-nest/media-client 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +68 -0
  2. package/build/core/index.d.ts +17 -0
  3. package/build/core/index.d.ts.map +1 -0
  4. package/build/core/index.js +41 -0
  5. package/build/core/index.js.map +1 -0
  6. package/build/core/reconnect.d.ts +95 -0
  7. package/build/core/reconnect.d.ts.map +1 -0
  8. package/build/core/reconnect.js +160 -0
  9. package/build/core/reconnect.js.map +1 -0
  10. package/build/core/signal.d.ts +184 -0
  11. package/build/core/signal.d.ts.map +1 -0
  12. package/build/core/signal.js +416 -0
  13. package/build/core/signal.js.map +1 -0
  14. package/build/core/socket.d.ts +57 -0
  15. package/build/core/socket.d.ts.map +1 -0
  16. package/build/core/socket.js +37 -0
  17. package/build/core/socket.js.map +1 -0
  18. package/build/core/state.d.ts +67 -0
  19. package/build/core/state.d.ts.map +1 -0
  20. package/build/core/state.js +193 -0
  21. package/build/core/state.js.map +1 -0
  22. package/build/index.d.ts +29 -0
  23. package/build/index.d.ts.map +1 -0
  24. package/build/index.js +51 -0
  25. package/build/index.js.map +1 -0
  26. package/build/protocol.d.ts +10 -0
  27. package/build/protocol.d.ts.map +1 -0
  28. package/build/protocol.js +26 -0
  29. package/build/protocol.js.map +1 -0
  30. package/build/react/index.d.ts +147 -0
  31. package/build/react/index.d.ts.map +1 -0
  32. package/build/react/index.js +319 -0
  33. package/build/react/index.js.map +1 -0
  34. package/build/room/browserDevice.d.ts +3 -0
  35. package/build/room/browserDevice.d.ts.map +1 -0
  36. package/build/room/browserDevice.js +94 -0
  37. package/build/room/browserDevice.js.map +1 -0
  38. package/build/room/device.d.ts +114 -0
  39. package/build/room/device.d.ts.map +1 -0
  40. package/build/room/device.js +3 -0
  41. package/build/room/device.js.map +1 -0
  42. package/build/room/room.d.ts +219 -0
  43. package/build/room/room.d.ts.map +1 -0
  44. package/build/room/room.js +438 -0
  45. package/build/room/room.js.map +1 -0
  46. package/package.json +69 -0
  47. package/src/_tests/clientBoundary.spec.ts +110 -0
  48. package/src/core/_tests/coreBoundary.spec.ts +70 -0
  49. package/src/core/_tests/fakeSignalServer.ts +188 -0
  50. package/src/core/_tests/reconnect.spec.ts +180 -0
  51. package/src/core/_tests/signal.spec.ts +347 -0
  52. package/src/core/_tests/state.spec.ts +226 -0
  53. package/src/core/index.ts +63 -0
  54. package/src/core/reconnect.ts +233 -0
  55. package/src/core/signal.ts +527 -0
  56. package/src/core/socket.ts +58 -0
  57. package/src/core/state.ts +251 -0
  58. package/src/index.ts +54 -0
  59. package/src/protocol.ts +9 -0
  60. package/src/react/_tests/hooks.spec.tsx +509 -0
  61. package/src/react/index.tsx +439 -0
  62. package/src/room/_tests/room.spec.ts +595 -0
  63. package/src/room/browserDevice.ts +114 -0
  64. package/src/room/device.ts +119 -0
  65. package/src/room/room.ts +600 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"room.js","sourceRoot":"","sources":["../../src/room/room.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@tribe-nest/media-client",
3
+ "version": "0.1.0",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "description": "Client SDK for the media network. `./core` is the HEADLESS half: a WebSocket client speaking media-protocol frames, backoff-with-jitter reconnection and a pure room-state reducer. Zero DOM, zero mediasoup-client, zero React, because the load harness, the egress client and the SIP gateway all need a protocol client and none of them is a browser.",
8
+ "sideEffects": false,
9
+ "exports": {
10
+ ".": {
11
+ "types": "./build/index.d.ts",
12
+ "module": "./src/index.ts",
13
+ "default": "./build/index.js"
14
+ },
15
+ "./core": {
16
+ "types": "./build/core/index.d.ts",
17
+ "module": "./src/core/index.ts",
18
+ "default": "./build/core/index.js"
19
+ },
20
+ "./react": {
21
+ "types": "./build/react/index.d.ts",
22
+ "module": "./src/react/index.tsx",
23
+ "default": "./build/react/index.js"
24
+ },
25
+ "./protocol": {
26
+ "types": "./build/protocol.d.ts",
27
+ "module": "./src/protocol.ts",
28
+ "default": "./build/protocol.js"
29
+ }
30
+ },
31
+ "files": [
32
+ "build",
33
+ "src"
34
+ ],
35
+ "scripts": {
36
+ "build": "tsc",
37
+ "dev": "tsc --watch --preserveWatchOutput",
38
+ "check-types": "tsc --noEmit && tsc --noEmit -p tsconfig.core.json",
39
+ "check-types:specs": "tsc --noEmit -p tsconfig.spec.json",
40
+ "test": "vitest run",
41
+ "test:watch": "vitest",
42
+ "prepack": "npm run build"
43
+ },
44
+ "dependencies": {
45
+ "@tribe-nest/media-protocol": "^1.0.0",
46
+ "mediasoup-client": "^3.22.0",
47
+ "zod": "^3.25.76"
48
+ },
49
+ "devDependencies": {
50
+ "@repo/typescript-config": "*",
51
+ "@testing-library/react": "^16.1.0",
52
+ "@types/node": "^22.13.1",
53
+ "@types/react": "^19.0.0",
54
+ "jsdom": "^25.0.1",
55
+ "react": "^19.1.0",
56
+ "react-dom": "^19.1.0",
57
+ "typescript": "5.8.2",
58
+ "vitest": "^3.2.4"
59
+ },
60
+ "license": "ISC",
61
+ "peerDependencies": {
62
+ "react": ">=18"
63
+ },
64
+ "peerDependenciesMeta": {
65
+ "react": {
66
+ "optional": true
67
+ }
68
+ }
69
+ }
@@ -0,0 +1,110 @@
1
+ import { readFileSync, readdirSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { describe, expect, it } from "vitest";
5
+
6
+ /**
7
+ * What the SDK is not allowed to know, and what it is not allowed to drag in.
8
+ *
9
+ * Two separate guarantees, both easy to break by accident and both invisible
10
+ * once broken:
11
+ *
12
+ * **`./core` stays isomorphic.** The load harness, the egress client and the
13
+ * SIP gateway all import it and none is a browser. `tsconfig.core.json`
14
+ * catches the type half; `core/_tests/coreBoundary.spec.ts` catches stray
15
+ * `node:` imports. This file adds the one they cannot: a `mediasoup-client`
16
+ * import reachable from the core barrel.
17
+ *
18
+ * **No part of the client learns TOPOLOGY.** A client that knows which node it
19
+ * is on can pick one, and picking a node is placement - the LiveKit limitation
20
+ * this whole architecture exists to escape. So `nodeId`, `peerNodeId` and
21
+ * announced addresses must not appear on any client-facing surface, and the
22
+ * join response deliberately omits them.
23
+ */
24
+
25
+ const SRC = dirname(fileURLToPath(import.meta.url)).replace(/\/_tests$/, "");
26
+
27
+ const stripComments = (source: string): string =>
28
+ source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:"'])\/\/.*$/gm, "$1");
29
+
30
+ function sourceFiles(dir: string): string[] {
31
+ return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
32
+ const full = join(dir, entry.name);
33
+ if (entry.isDirectory()) return entry.name === "_tests" ? [] : sourceFiles(full);
34
+ return /\.tsx?$/.test(entry.name) ? [full] : [];
35
+ });
36
+ }
37
+
38
+ const relative = (file: string) => file.slice(SRC.length + 1);
39
+
40
+ describe("mediasoup-client is confined to one file", () => {
41
+ const files = sourceFiles(SRC);
42
+
43
+ it("has sources to check", () => {
44
+ expect(files.length).toBeGreaterThan(6);
45
+ });
46
+
47
+ it("is imported ONLY by the browser device adapter", () => {
48
+ // Everything else is written against `MediaDevice`, which is what lets the
49
+ // room's ordering rules - device before transport, transport before
50
+ // consume, attach before resume - be tested without a browser at all.
51
+ const importers = files
52
+ .filter((file) => /from\s+"mediasoup-client(\/[^"]*)?"/.test(stripComments(readFileSync(file, "utf8"))))
53
+ .map(relative)
54
+ .sort();
55
+
56
+ expect(importers).toEqual(["room/browserDevice.ts", "room/device.ts"]);
57
+ });
58
+
59
+ it("keeps `device.ts` to TYPES only, so importing the seam pulls in nothing", () => {
60
+ const source = stripComments(readFileSync(join(SRC, "room/device.ts"), "utf8"));
61
+ // A value import here would put the library in every bundle that wanted the
62
+ // interface, including the harness's.
63
+ expect(source).toMatch(/import type \{[^}]*\} from "mediasoup-client\/types"/);
64
+ expect(source).not.toMatch(/^import \{/m);
65
+ });
66
+
67
+ it("is never reachable from the core barrel", () => {
68
+ const coreFiles = sourceFiles(join(SRC, "core"));
69
+ for (const file of coreFiles) {
70
+ const source = stripComments(readFileSync(file, "utf8"));
71
+ expect(source, relative(file)).not.toMatch(/mediasoup-client/);
72
+ expect(source, relative(file)).not.toMatch(/\bdocument\.|window\.|navigator\./);
73
+ }
74
+ });
75
+ });
76
+
77
+ describe("the client cannot learn topology", () => {
78
+ const files = sourceFiles(SRC);
79
+
80
+ it("names no node anywhere in the SDK", () => {
81
+ // Not a style rule. A client that can attribute itself to a node can prefer
82
+ // one, and preference is placement.
83
+ for (const file of files) {
84
+ const source = stripComments(readFileSync(file, "utf8"));
85
+ expect(source, relative(file)).not.toMatch(/\bnodeId\b/);
86
+ expect(source, relative(file)).not.toMatch(/\bpeerNodeId\b/);
87
+ expect(source, relative(file)).not.toMatch(/announcedAddress|announcedIp/);
88
+ }
89
+ });
90
+
91
+ it("speaks no pipe or mesh verb", () => {
92
+ for (const file of files) {
93
+ const source = stripComments(readFileSync(file, "utf8"));
94
+ expect(source, relative(file)).not.toMatch(/method:\s*"(pipe|mesh)/i);
95
+ }
96
+ });
97
+ });
98
+
99
+ describe("react is optional", () => {
100
+ it("is absent from every subpath except ./react", () => {
101
+ // `./core` and `.` are imported by non-React consumers - the harness, the
102
+ // egress client, a plain-DOM host page. A stray React import in either
103
+ // makes it a required dependency of all of them.
104
+ const nonReact = sourceFiles(SRC).filter((f) => !relative(f).startsWith("react/"));
105
+ for (const file of nonReact) {
106
+ const source = stripComments(readFileSync(file, "utf8"));
107
+ expect(source, relative(file)).not.toMatch(/from\s+"react"/);
108
+ }
109
+ });
110
+ });
@@ -0,0 +1,70 @@
1
+ import { readFileSync, readdirSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { describe, expect, it } from "vitest";
5
+
6
+ /**
7
+ * `./core` is isomorphic, and this is what keeps it that way.
8
+ *
9
+ * The load harness, the egress client and the SIP gateway all need a protocol
10
+ * client and none of them is a browser. One `window.` reference or one
11
+ * `mediasoup-client` import at module scope makes this subpath unusable for all
12
+ * three, and the failure appears in someone else's phase, weeks later, as
13
+ * "cannot read properties of undefined".
14
+ *
15
+ * `tsconfig.json` drops the DOM lib, which catches the type-level half. This
16
+ * catches the rest, including a stray `node:` import that would break a
17
+ * browser.
18
+ */
19
+
20
+ const CORE_DIR = dirname(fileURLToPath(import.meta.url)).replace(/\/_tests$/, "");
21
+
22
+ function stripComments(source: string): string {
23
+ return source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:"'])\/\/.*$/gm, "$1");
24
+ }
25
+
26
+ function sourceFiles(dir: string): string[] {
27
+ return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
28
+ if (entry.isDirectory()) return entry.name === "_tests" ? [] : sourceFiles(join(dir, entry.name));
29
+ return entry.name.endsWith(".ts") ? [join(dir, entry.name)] : [];
30
+ });
31
+ }
32
+
33
+ describe("core stays isomorphic", () => {
34
+ const files = sourceFiles(CORE_DIR);
35
+
36
+ it("has sources to check", () => {
37
+ expect(files.length).toBeGreaterThan(3);
38
+ });
39
+
40
+ for (const file of files) {
41
+ // Comments are stripped first. These files explain WHY they avoid the DOM,
42
+ // so scanning prose for `window.` finds the explanation rather than a
43
+ // violation.
44
+ const source = stripComments(readFileSync(file, "utf8"));
45
+ const name = file.slice(CORE_DIR.length + 1);
46
+
47
+ it(`${name} touches no browser global`, () => {
48
+ expect(source).not.toMatch(/\bwindow\s*\./);
49
+ expect(source).not.toMatch(/\bdocument\s*\./);
50
+ expect(source).not.toMatch(/\bnavigator\s*\./);
51
+ expect(source).not.toMatch(/\blocalStorage\b/);
52
+ });
53
+
54
+ it(`${name} imports no browser-only, node-only or React dependency`, () => {
55
+ const imports = [...source.matchAll(/from\s+"([^"]+)"/g)].map((m) => m[1] as string);
56
+ for (const specifier of imports) {
57
+ expect(specifier, `${name} imports ${specifier}`).not.toMatch(/^node:/);
58
+ expect(specifier, `${name} imports ${specifier}`).not.toMatch(/mediasoup/);
59
+ expect(specifier, `${name} imports ${specifier}`).not.toMatch(/^react(-dom)?$/);
60
+ }
61
+ });
62
+
63
+ it(`${name} constructs no socket of its own`, () => {
64
+ // Everything goes through the injected factory, except the one documented
65
+ // lazy lookup of a global in socket.ts.
66
+ if (name === "socket.ts") return;
67
+ expect(source).not.toMatch(/new\s+WebSocket\b/);
68
+ });
69
+ }
70
+ });
@@ -0,0 +1,188 @@
1
+ import {
2
+ requestFrameSchema,
3
+ type EventFrame,
4
+ type MediaErrorCode,
5
+ type RequestFrame,
6
+ type ServerFrame,
7
+ } from "@tribe-nest/media-protocol";
8
+
9
+ import { SOCKET_CLOSED, SOCKET_CONNECTING, SOCKET_OPEN, type SocketEventMap, type WebSocketLike } from "../socket";
10
+
11
+ /**
12
+ * An in-process node, for testing the client against the real frames without a
13
+ * network, a mediasoup worker or a running media server.
14
+ *
15
+ * It parses every frame the client sends with the CONTRACT's own
16
+ * `requestFrameSchema`, so a client that invents a field or drops a required
17
+ * one fails here rather than at integration against P1's node. That is the
18
+ * point of the fake: not "does my client talk to my fake", but "does my client
19
+ * emit frames the frozen schema accepts".
20
+ */
21
+
22
+ type Listener = (event: never) => void;
23
+
24
+ export class FakeSocket implements WebSocketLike {
25
+ readyState = SOCKET_CONNECTING;
26
+ readonly sent: string[] = [];
27
+ private readonly listeners = new Map<keyof SocketEventMap, Set<Listener>>();
28
+
29
+ constructor(
30
+ readonly url: string,
31
+ private readonly server: FakeSignalServer,
32
+ ) {}
33
+
34
+ addEventListener<K extends keyof SocketEventMap>(type: K, listener: (event: SocketEventMap[K]) => void): void {
35
+ const set = this.listeners.get(type) ?? new Set<Listener>();
36
+ set.add(listener as Listener);
37
+ this.listeners.set(type, set);
38
+ }
39
+
40
+ removeEventListener<K extends keyof SocketEventMap>(type: K, listener: (event: SocketEventMap[K]) => void): void {
41
+ this.listeners.get(type)?.delete(listener as Listener);
42
+ }
43
+
44
+ send(data: string): void {
45
+ this.sent.push(data);
46
+ this.server.receive(this, data);
47
+ }
48
+
49
+ close(code?: number, reason?: string): void {
50
+ if (this.readyState === SOCKET_CLOSED) return;
51
+ this.readyState = SOCKET_CLOSED;
52
+ this.emit("close", { type: "close", ...(code === undefined ? {} : { code }), ...(reason === undefined ? {} : { reason }) });
53
+ }
54
+
55
+ // ------------------------------------------------------------ node-side API
56
+
57
+ open(): void {
58
+ this.readyState = SOCKET_OPEN;
59
+ this.emit("open", { type: "open" });
60
+ }
61
+
62
+ deliver(frame: ServerFrame | string): void {
63
+ if (this.readyState !== SOCKET_OPEN) return;
64
+ this.emit("message", { type: "message", data: typeof frame === "string" ? frame : JSON.stringify(frame) });
65
+ }
66
+
67
+ dropFromServer(code = 1006, reason = "node closed"): void {
68
+ if (this.readyState === SOCKET_CLOSED) return;
69
+ this.readyState = SOCKET_CLOSED;
70
+ this.emit("close", { type: "close", code, reason });
71
+ }
72
+
73
+ private emit<K extends keyof SocketEventMap>(type: K, event: SocketEventMap[K]): void {
74
+ const set = this.listeners.get(type);
75
+ if (!set) return;
76
+ // Queued rather than called inline, so the client sees the same "nothing
77
+ // happens synchronously inside send()" ordering a real socket gives it.
78
+ for (const listener of [...set]) queueMicrotask(() => (listener as (e: SocketEventMap[K]) => void)(event));
79
+ }
80
+ }
81
+
82
+ export type FakeServerOptions = {
83
+ /** Reply ok to `join` and push a `joined` event. Off when a spec drives it by hand. */
84
+ autoJoin?: boolean;
85
+ /** Which of the two join frames arrives first. The contract fixes no order. */
86
+ joinOrder?: "reply-first" | "event-first";
87
+ joined?: Partial<Extract<EventFrame, { event: "joined" }>>;
88
+ /** Called for every request AFTER it is recorded. */
89
+ onRequest?: (frame: RequestFrame, socket: FakeSocket, server: FakeSignalServer) => void;
90
+ };
91
+
92
+ export class FakeSignalServer {
93
+ readonly sockets: FakeSocket[] = [];
94
+ readonly urls: string[] = [];
95
+ readonly received: RequestFrame[] = [];
96
+ /** Frames the CONTRACT schema refused. A conforming client leaves this empty. */
97
+ readonly invalidFrames: unknown[] = [];
98
+
99
+ constructor(private readonly options: FakeServerOptions = {}) {}
100
+
101
+ /** Pass this as `webSocket` to `MediaSignal`. */
102
+ readonly factory = (url: string): WebSocketLike => {
103
+ this.urls.push(url);
104
+ const socket = new FakeSocket(url, this);
105
+ this.sockets.push(socket);
106
+ queueMicrotask(() => socket.open());
107
+ return socket;
108
+ };
109
+
110
+ get socket(): FakeSocket {
111
+ const socket = this.sockets[this.sockets.length - 1];
112
+ if (!socket) throw new Error("no socket has been opened");
113
+ return socket;
114
+ }
115
+
116
+ receive(socket: FakeSocket, data: string): void {
117
+ let raw: unknown;
118
+ try {
119
+ raw = JSON.parse(data);
120
+ } catch {
121
+ this.invalidFrames.push(data);
122
+ return;
123
+ }
124
+ const parsed = requestFrameSchema.safeParse(raw);
125
+ if (!parsed.success) {
126
+ this.invalidFrames.push(raw);
127
+ return;
128
+ }
129
+ this.received.push(parsed.data);
130
+ if (parsed.data.method === "join" && (this.options.autoJoin ?? true)) {
131
+ this.completeJoin(socket, parsed.data.id);
132
+ }
133
+ this.options.onRequest?.(parsed.data, socket, this);
134
+ }
135
+
136
+ completeJoin(socket: FakeSocket, id: number): void {
137
+ const reply = () => this.reply(id, { accepted: true }, socket);
138
+ const event = () => this.event(this.joinedFrame(), socket);
139
+ if ((this.options.joinOrder ?? "reply-first") === "reply-first") {
140
+ reply();
141
+ event();
142
+ } else {
143
+ event();
144
+ reply();
145
+ }
146
+ }
147
+
148
+ joinedFrame(overrides: Partial<Extract<EventFrame, { event: "joined" }>> = {}): EventFrame {
149
+ return {
150
+ event: "joined",
151
+ identity: "u-1",
152
+ room: "room-1",
153
+ routerRtpCapabilities: { codecs: [] },
154
+ peers: [],
155
+ producers: [],
156
+ iceServers: [{ urls: "turn:turn.example:3478", username: "u", credential: "c" }],
157
+ recording: false,
158
+ ...this.options.joined,
159
+ ...overrides,
160
+ };
161
+ }
162
+
163
+ reply(id: number, data: unknown, socket: FakeSocket = this.socket): void {
164
+ socket.deliver({ id, ok: true, data });
165
+ }
166
+
167
+ fail(id: number, code: MediaErrorCode, message?: string, socket: FakeSocket = this.socket): void {
168
+ socket.deliver({ id, ok: false, code, ...(message === undefined ? {} : { message }) });
169
+ }
170
+
171
+ event(frame: EventFrame, socket: FakeSocket = this.socket): void {
172
+ socket.deliver(frame);
173
+ }
174
+
175
+ /** Anything at all, including frames the protocol does not describe. */
176
+ raw(payload: unknown, socket: FakeSocket = this.socket): void {
177
+ socket.deliver(typeof payload === "string" ? payload : JSON.stringify(payload));
178
+ }
179
+
180
+ lastRequest(method: RequestFrame["method"]): RequestFrame | undefined {
181
+ return [...this.received].reverse().find((f) => f.method === method);
182
+ }
183
+ }
184
+
185
+ /** Lets a spec await the microtask queue the fake socket delivers on. */
186
+ export const flush = async (times = 4): Promise<void> => {
187
+ for (let i = 0; i < times; i += 1) await Promise.resolve();
188
+ };
@@ -0,0 +1,180 @@
1
+ import { MEDIA_ERROR_CODES, MediaError, type MediaErrorCode } from "@tribe-nest/media-protocol";
2
+ import { describe, expect, it } from "vitest";
3
+
4
+ import {
5
+ DEFAULT_RECONNECT_OPTIONS,
6
+ RECONNECT_CODE_CLASSES,
7
+ backoffDelay,
8
+ classifyCode,
9
+ decideReconnect,
10
+ superviseConnection,
11
+ type ReconnectDecision,
12
+ } from "../reconnect";
13
+ import type { DisconnectCause } from "../signal";
14
+
15
+ /** A deterministic `Math.random`, so delays are asserted exactly. */
16
+ const fixedRandom = (value: number) => () => value;
17
+
18
+ describe("backoff", () => {
19
+ it("grows exponentially and stops at the ceiling", () => {
20
+ const o = { jitter: "none" as const, baseMs: 500, factor: 2, maxMs: 8_000 };
21
+ expect([0, 1, 2, 3, 4, 5, 6].map((a) => backoffDelay(a, o))).toEqual([500, 1_000, 2_000, 4_000, 8_000, 8_000, 8_000]);
22
+ });
23
+
24
+ it("spreads a fleet across the window instead of retrying in lockstep", () => {
25
+ // Full jitter is the default because the retry storm IS the outage: every
26
+ // client on a dead node reconnecting at the same millisecond takes down the
27
+ // node the balancer sends them to.
28
+ const o = { baseMs: 1_000, factor: 2, maxMs: 30_000, jitter: "full" as const };
29
+ expect(backoffDelay(3, { ...o, random: fixedRandom(0) })).toBe(0);
30
+ expect(backoffDelay(3, { ...o, random: fixedRandom(0.5) })).toBe(4_000);
31
+ expect(backoffDelay(3, { ...o, random: fixedRandom(1) })).toBe(8_000);
32
+ });
33
+
34
+ it("equal jitter keeps half the window fixed", () => {
35
+ const o = { baseMs: 1_000, factor: 2, maxMs: 30_000, jitter: "equal" as const };
36
+ expect(backoffDelay(2, { ...o, random: fixedRandom(0) })).toBe(2_000);
37
+ expect(backoffDelay(2, { ...o, random: fixedRandom(1) })).toBe(4_000);
38
+ });
39
+ });
40
+
41
+ describe("code classification", () => {
42
+ it("classifies every code the contract declares, exactly once", () => {
43
+ // The point of this test: when the frozen contract gains an error code, it
44
+ // fails HERE rather than falling into whichever branch the default happened
45
+ // to be. An unclassified code silently becomes "stop", and a client that
46
+ // stops on something transient looks like a broken product.
47
+ for (const code of MEDIA_ERROR_CODES) {
48
+ const hits = (Object.keys(RECONNECT_CODE_CLASSES) as (keyof typeof RECONNECT_CODE_CLASSES)[]).filter((name) =>
49
+ RECONNECT_CODE_CLASSES[name].has(code),
50
+ );
51
+ expect(hits, `${code} is classified ${hits.length} times`).toHaveLength(1);
52
+ expect(classifyCode(code)).toBe(hits[0]);
53
+ }
54
+ });
55
+ });
56
+
57
+ describe("decisions", () => {
58
+ const options = { random: fixedRandom(0.5), jitter: "full" as const, baseMs: 1_000, factor: 2, maxMs: 30_000 };
59
+
60
+ it("sends a drained node's clients back through MEDIA_URL, no sooner than it asked", () => {
61
+ const cause: DisconnectCause = { type: "draining", reconnectAfterMs: 5_000 };
62
+ const decision = decideReconnect({ cause, attempt: 0, options });
63
+
64
+ expect(decision).toEqual<ReconnectDecision>({
65
+ action: "reconnect",
66
+ attempt: 0,
67
+ // The node's window, plus jitter so the whole population does not arrive
68
+ // at the replacement in one burst.
69
+ delayMs: 5_500,
70
+ viaMediaUrl: true,
71
+ });
72
+ });
73
+
74
+ it("stops when the room ended or the client closed", () => {
75
+ expect(decideReconnect({ cause: { type: "room_closed", reason: "ended" }, attempt: 0 })).toEqual({
76
+ action: "stop",
77
+ reason: "room_closed",
78
+ });
79
+ expect(decideReconnect({ cause: { type: "closed_by_client" }, attempt: 0 })).toEqual({
80
+ action: "stop",
81
+ reason: "client_closed",
82
+ });
83
+ });
84
+
85
+ it("retries a socket drop", () => {
86
+ const decision = decideReconnect({ cause: { type: "socket_closed", code: 1006 }, attempt: 2, options });
87
+ expect(decision).toMatchObject({ action: "reconnect", delayMs: 2_000, viaMediaUrl: true });
88
+ });
89
+
90
+ it("retries an expired credential once, then stops", () => {
91
+ // A laptop resumed from sleep deserves the retry. A revoked grant answers
92
+ // the same way for ever, and retrying it is a loop against the minter.
93
+ const cause: DisconnectCause = { type: "refused", code: "unauthorized" };
94
+ expect(decideReconnect({ cause, attempt: 0, options })).toMatchObject({ action: "reconnect" });
95
+ expect(decideReconnect({ cause, attempt: 1, options })).toEqual({
96
+ action: "stop",
97
+ reason: "refused",
98
+ code: "unauthorized",
99
+ });
100
+ });
101
+
102
+ it("waits out a duplicate identity instead of hammering", () => {
103
+ // The node has not reaped our previous session yet. Waiting is the fix.
104
+ const decision = decideReconnect({
105
+ cause: { type: "refused", code: "duplicate_identity" },
106
+ attempt: 0,
107
+ options: { ...options, duplicateIdentityFloorMs: 2_000 },
108
+ });
109
+ expect(decision).toMatchObject({ action: "reconnect", delayMs: 2_000 });
110
+ });
111
+
112
+ it("stops on a refusal a retry cannot fix", () => {
113
+ for (const code of ["protocol_version", "forbidden", "no_such_room", "bad_request"] as MediaErrorCode[]) {
114
+ expect(decideReconnect({ cause: { type: "refused", code }, attempt: 0, options })).toEqual({
115
+ action: "stop",
116
+ reason: "refused",
117
+ code,
118
+ });
119
+ }
120
+ });
121
+
122
+ it("gives up after maxAttempts", () => {
123
+ const decision = decideReconnect({
124
+ cause: { type: "socket_closed" },
125
+ attempt: DEFAULT_RECONNECT_OPTIONS.maxAttempts,
126
+ options,
127
+ });
128
+ expect(decision).toEqual({ action: "stop", reason: "attempts_exhausted" });
129
+ });
130
+ });
131
+
132
+ describe("the supervisor loop", () => {
133
+ const options = { random: fixedRandom(0.5), jitter: "full" as const, baseMs: 1_000, factor: 2, maxMs: 30_000 };
134
+
135
+ it("backs off across failures and resets the ladder after a successful join", async () => {
136
+ const slept: number[] = [];
137
+ const causes: DisconnectCause[] = [
138
+ { type: "socket_closed" }, // after the first successful join
139
+ { type: "room_closed", reason: "ended" }, // after the second
140
+ ];
141
+ let attempts = 0;
142
+
143
+ const stop = await superviseConnection<string>({
144
+ attempt: async () => {
145
+ attempts += 1;
146
+ // Two hard failures, then a join, then one more failure, then a join.
147
+ if (attempts <= 2) throw new MediaError("internal", "node refused");
148
+ return `session-${attempts}`;
149
+ },
150
+ waitForClose: async () => causes.shift() ?? { type: "closed_by_client" },
151
+ sleep: async (ms) => {
152
+ slept.push(ms);
153
+ },
154
+ options,
155
+ });
156
+
157
+ expect(stop).toEqual({ action: "stop", reason: "room_closed" });
158
+ // 500 = attempt 0, 1000 = attempt 1, then a join resets so the drop after it
159
+ // waits 500 again rather than 2000.
160
+ expect(slept).toEqual([500, 1_000, 500]);
161
+ expect(attempts).toBe(4);
162
+ });
163
+
164
+ it("stops without sleeping when the refusal is terminal", async () => {
165
+ const slept: number[] = [];
166
+ const stop = await superviseConnection<string>({
167
+ attempt: async () => {
168
+ throw new MediaError("protocol_version", "node speaks 2");
169
+ },
170
+ waitForClose: async () => ({ type: "closed_by_client" }),
171
+ sleep: async (ms) => {
172
+ slept.push(ms);
173
+ },
174
+ options,
175
+ });
176
+
177
+ expect(stop).toEqual({ action: "stop", reason: "refused", code: "protocol_version" });
178
+ expect(slept).toEqual([]);
179
+ });
180
+ });