@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.
- package/README.md +68 -0
- package/build/core/index.d.ts +17 -0
- package/build/core/index.d.ts.map +1 -0
- package/build/core/index.js +41 -0
- package/build/core/index.js.map +1 -0
- package/build/core/reconnect.d.ts +95 -0
- package/build/core/reconnect.d.ts.map +1 -0
- package/build/core/reconnect.js +160 -0
- package/build/core/reconnect.js.map +1 -0
- package/build/core/signal.d.ts +184 -0
- package/build/core/signal.d.ts.map +1 -0
- package/build/core/signal.js +416 -0
- package/build/core/signal.js.map +1 -0
- package/build/core/socket.d.ts +57 -0
- package/build/core/socket.d.ts.map +1 -0
- package/build/core/socket.js +37 -0
- package/build/core/socket.js.map +1 -0
- package/build/core/state.d.ts +67 -0
- package/build/core/state.d.ts.map +1 -0
- package/build/core/state.js +193 -0
- package/build/core/state.js.map +1 -0
- package/build/index.d.ts +29 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +51 -0
- package/build/index.js.map +1 -0
- package/build/protocol.d.ts +10 -0
- package/build/protocol.d.ts.map +1 -0
- package/build/protocol.js +26 -0
- package/build/protocol.js.map +1 -0
- package/build/react/index.d.ts +147 -0
- package/build/react/index.d.ts.map +1 -0
- package/build/react/index.js +319 -0
- package/build/react/index.js.map +1 -0
- package/build/room/browserDevice.d.ts +3 -0
- package/build/room/browserDevice.d.ts.map +1 -0
- package/build/room/browserDevice.js +94 -0
- package/build/room/browserDevice.js.map +1 -0
- package/build/room/device.d.ts +114 -0
- package/build/room/device.d.ts.map +1 -0
- package/build/room/device.js +3 -0
- package/build/room/device.js.map +1 -0
- package/build/room/room.d.ts +219 -0
- package/build/room/room.d.ts.map +1 -0
- package/build/room/room.js +438 -0
- package/build/room/room.js.map +1 -0
- package/package.json +69 -0
- package/src/_tests/clientBoundary.spec.ts +110 -0
- package/src/core/_tests/coreBoundary.spec.ts +70 -0
- package/src/core/_tests/fakeSignalServer.ts +188 -0
- package/src/core/_tests/reconnect.spec.ts +180 -0
- package/src/core/_tests/signal.spec.ts +347 -0
- package/src/core/_tests/state.spec.ts +226 -0
- package/src/core/index.ts +63 -0
- package/src/core/reconnect.ts +233 -0
- package/src/core/signal.ts +527 -0
- package/src/core/socket.ts +58 -0
- package/src/core/state.ts +251 -0
- package/src/index.ts +54 -0
- package/src/protocol.ts +9 -0
- package/src/react/_tests/hooks.spec.tsx +509 -0
- package/src/react/index.tsx +439 -0
- package/src/room/_tests/room.spec.ts +595 -0
- package/src/room/browserDevice.ts +114 -0
- package/src/room/device.ts +119 -0
- 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":";;;AA0DA,oCAWC;AAmED,oCAOC;AAED,0CA6BC;AA2BD,kDA+BC;AAtOD,qCAAgE;AAuCnD,QAAA,yBAAyB,GAAqB;IACzD,MAAM,EAAE,GAAG;IACX,KAAK,EAAE,MAAM;IACb,MAAM,EAAE,CAAC;IACT,MAAM,EAAE,MAAM;IACd,WAAW,EAAE,EAAE;IACf,wBAAwB,EAAE,KAAK;IAC/B,MAAM,EAAE,IAAI,CAAC,MAAM;CACpB,CAAC;AAEF;;;;;;GAMG;AACH,SAAgB,YAAY,CAAC,OAAe,EAAE,OAAmC;IAC/E,MAAM,CAAC,GAAG,EAAE,GAAG,iCAAyB,EAAE,GAAG,OAAO,EAAE,CAAC;IACvD,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IACvF,QAAQ,CAAC,CAAC,MAAM,EAAE,CAAC;QACjB,KAAK,MAAM;YACT,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC7B,KAAK,OAAO;YACV,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,GAAG,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC;QAC9D,KAAK,MAAM;YACT,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,GAAG,OAAO,CAAC,CAAC;IAC5C,CAAC;AACH,CAAC;AAwBD;;;;;;GAMG;AACH,MAAM,eAAe,GAAgC,IAAI,GAAG,CAAiB;IAC3E,eAAe;IACf,UAAU;IACV,UAAU;IACV,oBAAoB;CACrB,CAAC,CAAC;AAEH;;;;;;GAMG;AACH,MAAM,gBAAgB,GAAgC,IAAI,GAAG,CAAiB,CAAC,cAAc,EAAE,UAAU,CAAC,CAAC,CAAC;AAE5G,MAAM,cAAc,GAAgC,IAAI,GAAG,CAAiB;IAC1E,kBAAkB;IAClB,qBAAqB;IACrB,WAAW;IACX,kBAAkB;IAClB,oBAAoB;IACpB,aAAa;IACb,cAAc;IACd,kBAAkB;IAClB,mBAAmB;IACnB,aAAa;CACd,CAAC,CAAC;AAEH,uFAAuF;AAC1E,QAAA,sBAAsB,GAAG;IACpC,SAAS,EAAE,eAAe;IAC1B,UAAU,EAAE,gBAAgB;IAC5B,QAAQ,EAAE,cAAc;CAChB,CAAC;AAEX,SAAgB,YAAY,CAAC,IAAoB;IAC/C,IAAI,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO,WAAW,CAAC;IAClD,IAAI,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO,YAAY,CAAC;IACpD,IAAI,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO,UAAU,CAAC;IAChD,8EAA8E;IAC9E,+EAA+E;IAC/E,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,SAAgB,eAAe,CAAC,KAK/B;IACC,MAAM,CAAC,GAAG,EAAE,GAAG,iCAAyB,EAAE,GAAG,KAAK,CAAC,OAAO,EAAE,CAAC;IAC7D,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,KAAK,CAAC;IAEjC,IAAI,KAAK,CAAC,IAAI,KAAK,kBAAkB;QAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC;IAC1F,IAAI,KAAK,CAAC,IAAI,KAAK,aAAa;QAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC;IAEnF,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC7B,MAAM,IAAI,GAAG,YAAY,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACtC,IAAI,IAAI,KAAK,UAAU;YAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC;QACxF,IAAI,IAAI,KAAK,YAAY,IAAI,OAAO,IAAI,CAAC,EAAE,CAAC;YAC1C,0EAA0E;YAC1E,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC;QACjE,CAAC;IACH,CAAC;IAED,IAAI,OAAO,IAAI,CAAC,CAAC,WAAW;QAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,oBAAoB,EAAE,CAAC;IAEtF,OAAO;QACL,MAAM,EAAE,WAAW;QACnB,OAAO;QACP,OAAO,EAAE,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;QACpC,WAAW,EAAE,IAAI;KAClB,CAAC;AACJ,CAAC;AAED,SAAS,QAAQ,CAAC,KAAsB,EAAE,OAAe,EAAE,CAAmB;IAC5E,MAAM,OAAO,GAAG,YAAY,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;IACzC,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;QAC9B,4EAA4E;QAC5E,0EAA0E;QAC1E,4CAA4C;QAC5C,OAAO,KAAK,CAAC,gBAAgB,GAAG,OAAO,CAAC;IAC1C,CAAC;IACD,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI,KAAK,eAAe,EAAE,CAAC;QAC/D,OAAO,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC;IACrC,CAAC;IACD,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI,KAAK,oBAAoB,EAAE,CAAC;QACpE,OAAO,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,wBAAwB,CAAC,CAAC;IACvD,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;GAOG;AACI,KAAK,UAAU,mBAAmB,CAAW,KAOnD;IACC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC,EAAU,EAAE,EAAE,CAAC,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;IACvG,IAAI,OAAO,GAAG,CAAC,CAAC;IAEhB,SAAS,CAAC;QACR,IAAI,KAAsB,CAAC;QAC3B,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,KAAK,CAAC,OAAO,EAAE,CAAC;YACtC,yEAAyE;YACzE,yEAAyE;YACzE,OAAO,GAAG,CAAC,CAAC;YACZ,KAAK,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,CAAC;YAC7B,KAAK,GAAG,MAAM,KAAK,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;QAC5C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,KAAK,GAAG,IAAA,uBAAc,EAAC,KAAK,CAAC,CAAC;QAChC,CAAC;QAED,MAAM,QAAQ,GAAG,eAAe,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QAC7E,KAAK,CAAC,UAAU,EAAE,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;QACpC,IAAI,QAAQ,CAAC,MAAM,KAAK,MAAM;YAAE,OAAO,QAAQ,CAAC;QAEhD,MAAM,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QAC9B,OAAO,IAAI,CAAC,CAAC;IACf,CAAC;AACH,CAAC"}
|
|
@@ -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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"}
|