@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
package/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # @tribe-nest/media-client
2
+
3
+ The client SDK for the media network. This is **P3a: the headless core only.**
4
+
5
+ ```
6
+ src/core/ the ./core subpath. ISOMORPHIC: no DOM, no mediasoup-client,
7
+ no React.
8
+ signal.ts one socket, media-protocol frames, request/reply correlation,
9
+ and the reason a connection ended
10
+ reconnect.ts backoff with jitter, and what to do about a draining node
11
+ state.ts a pure reducer: peers, producers, active speakers, recording
12
+ socket.ts the WebSocketLike shape and the injected factory
13
+
14
+ src/protocol.ts a re-export of @tribe-nest/media-protocol. Nothing added.
15
+ ```
16
+
17
+ ## Why `./core` is not a browser SDK
18
+
19
+ The load harness, the egress client and the SIP gateway all need a protocol
20
+ client, and none of them is a browser. Without a headless subpath each of them
21
+ hand-rolls a socket, and three hand-rolled clients drift from each other and
22
+ from the node. `src/core/_tests/coreBoundary.spec.ts` and the DOM-free `lib` in
23
+ `tsconfig.json` are what hold the line.
24
+
25
+ The room API, `mediasoup-client`, and the React and Forge wrappers are **P3b**
26
+ and land at other subpaths. There is deliberately no `.` export yet.
27
+
28
+ ## The three things the core owns
29
+
30
+ **Correlation.** A request carries an `id` and gets exactly one reply with that
31
+ `id`; an event carries no `id`. That is the whole protocol, so it is the whole
32
+ of `MediaSignal`.
33
+
34
+ **The handshake.** `join` is the first frame on the socket and carries the
35
+ token. Never a query parameter: a query string lands in load-balancer access
36
+ logs, and a join ticket in a log is a join ticket for anyone who can read logs.
37
+ `connect()` resolves only when both the join reply and the `joined` event have
38
+ arrived, in either order, because the reply says the node accepted the token and
39
+ the event carries the room.
40
+
41
+ **Why the connection ended.** A bare socket close tells a caller nothing, and
42
+ `draining`, `roomClosed` and a refusal need three different responses. The
43
+ `DisconnectCause` is what `decideReconnect` decides on.
44
+
45
+ ## Credentials are a callback, never a value
46
+
47
+ ```ts
48
+ const signal = new MediaSignal({
49
+ getCredentials: async () => {
50
+ const { mediaUrl, token } = await mintJoinToken();
51
+ return { mediaUrl, token };
52
+ },
53
+ });
54
+ ```
55
+
56
+ A join ticket expires in minutes and a call lasts an hour. A token passed once
57
+ is not a naming choice, it is a first-reconnect failure.
58
+
59
+ ## Testing
60
+
61
+ `npm run test` runs everything against an in-process fake node
62
+ (`src/core/_tests/fakeSignalServer.ts`). No network, no mediasoup, no browser.
63
+ The fake parses every frame the client sends with the contract's own
64
+ `requestFrameSchema`, so the question the suite answers is "does this client
65
+ emit frames the frozen schema accepts", not "does this client talk to this
66
+ fake".
67
+
68
+ Integration against a real node (`P1`) is not here and is not stubbed.
@@ -0,0 +1,17 @@
1
+ /**
2
+ * `@tribe-nest/media-client/core` - the headless half of the SDK.
3
+ *
4
+ * No DOM, no `mediasoup-client`, no React, and that is load-bearing rather than
5
+ * tidy: the load harness, the egress client and the SIP gateway all need a
6
+ * protocol client and none of them is a browser. Without this subpath each of
7
+ * them hand-rolls a socket and the three drift.
8
+ *
9
+ * What lives here: the signalling client, the reconnect policy and the room
10
+ * state reducer. The room API, `mediasoup-client` and the React and Forge
11
+ * wrappers are P3b and are exported from other subpaths.
12
+ */
13
+ export { MediaSignal, assertTokenNotInUrl, causeFromError, type DisconnectCause, type MediaCoreCredentials, type MediaSignalOptions, type SignalLogLevel, type SignalPhase, type SignalRequest, } from "./signal";
14
+ export { DEFAULT_RECONNECT_OPTIONS, RECONNECT_CODE_CLASSES, backoffDelay, classifyCode, decideReconnect, superviseConnection, type JitterMode, type ReconnectDecision, type ReconnectOptions, type StopReason, } from "./reconnect";
15
+ export { activeProducers, initialRoomState, producerById, producersOf, reduceRoomState, reduceRoomStateAll, visibleProducers, type ProducerEntry, type RoomState, } from "./state";
16
+ export { SOCKET_CLOSED, SOCKET_CLOSING, SOCKET_CONNECTING, SOCKET_OPEN, defaultWebSocketFactory, type MediaWebSocketFactory, type SocketCloseEvent, type SocketErrorEvent, type SocketEventMap, type SocketMessageEvent, type SocketOpenEvent, type WebSocketLike, } from "./socket";
17
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EACL,WAAW,EACX,mBAAmB,EACnB,cAAc,EACd,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,aAAa,GACnB,MAAM,UAAU,CAAC;AAElB,OAAO,EACL,yBAAyB,EACzB,sBAAsB,EACtB,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,mBAAmB,EACnB,KAAK,UAAU,EACf,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,UAAU,GAChB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,YAAY,EACZ,WAAW,EACX,eAAe,EACf,kBAAkB,EAClB,gBAAgB,EAChB,KAAK,aAAa,EAClB,KAAK,SAAS,GACf,MAAM,SAAS,CAAC;AAEjB,OAAO,EACL,aAAa,EACb,cAAc,EACd,iBAAiB,EACjB,WAAW,EACX,uBAAuB,EACvB,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,eAAe,EACpB,KAAK,aAAa,GACnB,MAAM,UAAU,CAAC"}
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.defaultWebSocketFactory = exports.SOCKET_OPEN = exports.SOCKET_CONNECTING = exports.SOCKET_CLOSING = exports.SOCKET_CLOSED = exports.visibleProducers = exports.reduceRoomStateAll = exports.reduceRoomState = exports.producersOf = exports.producerById = exports.initialRoomState = exports.activeProducers = exports.superviseConnection = exports.decideReconnect = exports.classifyCode = exports.backoffDelay = exports.RECONNECT_CODE_CLASSES = exports.DEFAULT_RECONNECT_OPTIONS = exports.causeFromError = exports.assertTokenNotInUrl = exports.MediaSignal = void 0;
4
+ /**
5
+ * `@tribe-nest/media-client/core` - the headless half of the SDK.
6
+ *
7
+ * No DOM, no `mediasoup-client`, no React, and that is load-bearing rather than
8
+ * tidy: the load harness, the egress client and the SIP gateway all need a
9
+ * protocol client and none of them is a browser. Without this subpath each of
10
+ * them hand-rolls a socket and the three drift.
11
+ *
12
+ * What lives here: the signalling client, the reconnect policy and the room
13
+ * state reducer. The room API, `mediasoup-client` and the React and Forge
14
+ * wrappers are P3b and are exported from other subpaths.
15
+ */
16
+ var signal_1 = require("./signal");
17
+ Object.defineProperty(exports, "MediaSignal", { enumerable: true, get: function () { return signal_1.MediaSignal; } });
18
+ Object.defineProperty(exports, "assertTokenNotInUrl", { enumerable: true, get: function () { return signal_1.assertTokenNotInUrl; } });
19
+ Object.defineProperty(exports, "causeFromError", { enumerable: true, get: function () { return signal_1.causeFromError; } });
20
+ var reconnect_1 = require("./reconnect");
21
+ Object.defineProperty(exports, "DEFAULT_RECONNECT_OPTIONS", { enumerable: true, get: function () { return reconnect_1.DEFAULT_RECONNECT_OPTIONS; } });
22
+ Object.defineProperty(exports, "RECONNECT_CODE_CLASSES", { enumerable: true, get: function () { return reconnect_1.RECONNECT_CODE_CLASSES; } });
23
+ Object.defineProperty(exports, "backoffDelay", { enumerable: true, get: function () { return reconnect_1.backoffDelay; } });
24
+ Object.defineProperty(exports, "classifyCode", { enumerable: true, get: function () { return reconnect_1.classifyCode; } });
25
+ Object.defineProperty(exports, "decideReconnect", { enumerable: true, get: function () { return reconnect_1.decideReconnect; } });
26
+ Object.defineProperty(exports, "superviseConnection", { enumerable: true, get: function () { return reconnect_1.superviseConnection; } });
27
+ var state_1 = require("./state");
28
+ Object.defineProperty(exports, "activeProducers", { enumerable: true, get: function () { return state_1.activeProducers; } });
29
+ Object.defineProperty(exports, "initialRoomState", { enumerable: true, get: function () { return state_1.initialRoomState; } });
30
+ Object.defineProperty(exports, "producerById", { enumerable: true, get: function () { return state_1.producerById; } });
31
+ Object.defineProperty(exports, "producersOf", { enumerable: true, get: function () { return state_1.producersOf; } });
32
+ Object.defineProperty(exports, "reduceRoomState", { enumerable: true, get: function () { return state_1.reduceRoomState; } });
33
+ Object.defineProperty(exports, "reduceRoomStateAll", { enumerable: true, get: function () { return state_1.reduceRoomStateAll; } });
34
+ Object.defineProperty(exports, "visibleProducers", { enumerable: true, get: function () { return state_1.visibleProducers; } });
35
+ var socket_1 = require("./socket");
36
+ Object.defineProperty(exports, "SOCKET_CLOSED", { enumerable: true, get: function () { return socket_1.SOCKET_CLOSED; } });
37
+ Object.defineProperty(exports, "SOCKET_CLOSING", { enumerable: true, get: function () { return socket_1.SOCKET_CLOSING; } });
38
+ Object.defineProperty(exports, "SOCKET_CONNECTING", { enumerable: true, get: function () { return socket_1.SOCKET_CONNECTING; } });
39
+ Object.defineProperty(exports, "SOCKET_OPEN", { enumerable: true, get: function () { return socket_1.SOCKET_OPEN; } });
40
+ Object.defineProperty(exports, "defaultWebSocketFactory", { enumerable: true, get: function () { return socket_1.defaultWebSocketFactory; } });
41
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;GAWG;AACH,mCAUkB;AAThB,qGAAA,WAAW,OAAA;AACX,6GAAA,mBAAmB,OAAA;AACnB,wGAAA,cAAc,OAAA;AAShB,yCAWqB;AAVnB,sHAAA,yBAAyB,OAAA;AACzB,mHAAA,sBAAsB,OAAA;AACtB,yGAAA,YAAY,OAAA;AACZ,yGAAA,YAAY,OAAA;AACZ,4GAAA,eAAe,OAAA;AACf,gHAAA,mBAAmB,OAAA;AAOrB,iCAUiB;AATf,wGAAA,eAAe,OAAA;AACf,yGAAA,gBAAgB,OAAA;AAChB,qGAAA,YAAY,OAAA;AACZ,oGAAA,WAAW,OAAA;AACX,wGAAA,eAAe,OAAA;AACf,2GAAA,kBAAkB,OAAA;AAClB,yGAAA,gBAAgB,OAAA;AAKlB,mCAakB;AAZhB,uGAAA,aAAa,OAAA;AACb,wGAAA,cAAc,OAAA;AACd,2GAAA,iBAAiB,OAAA;AACjB,qGAAA,WAAW,OAAA;AACX,iHAAA,uBAAuB,OAAA"}
@@ -0,0 +1,95 @@
1
+ import { type MediaErrorCode } from "@tribe-nest/media-protocol";
2
+ import { type DisconnectCause } from "./signal";
3
+ /**
4
+ * When to try again, and when to stop.
5
+ *
6
+ * Two decisions, both of which cost an outage if guessed:
7
+ *
8
+ * **Jitter is not a refinement.** Every client on a node that just died
9
+ * reconnects at once. Without jitter they arrive in lockstep, land together on
10
+ * whichever node the balancer picks, and take that one down too. The retry
11
+ * storm is the outage; the original failure was one node.
12
+ *
13
+ * **`draining` is not a failure.** The node is asking to be left. Retrying the
14
+ * same node is the one response that cannot work, so a drain always sends the
15
+ * client back through `MEDIA_URL` for a fresh placement and a fresh token, and
16
+ * waits at least as long as the node asked.
17
+ *
18
+ * Everything here is pure. `Math.random` is injectable, so a spec pins exact
19
+ * delays rather than asserting a range and hoping.
20
+ */
21
+ export type JitterMode = "full" | "equal" | "none";
22
+ export type ReconnectOptions = {
23
+ baseMs: number;
24
+ maxMs: number;
25
+ factor: number;
26
+ jitter: JitterMode;
27
+ /** Give up after this many consecutive failed attempts. */
28
+ maxAttempts: number;
29
+ /**
30
+ * A reconnect that races the node's reaping of our old session comes back
31
+ * `duplicate_identity`. Waiting is the fix, so this is a floor rather than a
32
+ * refusal.
33
+ */
34
+ duplicateIdentityFloorMs: number;
35
+ random: () => number;
36
+ };
37
+ export declare const DEFAULT_RECONNECT_OPTIONS: ReconnectOptions;
38
+ /**
39
+ * Delay before attempt number `attempt` (0 = the first retry).
40
+ *
41
+ * Full jitter is the default because it is the variant that actually
42
+ * decorrelates a fleet: exponential-with-a-small-fudge still has every client
43
+ * retrying inside the same narrow window.
44
+ */
45
+ export declare function backoffDelay(attempt: number, options?: Partial<ReconnectOptions>): number;
46
+ export type StopReason = "client_closed" | "room_closed" | "refused" | "attempts_exhausted";
47
+ export type ReconnectDecision = {
48
+ action: "stop";
49
+ reason: StopReason;
50
+ code?: MediaErrorCode;
51
+ } | {
52
+ action: "reconnect";
53
+ delayMs: number;
54
+ /** The attempt this decision authorises, 0-based. */
55
+ attempt: number;
56
+ /**
57
+ * Always true, and stated in the type rather than in prose: the client
58
+ * re-resolves through `MEDIA_URL` and takes a FRESH token every time. It
59
+ * never holds a node address (the wire deliberately carries no `nodeId`),
60
+ * and a join ticket lives minutes.
61
+ */
62
+ viaMediaUrl: true;
63
+ };
64
+ /** Exposed so the exhaustiveness spec can assert every contract code is classified. */
65
+ export declare const RECONNECT_CODE_CLASSES: {
66
+ readonly retryable: ReadonlySet<"room_closed" | "unauthorized" | "replayed" | "protocol_version" | "forbidden_transport" | "forbidden" | "not_subscribable" | "subscription_limit" | "no_such_room" | "no_such_producer" | "no_such_transport" | "duplicate_identity" | "node_draining" | "capacity" | "bad_request" | "internal">;
67
+ readonly credential: ReadonlySet<"room_closed" | "unauthorized" | "replayed" | "protocol_version" | "forbidden_transport" | "forbidden" | "not_subscribable" | "subscription_limit" | "no_such_room" | "no_such_producer" | "no_such_transport" | "duplicate_identity" | "node_draining" | "capacity" | "bad_request" | "internal">;
68
+ readonly terminal: ReadonlySet<"room_closed" | "unauthorized" | "replayed" | "protocol_version" | "forbidden_transport" | "forbidden" | "not_subscribable" | "subscription_limit" | "no_such_room" | "no_such_producer" | "no_such_transport" | "duplicate_identity" | "node_draining" | "capacity" | "bad_request" | "internal">;
69
+ };
70
+ export declare function classifyCode(code: MediaErrorCode): "retryable" | "credential" | "terminal";
71
+ export declare function decideReconnect(input: {
72
+ cause: DisconnectCause;
73
+ /** Consecutive failed attempts since the last successful join. 0 on the first drop. */
74
+ attempt: number;
75
+ options?: Partial<ReconnectOptions>;
76
+ }): ReconnectDecision;
77
+ /**
78
+ * Drives connect-then-wait-then-decide until something says stop.
79
+ *
80
+ * Deliberately knows nothing about sockets: `attempt` returns whatever a
81
+ * session is and `waitForClose` resolves with why it ended. That is what makes
82
+ * the whole loop testable with no server, and it is the same loop the room API
83
+ * (P3b) should use rather than deriving a second one.
84
+ */
85
+ export declare function superviseConnection<TSession>(input: {
86
+ attempt: () => Promise<TSession>;
87
+ waitForClose: (session: TSession) => Promise<DisconnectCause>;
88
+ onConnected?: (session: TSession) => void;
89
+ onDecision?: (decision: ReconnectDecision, cause: DisconnectCause) => void;
90
+ sleep?: (ms: number) => Promise<void>;
91
+ options?: Partial<ReconnectOptions>;
92
+ }): Promise<Extract<ReconnectDecision, {
93
+ action: "stop";
94
+ }>>;
95
+ //# sourceMappingURL=reconnect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reconnect.d.ts","sourceRoot":"","sources":["../../src/core/reconnect.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAEjE,OAAO,EAAkB,KAAK,eAAe,EAAE,MAAM,UAAU,CAAC;AAEhE;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;AAEnD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,UAAU,CAAC;IACnB,2DAA2D;IAC3D,WAAW,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,wBAAwB,EAAE,MAAM,CAAC;IACjC,MAAM,EAAE,MAAM,MAAM,CAAC;CACtB,CAAC;AAEF,eAAO,MAAM,yBAAyB,EAAE,gBAQvC,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,gBAAgB,CAAC,GAAG,MAAM,CAWzF;AAED,MAAM,MAAM,UAAU,GAClB,eAAe,GACf,aAAa,GACb,SAAS,GACT,oBAAoB,CAAC;AAEzB,MAAM,MAAM,iBAAiB,GACzB;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,UAAU,CAAC;IAAC,IAAI,CAAC,EAAE,cAAc,CAAA;CAAE,GAC7D;IACE,MAAM,EAAE,WAAW,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,qDAAqD;IACrD,OAAO,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,WAAW,EAAE,IAAI,CAAC;CACnB,CAAC;AAsCN,uFAAuF;AACvF,eAAO,MAAM,sBAAsB;;;;CAIzB,CAAC;AAEX,wBAAgB,YAAY,CAAC,IAAI,EAAE,cAAc,GAAG,WAAW,GAAG,YAAY,GAAG,UAAU,CAO1F;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE;IACrC,KAAK,EAAE,eAAe,CAAC;IACvB,uFAAuF;IACvF,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC;CACrC,GAAG,iBAAiB,CAwBpB;AAmBD;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,KAAK,EAAE;IACzD,OAAO,EAAE,MAAM,OAAO,CAAC,QAAQ,CAAC,CAAC;IACjC,YAAY,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC;IAC9D,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,IAAI,CAAC;IAC1C,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,iBAAiB,EAAE,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;IAC3E,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,OAAO,CAAC,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC;CACrC,GAAG,OAAO,CAAC,OAAO,CAAC,iBAAiB,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC,CAwB1D"}
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RECONNECT_CODE_CLASSES = exports.DEFAULT_RECONNECT_OPTIONS = void 0;
4
+ exports.backoffDelay = backoffDelay;
5
+ exports.classifyCode = classifyCode;
6
+ exports.decideReconnect = decideReconnect;
7
+ exports.superviseConnection = superviseConnection;
8
+ const signal_1 = require("./signal");
9
+ exports.DEFAULT_RECONNECT_OPTIONS = {
10
+ baseMs: 500,
11
+ maxMs: 30_000,
12
+ factor: 2,
13
+ jitter: "full",
14
+ maxAttempts: 10,
15
+ duplicateIdentityFloorMs: 2_000,
16
+ random: Math.random,
17
+ };
18
+ /**
19
+ * Delay before attempt number `attempt` (0 = the first retry).
20
+ *
21
+ * Full jitter is the default because it is the variant that actually
22
+ * decorrelates a fleet: exponential-with-a-small-fudge still has every client
23
+ * retrying inside the same narrow window.
24
+ */
25
+ function backoffDelay(attempt, options) {
26
+ const o = { ...exports.DEFAULT_RECONNECT_OPTIONS, ...options };
27
+ const ceiling = Math.min(o.maxMs, o.baseMs * Math.pow(o.factor, Math.max(0, attempt)));
28
+ switch (o.jitter) {
29
+ case "none":
30
+ return Math.round(ceiling);
31
+ case "equal":
32
+ return Math.round(ceiling / 2 + o.random() * (ceiling / 2));
33
+ case "full":
34
+ return Math.round(o.random() * ceiling);
35
+ }
36
+ }
37
+ /**
38
+ * Codes that a later attempt could plausibly survive.
39
+ *
40
+ * Split by hand, and pinned by a spec against `MEDIA_ERROR_CODES`, so that a
41
+ * code added to the contract fails a test here instead of silently falling into
42
+ * whichever branch the default happened to be.
43
+ */
44
+ const RETRYABLE_CODES = new Set([
45
+ "node_draining",
46
+ "capacity",
47
+ "internal",
48
+ "duplicate_identity",
49
+ ]);
50
+ /**
51
+ * Codes that mean "the credential was wrong", which a fresh mint may fix once.
52
+ *
53
+ * Once, not forever: a laptop resumed from sleep presents an expired ticket and
54
+ * deserves a retry, but a revoked grant answers the same way every time and
55
+ * retrying it is a loop against the minting endpoint.
56
+ */
57
+ const CREDENTIAL_CODES = new Set(["unauthorized", "replayed"]);
58
+ const TERMINAL_CODES = new Set([
59
+ "protocol_version",
60
+ "forbidden_transport",
61
+ "forbidden",
62
+ "not_subscribable",
63
+ "subscription_limit",
64
+ "room_closed",
65
+ "no_such_room",
66
+ "no_such_producer",
67
+ "no_such_transport",
68
+ "bad_request",
69
+ ]);
70
+ /** Exposed so the exhaustiveness spec can assert every contract code is classified. */
71
+ exports.RECONNECT_CODE_CLASSES = {
72
+ retryable: RETRYABLE_CODES,
73
+ credential: CREDENTIAL_CODES,
74
+ terminal: TERMINAL_CODES,
75
+ };
76
+ function classifyCode(code) {
77
+ if (RETRYABLE_CODES.has(code))
78
+ return "retryable";
79
+ if (CREDENTIAL_CODES.has(code))
80
+ return "credential";
81
+ if (TERMINAL_CODES.has(code))
82
+ return "terminal";
83
+ // Unclassified is treated as terminal: a client that retries something nobody
84
+ // reasoned about hammers the node for as long as the user leaves the tab open.
85
+ return "terminal";
86
+ }
87
+ function decideReconnect(input) {
88
+ const o = { ...exports.DEFAULT_RECONNECT_OPTIONS, ...input.options };
89
+ const { cause, attempt } = input;
90
+ if (cause.type === "closed_by_client")
91
+ return { action: "stop", reason: "client_closed" };
92
+ if (cause.type === "room_closed")
93
+ return { action: "stop", reason: "room_closed" };
94
+ if (cause.type === "refused") {
95
+ const kind = classifyCode(cause.code);
96
+ if (kind === "terminal")
97
+ return { action: "stop", reason: "refused", code: cause.code };
98
+ if (kind === "credential" && attempt >= 1) {
99
+ // The retry already carried a freshly minted token and was refused again.
100
+ return { action: "stop", reason: "refused", code: cause.code };
101
+ }
102
+ }
103
+ if (attempt >= o.maxAttempts)
104
+ return { action: "stop", reason: "attempts_exhausted" };
105
+ return {
106
+ action: "reconnect",
107
+ attempt,
108
+ delayMs: delayFor(cause, attempt, o),
109
+ viaMediaUrl: true,
110
+ };
111
+ }
112
+ function delayFor(cause, attempt, o) {
113
+ const backoff = backoffDelay(attempt, o);
114
+ if (cause.type === "draining") {
115
+ // The node named a window; honour it and add the jittered backoff on top so
116
+ // a drained node's whole population does not arrive at the replacement in
117
+ // one burst the instant the window expires.
118
+ return cause.reconnectAfterMs + backoff;
119
+ }
120
+ if (cause.type === "refused" && cause.code === "node_draining") {
121
+ return Math.max(backoff, o.baseMs);
122
+ }
123
+ if (cause.type === "refused" && cause.code === "duplicate_identity") {
124
+ return Math.max(backoff, o.duplicateIdentityFloorMs);
125
+ }
126
+ return backoff;
127
+ }
128
+ /**
129
+ * Drives connect-then-wait-then-decide until something says stop.
130
+ *
131
+ * Deliberately knows nothing about sockets: `attempt` returns whatever a
132
+ * session is and `waitForClose` resolves with why it ended. That is what makes
133
+ * the whole loop testable with no server, and it is the same loop the room API
134
+ * (P3b) should use rather than deriving a second one.
135
+ */
136
+ async function superviseConnection(input) {
137
+ const sleep = input.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
138
+ let attempt = 0;
139
+ for (;;) {
140
+ let cause;
141
+ try {
142
+ const session = await input.attempt();
143
+ // A successful join resets the ladder: an hour-long call that drops once
144
+ // should not start at a 30-second delay because of a blip at minute two.
145
+ attempt = 0;
146
+ input.onConnected?.(session);
147
+ cause = await input.waitForClose(session);
148
+ }
149
+ catch (error) {
150
+ cause = (0, signal_1.causeFromError)(error);
151
+ }
152
+ const decision = decideReconnect({ cause, attempt, options: input.options });
153
+ input.onDecision?.(decision, cause);
154
+ if (decision.action === "stop")
155
+ return decision;
156
+ await sleep(decision.delayMs);
157
+ attempt += 1;
158
+ }
159
+ }
160
+ //# sourceMappingURL=reconnect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reconnect.js","sourceRoot":"","sources":["../../src/core/reconnect.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,184 @@
1
+ import { type EventFrame, type EventName, type MediaErrorCode, type RequestFrame } from "@tribe-nest/media-protocol";
2
+ import { type MediaWebSocketFactory } from "./socket";
3
+ /**
4
+ * The headless signalling client: one socket, media-protocol frames, nothing
5
+ * else.
6
+ *
7
+ * It owns exactly three things the wire specifies and no policy beyond them:
8
+ *
9
+ * 1. **Request/reply correlation.** A request carries an `id` and gets exactly
10
+ * one reply with that `id`; an event carries no `id` and is never replied
11
+ * to. That is the whole protocol, so it is the whole of this class.
12
+ * 2. **The handshake.** `join` is the FIRST frame on the socket and carries the
13
+ * token. Nothing else may be sent before it.
14
+ * 3. **Why the connection ended**, as a `DisconnectCause` the reconnect policy
15
+ * can decide on. A socket that just closes tells the caller nothing, and
16
+ * "the node is draining" and "the room ended" need opposite responses.
17
+ *
18
+ * It deliberately does NOT reconnect, hold room state or know what a track is.
19
+ * Those are `core/reconnect.ts`, `core/state.ts` and P3b's room API.
20
+ */
21
+ /** What a caller must supply to open a connection. Fetched fresh EVERY attempt. */
22
+ export type MediaCoreCredentials = {
23
+ /** `MEDIA_URL`. The load balancer, never a node address the client picked. */
24
+ mediaUrl: string;
25
+ /** A join ticket, minted per attempt. */
26
+ token: string;
27
+ };
28
+ export type SignalLogLevel = "debug" | "warn" | "error";
29
+ export type MediaSignalOptions = {
30
+ /**
31
+ * Called before EVERY connection attempt, never once at construction.
32
+ *
33
+ * A join ticket expires in minutes and a call lasts an hour, so a token
34
+ * handed over once is a defect rather than a naming choice: the first
35
+ * reconnect after a network blip would present an expired token.
36
+ */
37
+ getCredentials: () => Promise<MediaCoreCredentials> | MediaCoreCredentials;
38
+ /** Defaults to `globalThis.WebSocket`, resolved lazily. */
39
+ webSocket?: MediaWebSocketFactory;
40
+ requestTimeoutMs?: number;
41
+ joinTimeoutMs?: number;
42
+ /**
43
+ * Overridable only so a spec can drive a mismatch. Production always sends
44
+ * `MEDIA_PROTOCOL_VERSION`.
45
+ */
46
+ protocolVersion?: number;
47
+ onLog?: (level: SignalLogLevel, message: string, detail?: unknown) => void;
48
+ };
49
+ export type SignalPhase = "idle" | "connecting" | "joining" | "joined" | "closed";
50
+ /**
51
+ * Why the connection ended.
52
+ *
53
+ * The distinctions are the ones a reconnect decision actually turns on: a
54
+ * drained node must not be retried, a closed room must not be rejoined, and a
55
+ * refusal carries the code the caller has to show a human.
56
+ */
57
+ export type DisconnectCause = {
58
+ type: "closed_by_client";
59
+ } | {
60
+ type: "room_closed";
61
+ reason: string;
62
+ } | {
63
+ type: "draining";
64
+ reconnectAfterMs: number;
65
+ } | {
66
+ type: "refused";
67
+ code: MediaErrorCode;
68
+ message?: string;
69
+ } | {
70
+ type: "socket_closed";
71
+ code?: number;
72
+ reason?: string;
73
+ };
74
+ type JoinedFrame = Extract<EventFrame, {
75
+ event: "joined";
76
+ }>;
77
+ /** `Omit` over a union collapses it to the common keys, which for a frame union
78
+ * is just `method`. Distributing keeps every variant's payload. */
79
+ type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
80
+ /**
81
+ * Everything a caller may send. `join` is absent on purpose: the handshake
82
+ * belongs to `connect()`, and a second join on a live socket is not something
83
+ * the protocol has an answer for.
84
+ */
85
+ export type SignalRequest = DistributiveOmit<Exclude<RequestFrame, {
86
+ method: "join";
87
+ }>, "id">;
88
+ type AnyHandler = (frame: EventFrame) => void;
89
+ type CloseHandler = (cause: DisconnectCause) => void;
90
+ /**
91
+ * A join ticket in a URL is a join ticket in every access log between the
92
+ * client and the node, and load balancers log query strings by default. The
93
+ * contract puts the token in the first frame; this refuses a URL that looks
94
+ * like someone routed around that.
95
+ */
96
+ export declare function assertTokenNotInUrl(mediaUrl: string): void;
97
+ /** Maps a thrown error back to the cause the reconnect policy decides on. */
98
+ export declare function causeFromError(error: unknown): DisconnectCause;
99
+ export declare class MediaSignal {
100
+ private readonly options;
101
+ private socket;
102
+ private nextId;
103
+ private readonly pending;
104
+ private readonly handlers;
105
+ private readonly anyHandlers;
106
+ private readonly closeHandlers;
107
+ /** Set by the events that explain a close BEFORE the socket goes away. */
108
+ private terminalCause;
109
+ private phaseValue;
110
+ private identityValue;
111
+ private joinedResolve;
112
+ private connectReject;
113
+ /**
114
+ * Which connection attempt is the current one.
115
+ *
116
+ * `connect()` awaits `getCredentials()` before it has a socket, and until that
117
+ * socket exists there is nothing for `close()` to close: it flips the phase
118
+ * and returns. So the attempt is stamped and re-checked, and an attempt that
119
+ * is no longer the current one abandons itself instead of opening a socket
120
+ * and JOINING with a live token that nothing then holds a reference to.
121
+ */
122
+ private attempt;
123
+ private boundClose;
124
+ constructor(options: MediaSignalOptions);
125
+ get phase(): SignalPhase;
126
+ /** Our own identity, as the node reported it in `joined`. */
127
+ get identity(): string | null;
128
+ /**
129
+ * Opens the socket, sends `join` as the first frame, and resolves once BOTH
130
+ * the join reply and the `joined` event have arrived.
131
+ *
132
+ * Waiting for both is deliberate. The reply says the node accepted the token;
133
+ * the event carries `routerRtpCapabilities`, the peer snapshot and the
134
+ * recording flag. A caller that resolved on the reply alone would be handed a
135
+ * room it knows nothing about, and the contract fixes no order between the
136
+ * two frames, so neither may be assumed to arrive first.
137
+ */
138
+ connect(): Promise<JoinedFrame>;
139
+ /**
140
+ * Sends a request and resolves with its `data`.
141
+ *
142
+ * `unknown`, not a guessed shape: the wire declares `data: unknown` because
143
+ * most of it is mediasoup's, and a client that pretends otherwise is asserting
144
+ * a contract this package does not hold.
145
+ */
146
+ request(frame: SignalRequest, timeoutMs?: number): Promise<unknown>;
147
+ /**
148
+ * Best-effort `leave` then close.
149
+ *
150
+ * Best-effort because the node tears the session down on socket close anyway.
151
+ * A leave that hangs must not stop a user closing a tab.
152
+ */
153
+ leave(): Promise<void>;
154
+ /** Closes the socket. Idempotent. */
155
+ close(): void;
156
+ on<K extends EventName>(event: K, handler: (frame: Extract<EventFrame, {
157
+ event: K;
158
+ }>) => void): () => void;
159
+ /** Every event, in arrival order. This is what the state reducer is fed. */
160
+ onAny(handler: AnyHandler): () => void;
161
+ onClose(handler: CloseHandler): () => void;
162
+ /**
163
+ * Throws when this attempt has been overtaken: either `close()` ran, or a
164
+ * second `connect()` started (which the entry guard permits once the phase is
165
+ * `closed`, so an abandoned attempt must not go on to open a second socket).
166
+ */
167
+ private assertAttemptIsCurrent;
168
+ private resetForConnect;
169
+ private bind;
170
+ private waitForOpen;
171
+ private performJoin;
172
+ private withDeadline;
173
+ private track;
174
+ private settle;
175
+ private send;
176
+ private handleMessage;
177
+ private dispatch;
178
+ private handleEvent;
179
+ private handleClose;
180
+ private closeSocket;
181
+ private log;
182
+ }
183
+ export {};
184
+ //# sourceMappingURL=signal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signal.d.ts","sourceRoot":"","sources":["../../src/core/signal.ts"],"names":[],"mappings":"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"}