livekit-client 2.22.0 → 2.22.2
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/dist/livekit-client.e2ee.worker.js +1 -1
- package/dist/livekit-client.e2ee.worker.js.map +1 -1
- package/dist/livekit-client.e2ee.worker.mjs +364 -40
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +1847 -179
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.fm.worker.js +1 -1
- package/dist/livekit-client.fm.worker.js.map +1 -1
- package/dist/livekit-client.fm.worker.mjs +187 -1
- package/dist/livekit-client.fm.worker.mjs.map +1 -1
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/src/api/SignalClient.d.ts +25 -1
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/api/SignalClientStateMachine.d.ts +85 -0
- package/dist/src/api/SignalClientStateMachine.d.ts.map +1 -0
- package/dist/src/api/WebSocketStream.d.ts.map +1 -1
- package/dist/src/api/utils.d.ts.map +1 -1
- package/dist/src/e2ee/E2eeManager.d.ts +7 -0
- package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
- package/dist/src/e2ee/constants.d.ts +5 -0
- package/dist/src/e2ee/constants.d.ts.map +1 -1
- package/dist/src/e2ee/types.d.ts +9 -2
- package/dist/src/e2ee/types.d.ts.map +1 -1
- package/dist/src/e2ee/worker/FrameCryptor.d.ts +42 -1
- package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
- package/dist/src/index.d.ts +3 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/logger.d.ts +2 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/options.d.ts +22 -0
- package/dist/src/options.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +2 -1
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/PCTransportManager.d.ts +12 -0
- package/dist/src/room/PCTransportManager.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +7 -2
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +8 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/data-stream/incoming/StreamReader.d.ts +17 -17
- package/dist/src/room/data-stream/incoming/StreamReader.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +1 -1
- package/dist/src/room/statsSummary.d.ts +13 -0
- package/dist/src/room/statsSummary.d.ts.map +1 -0
- package/dist/src/room/token-source/utils.d.ts.map +1 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts +10 -0
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/utils/machineInspector.d.ts +54 -0
- package/dist/src/utils/machineInspector.d.ts.map +1 -0
- package/dist/ts4.2/api/SignalClient.d.ts +25 -1
- package/dist/ts4.2/api/SignalClientStateMachine.d.ts +85 -0
- package/dist/ts4.2/e2ee/E2eeManager.d.ts +7 -0
- package/dist/ts4.2/e2ee/constants.d.ts +5 -0
- package/dist/ts4.2/e2ee/types.d.ts +9 -2
- package/dist/ts4.2/e2ee/worker/FrameCryptor.d.ts +42 -1
- package/dist/ts4.2/index.d.ts +3 -2
- package/dist/ts4.2/logger.d.ts +2 -1
- package/dist/ts4.2/options.d.ts +22 -0
- package/dist/ts4.2/room/PCTransport.d.ts +2 -1
- package/dist/ts4.2/room/PCTransportManager.d.ts +12 -0
- package/dist/ts4.2/room/RTCEngine.d.ts +7 -2
- package/dist/ts4.2/room/Room.d.ts +8 -1
- package/dist/ts4.2/room/data-stream/incoming/StreamReader.d.ts +17 -17
- package/dist/ts4.2/room/events.d.ts +1 -1
- package/dist/ts4.2/room/statsSummary.d.ts +13 -0
- package/dist/ts4.2/room/track/Track.d.ts +10 -0
- package/dist/ts4.2/utils/machineInspector.d.ts +54 -0
- package/package.json +7 -1
- package/src/api/SignalClient.test.ts +320 -8
- package/src/api/SignalClient.ts +261 -82
- package/src/api/SignalClientStateMachine.test.ts +472 -0
- package/src/api/SignalClientStateMachine.ts +180 -0
- package/src/api/WebSocketStream.ts +19 -3
- package/src/api/utils.test.ts +20 -1
- package/src/api/utils.ts +5 -0
- package/src/e2ee/E2eeManager.ts +38 -14
- package/src/e2ee/constants.ts +6 -0
- package/src/e2ee/subscriberBlackScreen.test.ts +544 -0
- package/src/e2ee/types.ts +9 -2
- package/src/e2ee/worker/FrameCryptor.race.test.ts +9 -26
- package/src/e2ee/worker/FrameCryptor.test.ts +0 -1
- package/src/e2ee/worker/FrameCryptor.ts +185 -52
- package/src/e2ee/worker/e2ee.worker.ts +38 -6
- package/src/index.ts +5 -0
- package/src/logger.ts +1 -0
- package/src/options.ts +24 -0
- package/src/room/PCTransport.ts +2 -1
- package/src/room/PCTransportManager.test.ts +35 -0
- package/src/room/PCTransportManager.ts +12 -4
- package/src/room/RTCEngine.ts +16 -7
- package/src/room/Room.ts +70 -5
- package/src/room/data-stream/incoming/IncomingDataStreamManager.test.ts +171 -0
- package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +17 -18
- package/src/room/data-stream/incoming/StreamReader.ts +20 -50
- package/src/room/events.ts +1 -1
- package/src/room/statsSummary.ts +187 -0
- package/src/room/token-source/test-tokens.ts +20 -0
- package/src/room/token-source/utils.test.ts +27 -0
- package/src/room/token-source/utils.ts +12 -5
- package/src/room/track/LocalAudioTrack.ts +9 -3
- package/src/room/track/Track.ts +27 -0
- package/src/room/utils.test.ts +24 -1
- package/src/room/utils.ts +1 -1
- package/src/utils/machineInspector.ts +90 -0
|
@@ -946,6 +946,9 @@ function _defineProperty(e, r, t) {
|
|
|
946
946
|
writable: true
|
|
947
947
|
}) : e[r] = t, e;
|
|
948
948
|
}
|
|
949
|
+
function _iterableToArray(r) {
|
|
950
|
+
if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r);
|
|
951
|
+
}
|
|
949
952
|
function _iterableToArrayLimit(r, l) {
|
|
950
953
|
var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
|
|
951
954
|
if (null != t) {
|
|
@@ -976,9 +979,53 @@ function _iterableToArrayLimit(r, l) {
|
|
|
976
979
|
function _nonIterableRest() {
|
|
977
980
|
throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
|
|
978
981
|
}
|
|
982
|
+
function ownKeys(e, r) {
|
|
983
|
+
var t = Object.keys(e);
|
|
984
|
+
if (Object.getOwnPropertySymbols) {
|
|
985
|
+
var o = Object.getOwnPropertySymbols(e);
|
|
986
|
+
r && (o = o.filter(function (r) {
|
|
987
|
+
return Object.getOwnPropertyDescriptor(e, r).enumerable;
|
|
988
|
+
})), t.push.apply(t, o);
|
|
989
|
+
}
|
|
990
|
+
return t;
|
|
991
|
+
}
|
|
992
|
+
function _objectSpread2(e) {
|
|
993
|
+
for (var r = 1; r < arguments.length; r++) {
|
|
994
|
+
var t = null != arguments[r] ? arguments[r] : {};
|
|
995
|
+
r % 2 ? ownKeys(Object(t), true).forEach(function (r) {
|
|
996
|
+
_defineProperty(e, r, t[r]);
|
|
997
|
+
}) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) {
|
|
998
|
+
Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r));
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
return e;
|
|
1002
|
+
}
|
|
1003
|
+
function _objectWithoutProperties(e, t) {
|
|
1004
|
+
if (null == e) return {};
|
|
1005
|
+
var o,
|
|
1006
|
+
r,
|
|
1007
|
+
i = _objectWithoutPropertiesLoose(e, t);
|
|
1008
|
+
if (Object.getOwnPropertySymbols) {
|
|
1009
|
+
var n = Object.getOwnPropertySymbols(e);
|
|
1010
|
+
for (r = 0; r < n.length; r++) o = n[r], -1 === t.indexOf(o) && {}.propertyIsEnumerable.call(e, o) && (i[o] = e[o]);
|
|
1011
|
+
}
|
|
1012
|
+
return i;
|
|
1013
|
+
}
|
|
1014
|
+
function _objectWithoutPropertiesLoose(r, e) {
|
|
1015
|
+
if (null == r) return {};
|
|
1016
|
+
var t = {};
|
|
1017
|
+
for (var n in r) if ({}.hasOwnProperty.call(r, n)) {
|
|
1018
|
+
if (-1 !== e.indexOf(n)) continue;
|
|
1019
|
+
t[n] = r[n];
|
|
1020
|
+
}
|
|
1021
|
+
return t;
|
|
1022
|
+
}
|
|
979
1023
|
function _slicedToArray(r, e) {
|
|
980
1024
|
return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest();
|
|
981
1025
|
}
|
|
1026
|
+
function _toArray(r) {
|
|
1027
|
+
return _arrayWithHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableRest();
|
|
1028
|
+
}
|
|
982
1029
|
function _toPrimitive(t, r) {
|
|
983
1030
|
if ("object" != typeof t || !t) return t;
|
|
984
1031
|
var e = t[Symbol.toPrimitive];
|
|
@@ -8505,6 +8552,7 @@ var LoggerNames;
|
|
|
8505
8552
|
LoggerNames["DataTracks"] = "livekit-data-tracks";
|
|
8506
8553
|
LoggerNames["Region"] = "livekit-region";
|
|
8507
8554
|
LoggerNames["ICE"] = "livekit-ice";
|
|
8555
|
+
LoggerNames["Stats"] = "livekit-stats";
|
|
8508
8556
|
})(LoggerNames || (LoggerNames = {}));
|
|
8509
8557
|
let livekitLogger = loglevelExports.getLogger(LoggerNames.Default);
|
|
8510
8558
|
const livekitLoggers = Object.values(LoggerNames).map(name => loglevelExports.getLogger(name));
|
|
@@ -12160,7 +12208,7 @@ function getMatch(exp, ua) {
|
|
|
12160
12208
|
}
|
|
12161
12209
|
function getOSVersion(ua) {
|
|
12162
12210
|
return ua.includes('mac os') ? getMatch(/\(.+?(\d+_\d+(:?_\d+)?)/, ua, 1).replace(/_/g, '.') : undefined;
|
|
12163
|
-
}var version$1 = "2.22.
|
|
12211
|
+
}var version$1 = "2.22.2";const version = version$1;
|
|
12164
12212
|
const protocolVersion = 17;
|
|
12165
12213
|
/** Initial client protocol. */
|
|
12166
12214
|
const CLIENT_PROTOCOL_DEFAULT = 0;
|
|
@@ -12446,7 +12494,7 @@ var RoomEvent;
|
|
|
12446
12494
|
* When a [[RemoteParticipant]] leaves *after* the local
|
|
12447
12495
|
* participant has joined.
|
|
12448
12496
|
*
|
|
12449
|
-
* args: ([[RemoteParticipant]])
|
|
12497
|
+
* args: ([[RemoteParticipant]], [[DisconnectReason]] | undefined)
|
|
12450
12498
|
*/
|
|
12451
12499
|
RoomEvent["ParticipantDisconnected"] = "participantDisconnected";
|
|
12452
12500
|
/**
|
|
@@ -13014,7 +13062,171 @@ var TrackEvent;
|
|
|
13014
13062
|
* @internal
|
|
13015
13063
|
*/
|
|
13016
13064
|
TrackEvent["PreConnectBufferFlushed"] = "preConnectBufferFlushed";
|
|
13017
|
-
})(TrackEvent || (TrackEvent = {}));function
|
|
13065
|
+
})(TrackEvent || (TrackEvent = {}));function compact(summary) {
|
|
13066
|
+
const compacted = {};
|
|
13067
|
+
for (const _ref of Object.entries(summary)) {
|
|
13068
|
+
var _ref2 = _slicedToArray(_ref, 2);
|
|
13069
|
+
const key = _ref2[0];
|
|
13070
|
+
const value = _ref2[1];
|
|
13071
|
+
if (value !== undefined) {
|
|
13072
|
+
compacted[key] = value;
|
|
13073
|
+
}
|
|
13074
|
+
}
|
|
13075
|
+
return compacted;
|
|
13076
|
+
}
|
|
13077
|
+
function resolution(width, height) {
|
|
13078
|
+
return width && height ? "".concat(width, "x").concat(height) : undefined;
|
|
13079
|
+
}
|
|
13080
|
+
/** keeps the derived durations readable; reported values are logged as they are */
|
|
13081
|
+
function round(seconds) {
|
|
13082
|
+
return Math.round(seconds * 10000) / 10000;
|
|
13083
|
+
}
|
|
13084
|
+
/**
|
|
13085
|
+
* Average time media spent in the jitter buffer, in s. Both counters are
|
|
13086
|
+
* cumulative, so this is the average over the lifetime of the stream.
|
|
13087
|
+
*/
|
|
13088
|
+
function jitterBuffer(stat) {
|
|
13089
|
+
const delay = stat.jitterBufferDelay;
|
|
13090
|
+
const emitted = stat.jitterBufferEmittedCount;
|
|
13091
|
+
return delay !== undefined && emitted ? round(delay / emitted) : undefined;
|
|
13092
|
+
}
|
|
13093
|
+
/**
|
|
13094
|
+
* Playout delay in s. Video receive reports it directly, the audio path keeps it
|
|
13095
|
+
* in the linked `media-playout` stats summed over the samples played out. Pairs
|
|
13096
|
+
* with `RemoteTrack.setPlayoutDelay`.
|
|
13097
|
+
*/
|
|
13098
|
+
function playoutDelay(stat, playout) {
|
|
13099
|
+
if (stat.playoutDelay !== undefined) {
|
|
13100
|
+
return round(stat.playoutDelay);
|
|
13101
|
+
}
|
|
13102
|
+
const total = playout === null || playout === void 0 ? void 0 : playout.totalPlayoutDelay;
|
|
13103
|
+
const samples = playout === null || playout === void 0 ? void 0 : playout.totalSamplesCount;
|
|
13104
|
+
return total !== undefined && samples ? round(total / samples) : undefined;
|
|
13105
|
+
}
|
|
13106
|
+
/**
|
|
13107
|
+
* Picks the interesting fields out of a `getStats()` report and groups them by
|
|
13108
|
+
* RTP stream, so a stats dump can be read without unfolding the raw report.
|
|
13109
|
+
*/
|
|
13110
|
+
function summarizeStatsReport(report) {
|
|
13111
|
+
var _a, _b;
|
|
13112
|
+
const byId = new Map();
|
|
13113
|
+
const candidatePairs = [];
|
|
13114
|
+
const inbound = [];
|
|
13115
|
+
const outbound = [];
|
|
13116
|
+
let transport;
|
|
13117
|
+
report.forEach(stat => byId.set(stat.id, stat));
|
|
13118
|
+
const codecOf = stat => {
|
|
13119
|
+
var _a;
|
|
13120
|
+
return stat.codecId ? (_a = byId.get(stat.codecId)) === null || _a === void 0 ? void 0 : _a.mimeType : undefined;
|
|
13121
|
+
};
|
|
13122
|
+
const relatedOf = (stat, key) => stat[key] ? byId.get(stat[key]) : undefined;
|
|
13123
|
+
report.forEach(stat => {
|
|
13124
|
+
switch (stat.type) {
|
|
13125
|
+
case 'inbound-rtp':
|
|
13126
|
+
{
|
|
13127
|
+
const playout = relatedOf(stat, 'playoutId');
|
|
13128
|
+
inbound.push(compact({
|
|
13129
|
+
kind: stat.kind,
|
|
13130
|
+
ssrc: stat.ssrc,
|
|
13131
|
+
mid: stat.mid,
|
|
13132
|
+
// matches `streamTrackID` in the track's own log context
|
|
13133
|
+
trackId: stat.trackIdentifier,
|
|
13134
|
+
codec: codecOf(stat),
|
|
13135
|
+
decoder: stat.decoderImplementation,
|
|
13136
|
+
resolution: resolution(stat.frameWidth, stat.frameHeight),
|
|
13137
|
+
fps: stat.framesPerSecond,
|
|
13138
|
+
bytesReceived: stat.bytesReceived,
|
|
13139
|
+
packetsReceived: stat.packetsReceived,
|
|
13140
|
+
packetsLost: stat.packetsLost,
|
|
13141
|
+
packetsDiscarded: stat.packetsDiscarded,
|
|
13142
|
+
// frames received without frames decoded is a decode failure
|
|
13143
|
+
framesReceived: stat.framesReceived,
|
|
13144
|
+
framesDecoded: stat.framesDecoded,
|
|
13145
|
+
framesDropped: stat.framesDropped,
|
|
13146
|
+
keyFramesDecoded: stat.keyFramesDecoded,
|
|
13147
|
+
freezeCount: stat.freezeCount,
|
|
13148
|
+
totalFreezesDuration: stat.totalFreezesDuration,
|
|
13149
|
+
pauseCount: stat.pauseCount,
|
|
13150
|
+
nackCount: stat.nackCount,
|
|
13151
|
+
pliCount: stat.pliCount,
|
|
13152
|
+
firCount: stat.firCount,
|
|
13153
|
+
jitter: stat.jitter,
|
|
13154
|
+
jitterBuffer: jitterBuffer(stat),
|
|
13155
|
+
playoutDelay: playoutDelay(stat, playout),
|
|
13156
|
+
audioLevel: stat.audioLevel,
|
|
13157
|
+
totalSamplesReceived: stat.totalSamplesReceived,
|
|
13158
|
+
concealedSamples: stat.concealedSamples
|
|
13159
|
+
}));
|
|
13160
|
+
break;
|
|
13161
|
+
}
|
|
13162
|
+
case 'outbound-rtp':
|
|
13163
|
+
{
|
|
13164
|
+
const remote = relatedOf(stat, 'remoteId');
|
|
13165
|
+
const source = relatedOf(stat, 'mediaSourceId');
|
|
13166
|
+
outbound.push(compact({
|
|
13167
|
+
kind: stat.kind,
|
|
13168
|
+
ssrc: stat.ssrc,
|
|
13169
|
+
mid: stat.mid,
|
|
13170
|
+
rid: stat.rid,
|
|
13171
|
+
trackId: source === null || source === void 0 ? void 0 : source.trackIdentifier,
|
|
13172
|
+
active: stat.active,
|
|
13173
|
+
codec: codecOf(stat),
|
|
13174
|
+
encoder: stat.encoderImplementation,
|
|
13175
|
+
resolution: resolution(stat.frameWidth, stat.frameHeight),
|
|
13176
|
+
fps: stat.framesPerSecond,
|
|
13177
|
+
// what the source produces, to tell a stalled capture from a stalled encoder
|
|
13178
|
+
captureResolution: resolution(source === null || source === void 0 ? void 0 : source.width, source === null || source === void 0 ? void 0 : source.height),
|
|
13179
|
+
captureFps: source === null || source === void 0 ? void 0 : source.framesPerSecond,
|
|
13180
|
+
audioLevel: source === null || source === void 0 ? void 0 : source.audioLevel,
|
|
13181
|
+
targetBitrate: stat.targetBitrate,
|
|
13182
|
+
bytesSent: stat.bytesSent,
|
|
13183
|
+
packetsSent: stat.packetsSent,
|
|
13184
|
+
retransmittedPacketsSent: stat.retransmittedPacketsSent,
|
|
13185
|
+
framesEncoded: stat.framesEncoded,
|
|
13186
|
+
keyFramesEncoded: stat.keyFramesEncoded,
|
|
13187
|
+
limitedBy: stat.qualityLimitationReason === 'none' ? undefined : stat.qualityLimitationReason,
|
|
13188
|
+
nackCount: stat.nackCount,
|
|
13189
|
+
pliCount: stat.pliCount,
|
|
13190
|
+
firCount: stat.firCount,
|
|
13191
|
+
// loss, jitter and RTT are only known from what the remote reports
|
|
13192
|
+
remotePacketsLost: remote === null || remote === void 0 ? void 0 : remote.packetsLost,
|
|
13193
|
+
remoteFractionLost: remote === null || remote === void 0 ? void 0 : remote.fractionLost,
|
|
13194
|
+
remoteJitter: remote === null || remote === void 0 ? void 0 : remote.jitter,
|
|
13195
|
+
remoteRoundTripTime: remote === null || remote === void 0 ? void 0 : remote.roundTripTime
|
|
13196
|
+
}));
|
|
13197
|
+
break;
|
|
13198
|
+
}
|
|
13199
|
+
case 'transport':
|
|
13200
|
+
transport = stat;
|
|
13201
|
+
break;
|
|
13202
|
+
case 'candidate-pair':
|
|
13203
|
+
candidatePairs.push(stat);
|
|
13204
|
+
break;
|
|
13205
|
+
}
|
|
13206
|
+
});
|
|
13207
|
+
const selectedPairId = transport === null || transport === void 0 ? void 0 : transport.selectedCandidatePairId;
|
|
13208
|
+
const pair = (_b = (_a = selectedPairId ? byId.get(selectedPairId) : undefined) !== null && _a !== void 0 ? _a : candidatePairs.find(candidate => candidate.selected)) !== null && _b !== void 0 ? _b : candidatePairs.find(candidate => candidate.nominated);
|
|
13209
|
+
const local = (pair === null || pair === void 0 ? void 0 : pair.localCandidateId) ? byId.get(pair.localCandidateId) : undefined;
|
|
13210
|
+
const remote = (pair === null || pair === void 0 ? void 0 : pair.remoteCandidateId) ? byId.get(pair.remoteCandidateId) : undefined;
|
|
13211
|
+
const connection = compact({
|
|
13212
|
+
ice: transport === null || transport === void 0 ? void 0 : transport.iceState,
|
|
13213
|
+
dtls: transport === null || transport === void 0 ? void 0 : transport.dtlsState,
|
|
13214
|
+
route: local && remote ? "".concat(local.candidateType, "/").concat(local.protocol, " -> ").concat(remote.candidateType) : undefined,
|
|
13215
|
+
network: local === null || local === void 0 ? void 0 : local.networkType,
|
|
13216
|
+
currentRoundTripTime: pair === null || pair === void 0 ? void 0 : pair.currentRoundTripTime,
|
|
13217
|
+
// the send bandwidth estimate; no RTP stream reports it
|
|
13218
|
+
availableOutgoingBitrate: pair === null || pair === void 0 ? void 0 : pair.availableOutgoingBitrate,
|
|
13219
|
+
availableIncomingBitrate: pair === null || pair === void 0 ? void 0 : pair.availableIncomingBitrate,
|
|
13220
|
+
bytesSent: pair === null || pair === void 0 ? void 0 : pair.bytesSent,
|
|
13221
|
+
bytesReceived: pair === null || pair === void 0 ? void 0 : pair.bytesReceived,
|
|
13222
|
+
candidatePairChanges: transport === null || transport === void 0 ? void 0 : transport.selectedCandidatePairChanges
|
|
13223
|
+
});
|
|
13224
|
+
return {
|
|
13225
|
+
connection: Object.keys(connection).length > 0 ? connection : undefined,
|
|
13226
|
+
outbound: outbound.length > 0 ? outbound : undefined,
|
|
13227
|
+
inbound: inbound.length > 0 ? inbound : undefined
|
|
13228
|
+
};
|
|
13229
|
+
}function cloneDeep$1(value) {
|
|
13018
13230
|
if (typeof value === 'undefined') {
|
|
13019
13231
|
return value;
|
|
13020
13232
|
}
|
|
@@ -13439,7 +13651,7 @@ function extractProcessorsFromOptions(options) {
|
|
|
13439
13651
|
return {
|
|
13440
13652
|
audioProcessor,
|
|
13441
13653
|
videoProcessor,
|
|
13442
|
-
optionsWithoutProcessor: cloneDeep(newOptions)
|
|
13654
|
+
optionsWithoutProcessor: cloneDeep$1(newOptions)
|
|
13443
13655
|
};
|
|
13444
13656
|
}
|
|
13445
13657
|
function getTrackSourceFromProto(source) {
|
|
@@ -13496,6 +13708,7 @@ class Track extends eventsExports.EventEmitter {
|
|
|
13496
13708
|
this._streamState = Track.StreamState.Active;
|
|
13497
13709
|
this.isInBackground = false;
|
|
13498
13710
|
this._currentBitrate = 0;
|
|
13711
|
+
this.finalStatsLogged = false;
|
|
13499
13712
|
this.log = livekitLogger;
|
|
13500
13713
|
this.appVisibilityChangedListener = () => {
|
|
13501
13714
|
if (this.backgroundTimeout) {
|
|
@@ -13644,6 +13857,24 @@ class Track extends eventsExports.EventEmitter {
|
|
|
13644
13857
|
cancelAnimationFrame(this.timeSyncHandle);
|
|
13645
13858
|
this.timeSyncHandle = undefined;
|
|
13646
13859
|
}
|
|
13860
|
+
this.logFinalStats();
|
|
13861
|
+
}
|
|
13862
|
+
/**
|
|
13863
|
+
* Dumps the raw stats of the track as it ends, once: a track that stops
|
|
13864
|
+
* between two of the room's stats dumps is gone by the time the next one runs.
|
|
13865
|
+
*/
|
|
13866
|
+
logFinalStats() {
|
|
13867
|
+
if (this.finalStatsLogged) {
|
|
13868
|
+
return;
|
|
13869
|
+
}
|
|
13870
|
+
this.finalStatsLogged = true;
|
|
13871
|
+
this.getRTCStatsReport().then(report => {
|
|
13872
|
+
if (report) {
|
|
13873
|
+
this.log.info('final track stats', summarizeStatsReport(report));
|
|
13874
|
+
}
|
|
13875
|
+
}).catch(error => this.log.debug('could not collect final track stats', {
|
|
13876
|
+
error
|
|
13877
|
+
}));
|
|
13647
13878
|
}
|
|
13648
13879
|
/** @internal */
|
|
13649
13880
|
updateLoggerOptions(loggerOptions) {
|
|
@@ -13869,7 +14100,7 @@ function supportsAddTrack() {
|
|
|
13869
14100
|
return 'addTrack' in RTCPeerConnection.prototype;
|
|
13870
14101
|
}
|
|
13871
14102
|
function supportsAdaptiveStream() {
|
|
13872
|
-
return typeof ResizeObserver !== undefined && typeof IntersectionObserver !== undefined;
|
|
14103
|
+
return typeof ResizeObserver !== 'undefined' && typeof IntersectionObserver !== 'undefined';
|
|
13873
14104
|
}
|
|
13874
14105
|
function supportsDynacast() {
|
|
13875
14106
|
return supportsTransceiver();
|
|
@@ -13927,6 +14158,22 @@ function supportsVP9() {
|
|
|
13927
14158
|
}
|
|
13928
14159
|
return hasVP9;
|
|
13929
14160
|
}
|
|
14161
|
+
function supportsH265() {
|
|
14162
|
+
if (!('getCapabilities' in RTCRtpSender)) {
|
|
14163
|
+
return false;
|
|
14164
|
+
}
|
|
14165
|
+
const capabilities = RTCRtpSender.getCapabilities('video');
|
|
14166
|
+
let hasH265 = false;
|
|
14167
|
+
if (capabilities) {
|
|
14168
|
+
for (const codec of capabilities.codecs) {
|
|
14169
|
+
if (codec.mimeType.toLowerCase() === 'video/h265') {
|
|
14170
|
+
hasH265 = true;
|
|
14171
|
+
break;
|
|
14172
|
+
}
|
|
14173
|
+
}
|
|
14174
|
+
}
|
|
14175
|
+
return hasH265;
|
|
14176
|
+
}
|
|
13930
14177
|
function isSVCCodec(codec) {
|
|
13931
14178
|
return codec === 'av1' || codec === 'vp9';
|
|
13932
14179
|
}
|
|
@@ -14560,6 +14807,11 @@ function parseSignalResponse(value) {
|
|
|
14560
14807
|
}
|
|
14561
14808
|
function getAbortReasonAsString(signal) {
|
|
14562
14809
|
let defaultMessage = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'Unknown reason';
|
|
14810
|
+
// the connect timeout hands this an Error rather than a signal, and its message is the only
|
|
14811
|
+
// description of what went wrong — without this a timeout reports itself as a generic abort
|
|
14812
|
+
if (signal instanceof Error) {
|
|
14813
|
+
return signal.message;
|
|
14814
|
+
}
|
|
14563
14815
|
if (!(signal instanceof AbortSignal)) {
|
|
14564
14816
|
return defaultMessage;
|
|
14565
14817
|
}
|
|
@@ -14577,6 +14829,11 @@ function getAbortReasonAsString(signal) {
|
|
|
14577
14829
|
const DECRYPTION_FAILURE_TOLERANCE = 10;
|
|
14578
14830
|
// flag set to indicate that e2ee has been setup for sender/receiver;
|
|
14579
14831
|
const E2EE_FLAG = 'lk_e2ee';
|
|
14832
|
+
/**
|
|
14833
|
+
* trackId a receiver's/sender's encoded stream pipeline was last set up for.
|
|
14834
|
+
* Needed to recover the right cryptor when a transceiver gets reused.
|
|
14835
|
+
*/
|
|
14836
|
+
const E2EE_TRACK_ID = 'lk_e2ee_track_id';
|
|
14580
14837
|
const SALT = 'LKFrameEncryptionKey';
|
|
14581
14838
|
const KEY_PROVIDER_DEFAULTS = {
|
|
14582
14839
|
sharedKey: false,
|
|
@@ -15673,12 +15930,12 @@ class E2EEManager extends eventsExports.EventEmitter {
|
|
|
15673
15930
|
});
|
|
15674
15931
|
}
|
|
15675
15932
|
setupEventListeners(room, keyProvider) {
|
|
15676
|
-
room.on(RoomEvent.TrackPublished, (pub, participant) => this.
|
|
15933
|
+
room.on(RoomEvent.TrackPublished, (pub, participant) => this.setParticipantCryptorEnabledForPublication(pub, participant.identity));
|
|
15677
15934
|
room.on(RoomEvent.ConnectionStateChanged, state => {
|
|
15678
15935
|
if (state === ConnectionState.Connected) {
|
|
15679
15936
|
room.remoteParticipants.forEach(participant => {
|
|
15680
15937
|
participant.trackPublications.forEach(pub => {
|
|
15681
|
-
this.
|
|
15938
|
+
this.setParticipantCryptorEnabledForPublication(pub, participant.identity);
|
|
15682
15939
|
});
|
|
15683
15940
|
});
|
|
15684
15941
|
}
|
|
@@ -15848,6 +16105,22 @@ class E2EEManager extends eventsExports.EventEmitter {
|
|
|
15848
16105
|
};
|
|
15849
16106
|
this.worker.postMessage(msg);
|
|
15850
16107
|
}
|
|
16108
|
+
/**
|
|
16109
|
+
* Derive the participant's encryption state from one of its publications.
|
|
16110
|
+
* Publications without trackInfo are skipped rather than thrown on: throwing
|
|
16111
|
+
* inside the forEach above would leave every remaining participant without an
|
|
16112
|
+
* encryption state, and a cryptor with unknown state can no longer decrypt.
|
|
16113
|
+
*/
|
|
16114
|
+
setParticipantCryptorEnabledForPublication(pub, participantIdentity) {
|
|
16115
|
+
if (!pub.trackInfo) {
|
|
16116
|
+
livekitLogger.warn('skipping e2ee enabled update for publication without trackInfo', {
|
|
16117
|
+
participant: participantIdentity,
|
|
16118
|
+
trackSid: pub.trackSid
|
|
16119
|
+
});
|
|
16120
|
+
return;
|
|
16121
|
+
}
|
|
16122
|
+
this.setParticipantCryptorEnabled(pub.trackInfo.encryption !== Encryption_Type.NONE, participantIdentity);
|
|
16123
|
+
}
|
|
15851
16124
|
setupE2EEReceiver(track, remoteId, trackInfo) {
|
|
15852
16125
|
if (!track.receiver) {
|
|
15853
16126
|
return;
|
|
@@ -15884,21 +16157,27 @@ class E2EEManager extends eventsExports.EventEmitter {
|
|
|
15884
16157
|
codec,
|
|
15885
16158
|
hasPacketTrailer
|
|
15886
16159
|
};
|
|
15887
|
-
// @ts-ignore
|
|
15888
16160
|
receiver.transform = new RTCRtpScriptTransform(this.worker, options);
|
|
15889
16161
|
} else {
|
|
15890
|
-
if (E2EE_FLAG in receiver &&
|
|
15891
|
-
//
|
|
16162
|
+
if (E2EE_FLAG in receiver && E2EE_TRACK_ID in receiver) {
|
|
16163
|
+
// The transceiver is being reused for a new track. We cannot set up a new
|
|
16164
|
+
// pipeline from here: this receiver's encoded streams were transferred to
|
|
16165
|
+
// the worker on first setup and a transferred stream can't be transferred
|
|
16166
|
+
// again, while createEncodedStreams() may only be called once per receiver.
|
|
16167
|
+
// So hand the worker the new track/participant and let it re-point (and if
|
|
16168
|
+
// necessary rebuild) the cryptor that still owns those streams.
|
|
15892
16169
|
const msg = {
|
|
15893
16170
|
kind: 'updateCodec',
|
|
15894
16171
|
data: {
|
|
15895
16172
|
trackId,
|
|
16173
|
+
previousTrackId: receiver[E2EE_TRACK_ID],
|
|
15896
16174
|
codec,
|
|
15897
16175
|
participantIdentity,
|
|
15898
16176
|
hasPacketTrailer
|
|
15899
16177
|
}
|
|
15900
16178
|
};
|
|
15901
16179
|
this.worker.postMessage(msg);
|
|
16180
|
+
receiver[E2EE_TRACK_ID] = trackId;
|
|
15902
16181
|
return;
|
|
15903
16182
|
}
|
|
15904
16183
|
// @ts-ignore
|
|
@@ -15923,7 +16202,6 @@ class E2EEManager extends eventsExports.EventEmitter {
|
|
|
15923
16202
|
trackId: trackId,
|
|
15924
16203
|
codec,
|
|
15925
16204
|
participantIdentity: participantIdentity,
|
|
15926
|
-
isReuse: E2EE_FLAG in receiver,
|
|
15927
16205
|
hasPacketTrailer
|
|
15928
16206
|
}
|
|
15929
16207
|
};
|
|
@@ -15931,6 +16209,8 @@ class E2EEManager extends eventsExports.EventEmitter {
|
|
|
15931
16209
|
}
|
|
15932
16210
|
// @ts-ignore
|
|
15933
16211
|
receiver[E2EE_FLAG] = true;
|
|
16212
|
+
// @ts-ignore
|
|
16213
|
+
receiver[E2EE_TRACK_ID] = trackId;
|
|
15934
16214
|
});
|
|
15935
16215
|
}
|
|
15936
16216
|
/**
|
|
@@ -15970,7 +16250,6 @@ class E2EEManager extends eventsExports.EventEmitter {
|
|
|
15970
16250
|
codec,
|
|
15971
16251
|
trackId,
|
|
15972
16252
|
participantIdentity: this.room.localParticipant.identity,
|
|
15973
|
-
isReuse: false,
|
|
15974
16253
|
hasPacketTrailer: hasFrameMetadataPublishOptions(frameMetadata),
|
|
15975
16254
|
packetTrailer: frameMetadata
|
|
15976
16255
|
}
|
|
@@ -16620,6 +16899,1140 @@ class AsyncQueue {
|
|
|
16620
16899
|
snapshot() {
|
|
16621
16900
|
return Array.from(this.pendingTasks.values());
|
|
16622
16901
|
}
|
|
16902
|
+
}/**
|
|
16903
|
+
* Opt-in registry that lets a development tool observe the state machines driving the connection
|
|
16904
|
+
* layer.
|
|
16905
|
+
*
|
|
16906
|
+
* The machines are private to the objects that own them and, more importantly, do not outlive them:
|
|
16907
|
+
* a full reconnect replaces the whole engine along with its `SignalClient`, so anything watching a
|
|
16908
|
+
* single machine reference goes blind exactly when the interesting part starts. Machines therefore
|
|
16909
|
+
* announce themselves here as they are constructed, and a subscriber sees the whole succession.
|
|
16910
|
+
*
|
|
16911
|
+
* Nothing is recorded until {@link enableMachineInspector} is called, which no shipping code does —
|
|
16912
|
+
* with the registry off, announcing is a comparison and a return.
|
|
16913
|
+
*/
|
|
16914
|
+
/** Kept small: enough for a panel opened mid-session to see how the connection got where it is. */
|
|
16915
|
+
/** Announces a machine to whatever is watching. A no-op unless the inspector was enabled. */
|
|
16916
|
+
function announceMachine(label, machine) {
|
|
16917
|
+
{
|
|
16918
|
+
return;
|
|
16919
|
+
}
|
|
16920
|
+
}const _excluded = ["client"];
|
|
16921
|
+
//#region src/emitter.ts
|
|
16922
|
+
/**
|
|
16923
|
+
* Minimal typed event emitter used internally by Fsm and BehavioralFsm.
|
|
16924
|
+
*
|
|
16925
|
+
* Two listener categories exist at runtime: named listeners (keyed by event
|
|
16926
|
+
* name) and wildcard listeners (keyed as `"*"`). The wildcard fires on EVERY
|
|
16927
|
+
* emit, regardless of event name, and its callback receives both the event
|
|
16928
|
+
* name and the data. Named listeners receive only the data.
|
|
16929
|
+
*
|
|
16930
|
+
* @typeParam TEventMap - Record mapping event names to their payload types.
|
|
16931
|
+
* Constrains `on()` and `emit()` to matching event/payload pairs.
|
|
16932
|
+
*/
|
|
16933
|
+
var Emitter = class Emitter {
|
|
16934
|
+
constructor() {
|
|
16935
|
+
_defineProperty(this, "listeners", /* @__PURE__ */new Map());
|
|
16936
|
+
}
|
|
16937
|
+
on(event, cb) {
|
|
16938
|
+
let set = this.listeners.get(event);
|
|
16939
|
+
if (!set) {
|
|
16940
|
+
set = /* @__PURE__ */new Set();
|
|
16941
|
+
this.listeners.set(event, set);
|
|
16942
|
+
}
|
|
16943
|
+
set.add(cb);
|
|
16944
|
+
return {
|
|
16945
|
+
off: () => {
|
|
16946
|
+
set.delete(cb);
|
|
16947
|
+
}
|
|
16948
|
+
};
|
|
16949
|
+
}
|
|
16950
|
+
/**
|
|
16951
|
+
* Emit a named event, notifying wildcard listeners first, then named listeners.
|
|
16952
|
+
*
|
|
16953
|
+
* Wildcard listeners receive `(eventName, data)`; named listeners receive
|
|
16954
|
+
* only `data`. The firing order (wildcards before named) is intentional —
|
|
16955
|
+
* it lets relay listeners (like Fsm's bfsm proxy) observe all events
|
|
16956
|
+
* before specific subscribers react.
|
|
16957
|
+
*/
|
|
16958
|
+
emit(event, data) {
|
|
16959
|
+
const wildcards = this.listeners.get("*");
|
|
16960
|
+
if (wildcards) for (const cb of wildcards) cb(event, data);
|
|
16961
|
+
const named = this.listeners.get(event);
|
|
16962
|
+
if (named) for (const cb of named) cb(data);
|
|
16963
|
+
}
|
|
16964
|
+
/**
|
|
16965
|
+
* Remove all listeners (named and wildcard).
|
|
16966
|
+
*
|
|
16967
|
+
* Existing `Subscription` objects remain valid — their `off()` closures
|
|
16968
|
+
* hold a reference to the now-empty Set, so calling them is harmless.
|
|
16969
|
+
* Called by `dispose()` to prevent memory retention after an FSM shuts down.
|
|
16970
|
+
*/
|
|
16971
|
+
clear() {
|
|
16972
|
+
this.listeners.clear();
|
|
16973
|
+
}
|
|
16974
|
+
};
|
|
16975
|
+
|
|
16976
|
+
//#endregion
|
|
16977
|
+
//#region src/json-safe.ts
|
|
16978
|
+
/**
|
|
16979
|
+
* Thrown internally when the walk hits a value that can't survive a
|
|
16980
|
+
* serialization boundary. Callers catch this to attach FSM/input context
|
|
16981
|
+
* before re-throwing a fully descriptive error — see `path`/`label` below.
|
|
16982
|
+
*/
|
|
16983
|
+
var NonSerializableValueError = class extends Error {
|
|
16984
|
+
constructor(path, label) {
|
|
16985
|
+
super("non-serializable value at ".concat(path, " (").concat(label, ")"));
|
|
16986
|
+
this.path = path;
|
|
16987
|
+
this.label = label;
|
|
16988
|
+
}
|
|
16989
|
+
};
|
|
16990
|
+
/**
|
|
16991
|
+
* Deep-clones `value`, throwing `NonSerializableValueError` the moment it
|
|
16992
|
+
* finds anything that isn't `null`, a boolean, a finite number, a string, a
|
|
16993
|
+
* plain array, or a plain object. `rootPath` seeds the path used in error
|
|
16994
|
+
* messages — e.g. `"args"` so a nested failure reads as `args[1].onComplete`.
|
|
16995
|
+
*
|
|
16996
|
+
* Cloning (rather than just validating) is what keeps a returned snapshot
|
|
16997
|
+
* from aliasing live FSM state: mutating the snapshot afterward — or the
|
|
16998
|
+
* original value passed into a deferred `handle()` call — can't reach back
|
|
16999
|
+
* into the FSM's internal deferred queue.
|
|
17000
|
+
*/
|
|
17001
|
+
const cloneJsonSafe = (value, rootPath) => {
|
|
17002
|
+
return cloneNode(value, rootPath, /* @__PURE__ */new Set());
|
|
17003
|
+
};
|
|
17004
|
+
const cloneNode = (value, path, ancestors) => {
|
|
17005
|
+
if (value === null) return null;
|
|
17006
|
+
switch (typeof value) {
|
|
17007
|
+
case "string":
|
|
17008
|
+
case "boolean":
|
|
17009
|
+
return value;
|
|
17010
|
+
case "number":
|
|
17011
|
+
if (!Number.isFinite(value)) throw new NonSerializableValueError(path, describeNonFiniteNumber(value));
|
|
17012
|
+
return value;
|
|
17013
|
+
case "object":
|
|
17014
|
+
return cloneObject(value, path, ancestors);
|
|
17015
|
+
default:
|
|
17016
|
+
throw new NonSerializableValueError(path, typeof value);
|
|
17017
|
+
}
|
|
17018
|
+
};
|
|
17019
|
+
const cloneObject = (obj, path, ancestors) => {
|
|
17020
|
+
var _obj$constructor$name, _obj$constructor;
|
|
17021
|
+
if (ancestors.has(obj)) throw new NonSerializableValueError(path, "circular reference");
|
|
17022
|
+
if (Array.isArray(obj)) {
|
|
17023
|
+
if (obj.length !== Object.keys(obj).length) throw new NonSerializableValueError(path, "sparse array or array with non-index properties");
|
|
17024
|
+
ancestors.add(obj);
|
|
17025
|
+
const cloned = obj.map((item, i) => cloneNode(item, "".concat(path, "[").concat(i, "]"), ancestors));
|
|
17026
|
+
ancestors.delete(obj);
|
|
17027
|
+
return cloned;
|
|
17028
|
+
}
|
|
17029
|
+
const proto = Object.getPrototypeOf(obj);
|
|
17030
|
+
if (proto !== Object.prototype && proto !== null) throw new NonSerializableValueError(path, (_obj$constructor$name = (_obj$constructor = obj.constructor) === null || _obj$constructor === void 0 ? void 0 : _obj$constructor.name) !== null && _obj$constructor$name !== void 0 ? _obj$constructor$name : "object");
|
|
17031
|
+
ancestors.add(obj);
|
|
17032
|
+
const cloned = {};
|
|
17033
|
+
for (const key of Object.keys(obj)) {
|
|
17034
|
+
const value = cloneNode(obj[key], "".concat(path, ".").concat(key), ancestors);
|
|
17035
|
+
Object.defineProperty(cloned, key, {
|
|
17036
|
+
value,
|
|
17037
|
+
enumerable: true,
|
|
17038
|
+
writable: true,
|
|
17039
|
+
configurable: true
|
|
17040
|
+
});
|
|
17041
|
+
}
|
|
17042
|
+
ancestors.delete(obj);
|
|
17043
|
+
return cloned;
|
|
17044
|
+
};
|
|
17045
|
+
const describeNonFiniteNumber = n => {
|
|
17046
|
+
if (Number.isNaN(n)) return "NaN";
|
|
17047
|
+
return n > 0 ? "Infinity" : "-Infinity";
|
|
17048
|
+
};
|
|
17049
|
+
/**
|
|
17050
|
+
* Deep-clones `value` WITHOUT validating it — unlike `cloneJsonSafe`, this
|
|
17051
|
+
* never throws. Plain objects/arrays are cloned recursively (so a caller
|
|
17052
|
+
* can't alias state back into whatever holds the result); anything else
|
|
17053
|
+
* (functions, `Date`/`Map`/class instances, symbols, non-finite numbers,
|
|
17054
|
+
* etc.) is passed through by reference as-is.
|
|
17055
|
+
*
|
|
17056
|
+
* This is `rehydrate()`'s side of the aliasing guarantee: `dehydrate()`
|
|
17057
|
+
* validates-and-clones on the way OUT (via `cloneJsonSafe`), but `rehydrate()`
|
|
17058
|
+
* trusts the snapshot it's given is already valid data — re-validating on
|
|
17059
|
+
* the way IN would be a redundant, unwanted asymmetry (see the build plan's
|
|
17060
|
+
* disclosed known gaps). We still need the clone so mutating the caller's
|
|
17061
|
+
* snapshot object after `rehydrate()` returns can't reach into the live
|
|
17062
|
+
* FSM's internal deferred queue.
|
|
17063
|
+
*
|
|
17064
|
+
* Cycle-safe: a value that contains itself is returned as-is (by reference)
|
|
17065
|
+
* rather than cloned infinitely — there's no validation step here to make
|
|
17066
|
+
* that throw, so silently keeping the shared reference is the only option
|
|
17067
|
+
* that doesn't hang.
|
|
17068
|
+
*/
|
|
17069
|
+
const cloneDeep = function (value) {
|
|
17070
|
+
let ancestors = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : /* @__PURE__ */new Set();
|
|
17071
|
+
if (value === null || typeof value !== "object") return value;
|
|
17072
|
+
if (ancestors.has(value)) return value;
|
|
17073
|
+
if (Array.isArray(value)) {
|
|
17074
|
+
ancestors.add(value);
|
|
17075
|
+
const cloned = value.map(item => cloneDeep(item, ancestors));
|
|
17076
|
+
ancestors.delete(value);
|
|
17077
|
+
return cloned;
|
|
17078
|
+
}
|
|
17079
|
+
const proto = Object.getPrototypeOf(value);
|
|
17080
|
+
if (proto !== Object.prototype && proto !== null) return value;
|
|
17081
|
+
ancestors.add(value);
|
|
17082
|
+
const cloned = {};
|
|
17083
|
+
for (const key of Object.keys(value)) {
|
|
17084
|
+
const clonedValue = cloneDeep(value[key], ancestors);
|
|
17085
|
+
Object.defineProperty(cloned, key, {
|
|
17086
|
+
value: clonedValue,
|
|
17087
|
+
enumerable: true,
|
|
17088
|
+
writable: true,
|
|
17089
|
+
configurable: true
|
|
17090
|
+
});
|
|
17091
|
+
}
|
|
17092
|
+
ancestors.delete(value);
|
|
17093
|
+
return cloned;
|
|
17094
|
+
};
|
|
17095
|
+
|
|
17096
|
+
//#endregion
|
|
17097
|
+
//#region src/types.ts
|
|
17098
|
+
/**
|
|
17099
|
+
* Symbol used as a property key to identify machina FSM instances at runtime.
|
|
17100
|
+
* Each class stamps itself with a MachinaType value so the ChildLink adapter
|
|
17101
|
+
* can dispatch handle()/canHandle()/reset() correctly without circular imports.
|
|
17102
|
+
*/
|
|
17103
|
+
const MACHINA_TYPE = Symbol("machina.type");
|
|
17104
|
+
|
|
17105
|
+
//#endregion
|
|
17106
|
+
//#region src/behavioral-fsm.ts
|
|
17107
|
+
const MAX_TRANSITION_DEPTH = 20;
|
|
17108
|
+
/**
|
|
17109
|
+
* Defines FSM behavior (states + transitions) while tracking per-client state
|
|
17110
|
+
* in a `WeakMap`. A single `BehavioralFsm` instance can drive any number of
|
|
17111
|
+
* independent client objects simultaneously — each gets its own state,
|
|
17112
|
+
* deferred queue, and lifecycle.
|
|
17113
|
+
*
|
|
17114
|
+
* Prefer `createBehavioralFsm()` over constructing this directly — the factory
|
|
17115
|
+
* infers all generic parameters from the config object.
|
|
17116
|
+
*
|
|
17117
|
+
* All public methods silently no-op after `dispose()` is called.
|
|
17118
|
+
*
|
|
17119
|
+
* @typeParam TClient - The client object type. Must be an object (non-primitive)
|
|
17120
|
+
* so it can serve as a WeakMap key.
|
|
17121
|
+
* @typeParam TStateNames - String literal union of valid state names.
|
|
17122
|
+
* @typeParam TInputNames - String literal union of valid input names.
|
|
17123
|
+
* @typeParam TBubbles - String literal union of inputs this FSM declares via
|
|
17124
|
+
* `bubbles`. Type-only — carried so `BubblesOfInstance` can extract it from
|
|
17125
|
+
* a constructed instance; nothing at runtime reads this generic.
|
|
17126
|
+
*/
|
|
17127
|
+
var BehavioralFsm = class BehavioralFsm {
|
|
17128
|
+
constructor(config) {
|
|
17129
|
+
_defineProperty(this, "id", void 0);
|
|
17130
|
+
_defineProperty(this, "initialState", void 0);
|
|
17131
|
+
_defineProperty(this, MACHINA_TYPE, "BehavioralFsm");
|
|
17132
|
+
_defineProperty(this, "states", void 0);
|
|
17133
|
+
_defineProperty(this, "emitter", new Emitter());
|
|
17134
|
+
_defineProperty(this, "clients", /* @__PURE__ */new WeakMap());
|
|
17135
|
+
_defineProperty(this, "knownClients", /* @__PURE__ */new Set());
|
|
17136
|
+
_defineProperty(this, "childSubscriptions", []);
|
|
17137
|
+
_defineProperty(this, "disposed", false);
|
|
17138
|
+
_defineProperty(this, "transitionDepth", 0);
|
|
17139
|
+
this.id = config.id;
|
|
17140
|
+
this.initialState = config.initialState;
|
|
17141
|
+
this.states = config.states;
|
|
17142
|
+
this.wrapChildLinks();
|
|
17143
|
+
this.setupChildSubscriptions();
|
|
17144
|
+
}
|
|
17145
|
+
/**
|
|
17146
|
+
* Dispatch an input to the given client's current state handler.
|
|
17147
|
+
*
|
|
17148
|
+
* Delegation order: if the current state has a `_child` FSM that can
|
|
17149
|
+
* handle the input, it is dispatched there. If the child emits `nohandler`,
|
|
17150
|
+
* the input bubbles up to this FSM's local handler. If no handler exists
|
|
17151
|
+
* here either, `nohandler` is emitted on this FSM's emitter.
|
|
17152
|
+
*
|
|
17153
|
+
* No-ops silently when disposed.
|
|
17154
|
+
*/
|
|
17155
|
+
handle(client, inputName) {
|
|
17156
|
+
var _this$states$meta$sta;
|
|
17157
|
+
if (this.disposed) return;
|
|
17158
|
+
const meta = this.getOrCreateClientMeta(client);
|
|
17159
|
+
for (var _len = arguments.length, args = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {
|
|
17160
|
+
args[_key - 2] = arguments[_key];
|
|
17161
|
+
}
|
|
17162
|
+
meta.currentActionArgs = args;
|
|
17163
|
+
const childLink = (_this$states$meta$sta = this.states[meta.state]) === null || _this$states$meta$sta === void 0 ? void 0 : _this$states$meta$sta._child;
|
|
17164
|
+
if (childLink) {
|
|
17165
|
+
if (childLink.canHandle(client, inputName)) {
|
|
17166
|
+
try {
|
|
17167
|
+
childLink.handle(client, inputName, ...args);
|
|
17168
|
+
} finally {
|
|
17169
|
+
meta.currentActionArgs = void 0;
|
|
17170
|
+
}
|
|
17171
|
+
return;
|
|
17172
|
+
}
|
|
17173
|
+
}
|
|
17174
|
+
this.handleLocally(client, inputName, args, meta);
|
|
17175
|
+
}
|
|
17176
|
+
/**
|
|
17177
|
+
* Returns true if the client's current state has a handler for `inputName`
|
|
17178
|
+
* (or a catch-all `"*"` handler), or if the current state's `_child` chain
|
|
17179
|
+
* can handle it — the check recurses to the same depth `handle()`'s
|
|
17180
|
+
* delegation can actually reach, so a grandchild-only input answers true
|
|
17181
|
+
* from the root. Does NOT initialize the client — no `_onEnter`, no
|
|
17182
|
+
* events, no side effects. Unseen clients are treated as if they were
|
|
17183
|
+
* already in `initialState`. Returns false when disposed.
|
|
17184
|
+
*/
|
|
17185
|
+
canHandle(client, inputName) {
|
|
17186
|
+
var _this$clients$get$sta, _this$clients$get, _stateObj$inputName;
|
|
17187
|
+
if (this.disposed) return false;
|
|
17188
|
+
const state = (_this$clients$get$sta = (_this$clients$get = this.clients.get(client)) === null || _this$clients$get === void 0 ? void 0 : _this$clients$get.state) !== null && _this$clients$get$sta !== void 0 ? _this$clients$get$sta : this.initialState;
|
|
17189
|
+
const stateObj = this.states[state];
|
|
17190
|
+
if ((_stateObj$inputName = stateObj === null || stateObj === void 0 ? void 0 : stateObj[inputName]) !== null && _stateObj$inputName !== void 0 ? _stateObj$inputName : stateObj === null || stateObj === void 0 ? void 0 : stateObj["*"]) return true;
|
|
17191
|
+
const childLink = stateObj === null || stateObj === void 0 ? void 0 : stateObj._child;
|
|
17192
|
+
return childLink ? childLink.canHandle(client, inputName) : false;
|
|
17193
|
+
}
|
|
17194
|
+
/**
|
|
17195
|
+
* Transition the client back to `initialState`, firing `_onEnter` and
|
|
17196
|
+
* lifecycle events as if entering it fresh. No-ops when disposed.
|
|
17197
|
+
*/
|
|
17198
|
+
reset(client) {
|
|
17199
|
+
if (this.disposed) return;
|
|
17200
|
+
this.transition(client, this.initialState);
|
|
17201
|
+
}
|
|
17202
|
+
/**
|
|
17203
|
+
* Returns the client's current state, or `undefined` if the client has
|
|
17204
|
+
* never been initialized (i.e. `handle()`, `transition()`, or `reset()`
|
|
17205
|
+
* have never been called for it). Does NOT trigger initialization.
|
|
17206
|
+
*/
|
|
17207
|
+
currentState(client) {
|
|
17208
|
+
var _this$clients$get2;
|
|
17209
|
+
return (_this$clients$get2 = this.clients.get(client)) === null || _this$clients$get2 === void 0 ? void 0 : _this$clients$get2.state;
|
|
17210
|
+
}
|
|
17211
|
+
/**
|
|
17212
|
+
* Directly transition `client` to `toState`, running the full lifecycle:
|
|
17213
|
+
* `_onExit` for the current state → `transitioning` event → update state →
|
|
17214
|
+
* `_onEnter` for new state → `transitioned` event → child reset → deferred
|
|
17215
|
+
* queue replay → bounce (if `_onEnter` returned a state name).
|
|
17216
|
+
*
|
|
17217
|
+
* Same-state transitions are silently ignored. Transitions to unknown state
|
|
17218
|
+
* names emit `invalidstate` instead of throwing. Throws if the transition
|
|
17219
|
+
* depth exceeds `MAX_TRANSITION_DEPTH` (likely an `_onEnter` → transition loop).
|
|
17220
|
+
*
|
|
17221
|
+
* No-ops when disposed.
|
|
17222
|
+
*/
|
|
17223
|
+
transition(client, toState) {
|
|
17224
|
+
if (this.disposed) return;
|
|
17225
|
+
const meta = this.getOrCreateClientMeta(client);
|
|
17226
|
+
const fromState = meta.state;
|
|
17227
|
+
if (toState === fromState) return;
|
|
17228
|
+
if (!Object.hasOwn(this.states, toState)) {
|
|
17229
|
+
this.emitter.emit("invalidstate", {
|
|
17230
|
+
stateName: toState,
|
|
17231
|
+
client
|
|
17232
|
+
});
|
|
17233
|
+
return;
|
|
17234
|
+
}
|
|
17235
|
+
this.transitionDepth++;
|
|
17236
|
+
if (this.transitionDepth > MAX_TRANSITION_DEPTH) {
|
|
17237
|
+
this.transitionDepth = 0;
|
|
17238
|
+
throw new Error("Max transition depth (".concat(MAX_TRANSITION_DEPTH, ") exceeded in FSM \"").concat(this.id, "\". Likely an infinite _onEnter \u2192 transition loop."));
|
|
17239
|
+
}
|
|
17240
|
+
try {
|
|
17241
|
+
const curStateObj = this.states[fromState];
|
|
17242
|
+
const newStateObj = this.states[toState];
|
|
17243
|
+
if (curStateObj !== null && curStateObj !== void 0 && curStateObj._onExit && typeof curStateObj._onExit === "function") {
|
|
17244
|
+
const exitArgs = this.buildHandlerArgs(client, "", meta);
|
|
17245
|
+
curStateObj._onExit(exitArgs);
|
|
17246
|
+
}
|
|
17247
|
+
meta.state = toState;
|
|
17248
|
+
const payload = {
|
|
17249
|
+
fromState,
|
|
17250
|
+
toState,
|
|
17251
|
+
client
|
|
17252
|
+
};
|
|
17253
|
+
this.emitter.emit("transitioning", payload);
|
|
17254
|
+
let bounceTarget = void 0;
|
|
17255
|
+
if (newStateObj !== null && newStateObj !== void 0 && newStateObj._onEnter && typeof newStateObj._onEnter === "function") {
|
|
17256
|
+
const enterArgs = this.buildHandlerArgs(client, "", meta);
|
|
17257
|
+
bounceTarget = newStateObj._onEnter(enterArgs);
|
|
17258
|
+
}
|
|
17259
|
+
this.emitter.emit("transitioned", payload);
|
|
17260
|
+
const childLink = newStateObj === null || newStateObj === void 0 ? void 0 : newStateObj._child;
|
|
17261
|
+
if (childLink) childLink.reset(client);
|
|
17262
|
+
this.processQueue(client, meta);
|
|
17263
|
+
if (typeof bounceTarget === "string" && meta.state === toState) this.transition(client, bounceTarget);
|
|
17264
|
+
} finally {
|
|
17265
|
+
this.transitionDepth--;
|
|
17266
|
+
}
|
|
17267
|
+
}
|
|
17268
|
+
/**
|
|
17269
|
+
* Returns the client's state as a dot-delimited path including any active
|
|
17270
|
+
* child FSM states (e.g. `"active.connecting.retrying"`). Returns just the
|
|
17271
|
+
* current state name when no child is active. Returns `""` for clients that
|
|
17272
|
+
* have never been initialized (unlike `currentState()` which returns `undefined`).
|
|
17273
|
+
*/
|
|
17274
|
+
compositeState(client) {
|
|
17275
|
+
var _this$states$meta$sta2;
|
|
17276
|
+
const meta = this.clients.get(client);
|
|
17277
|
+
if (!meta) return "";
|
|
17278
|
+
const childLink = (_this$states$meta$sta2 = this.states[meta.state]) === null || _this$states$meta$sta2 === void 0 ? void 0 : _this$states$meta$sta2._child;
|
|
17279
|
+
if (childLink) {
|
|
17280
|
+
const childComposite = childLink.compositeState(client);
|
|
17281
|
+
if (childComposite) return "".concat(meta.state, ".").concat(childComposite);
|
|
17282
|
+
}
|
|
17283
|
+
return meta.state;
|
|
17284
|
+
}
|
|
17285
|
+
rehydrate(client, input) {
|
|
17286
|
+
if (this.disposed) return;
|
|
17287
|
+
if (typeof input === "string") {
|
|
17288
|
+
this.rehydrateCompositePath(client, input);
|
|
17289
|
+
return;
|
|
17290
|
+
}
|
|
17291
|
+
const writes = this.planSnapshotWrites(client, input);
|
|
17292
|
+
for (const write of writes) write();
|
|
17293
|
+
}
|
|
17294
|
+
/**
|
|
17295
|
+
* Snapshot everything machina tracks for `client`: current state, pending
|
|
17296
|
+
* deferred inputs, and — recursively — the same for every `_child` that has
|
|
17297
|
+
* ever seen this client, active or not. Feed the result to the object form
|
|
17298
|
+
* of `rehydrate()` to restore it later, deferrals included.
|
|
17299
|
+
*
|
|
17300
|
+
* Returns `undefined` for a client this FSM has never seen (mirrors
|
|
17301
|
+
* `currentState()`) — the call does NOT trigger initialization.
|
|
17302
|
+
*
|
|
17303
|
+
* Throws if any deferred input's args contain a non-serializable value
|
|
17304
|
+
* (function, undefined, symbol, bigint, non-finite number, Date/Map/class
|
|
17305
|
+
* instance, or a circular reference) — naming the input, its `until` target
|
|
17306
|
+
* if any, the FSM id, and the exact value path. Throws for an Fsm child
|
|
17307
|
+
* that's on `client`'s active path *relative to the true root* (consistent
|
|
17308
|
+
* with `rehydrate()`'s conditional throw) — an Fsm owns its own context, so
|
|
17309
|
+
* there's nothing per-client to snapshot. An Fsm child declared at a state
|
|
17310
|
+
* `client` never visited, OR nested under a `BehavioralFsm` child that is
|
|
17311
|
+
* itself off-path from the root, is skipped rather than throwing — neither
|
|
17312
|
+
* has any per-client state to lose, so one Fsm child anywhere in the
|
|
17313
|
+
* hierarchy doesn't disable `dehydrate()` for clients that never reach that
|
|
17314
|
+
* branch, no matter how deeply nested the Fsm child is.
|
|
17315
|
+
*
|
|
17316
|
+
* Meant for clients at rest between `handle()` calls — `currentActionArgs`
|
|
17317
|
+
* (the in-flight args mid-handler) has no meaning here and is excluded.
|
|
17318
|
+
*
|
|
17319
|
+
* @param isOnActivePath - @internal Whether this FSM itself is currently
|
|
17320
|
+
* reachable from the true root's active path. Defaults to `true` for the
|
|
17321
|
+
* public entry point (this FSM IS the root from its own perspective); the
|
|
17322
|
+
* `ChildLink` adapter passes `false` down when recursing into a nested
|
|
17323
|
+
* `BehavioralFsm` child that is itself off-path, so that child's own
|
|
17324
|
+
* Fsm-child checks don't recompute reachability from its dormant local
|
|
17325
|
+
* state alone.
|
|
17326
|
+
*/
|
|
17327
|
+
dehydrate(client) {
|
|
17328
|
+
let isOnActivePath = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
|
|
17329
|
+
const meta = this.clients.get(client);
|
|
17330
|
+
if (!meta) return;
|
|
17331
|
+
const snapshot = {
|
|
17332
|
+
state: meta.state,
|
|
17333
|
+
deferred: meta.deferredQueue.map(item => this.snapshotDeferredInput(item))
|
|
17334
|
+
};
|
|
17335
|
+
const children = this.collectChildSnapshots(client, meta.state, isOnActivePath);
|
|
17336
|
+
if (children) snapshot.children = children;
|
|
17337
|
+
return snapshot;
|
|
17338
|
+
}
|
|
17339
|
+
/**
|
|
17340
|
+
* @internal
|
|
17341
|
+
* Validates `snapshot` against this level's state graph and recurses into
|
|
17342
|
+
* every declared child, returning write thunks to run only once the WHOLE
|
|
17343
|
+
* tree — every level — validates successfully. Nothing is written here.
|
|
17344
|
+
* Called by the object-form of `rehydrate()` and, recursively, by ChildLink
|
|
17345
|
+
* so a nested BehavioralFsm participates in the same validate-then-write
|
|
17346
|
+
* pass. Not part of the public persistence API — call `rehydrate()` instead.
|
|
17347
|
+
*/
|
|
17348
|
+
planSnapshotWrites(client, snapshot) {
|
|
17349
|
+
if (this.disposed) return [];
|
|
17350
|
+
const state = snapshot.state,
|
|
17351
|
+
deferred = snapshot.deferred,
|
|
17352
|
+
children = snapshot.children;
|
|
17353
|
+
if (!Object.hasOwn(this.states, state)) throw new Error("rehydrate: unknown state \"".concat(state, "\" in FSM \"").concat(this.id, "\". Valid states: ").concat(Object.keys(this.states).join(", ")));
|
|
17354
|
+
const writes = [];
|
|
17355
|
+
if (children) for (const stateName of Object.keys(children)) {
|
|
17356
|
+
var _this$states$stateNam;
|
|
17357
|
+
if (!Object.hasOwn(this.states, stateName)) throw new Error("rehydrate: unknown state \"".concat(stateName, "\" in FSM \"").concat(this.id, "\" referenced by snapshot.children."));
|
|
17358
|
+
const childLink = (_this$states$stateNam = this.states[stateName]) === null || _this$states$stateNam === void 0 ? void 0 : _this$states$stateNam._child;
|
|
17359
|
+
if (!childLink) throw new Error("rehydrate: state \"".concat(stateName, "\" in FSM \"").concat(this.id, "\" has no _child, but the snapshot has a children[\"").concat(stateName, "\"] entry."));
|
|
17360
|
+
writes.push(...childLink.planRehydrate(client, children[stateName]));
|
|
17361
|
+
}
|
|
17362
|
+
const deferredQueue = deferred.map(item => _objectSpread2({
|
|
17363
|
+
inputName: item.inputName,
|
|
17364
|
+
args: cloneDeep(item.args)
|
|
17365
|
+
}, item.untilState !== void 0 ? {
|
|
17366
|
+
untilState: item.untilState
|
|
17367
|
+
} : {}));
|
|
17368
|
+
writes.push(() => {
|
|
17369
|
+
if (!this.clients.has(client)) this.knownClients.add(new WeakRef(client));
|
|
17370
|
+
this.clients.set(client, {
|
|
17371
|
+
state,
|
|
17372
|
+
deferredQueue
|
|
17373
|
+
});
|
|
17374
|
+
});
|
|
17375
|
+
return writes;
|
|
17376
|
+
}
|
|
17377
|
+
rehydrateCompositePath(client, compositeState) {
|
|
17378
|
+
const _compositeState$split = compositeState.split("."),
|
|
17379
|
+
_compositeState$split2 = _toArray(_compositeState$split),
|
|
17380
|
+
state = _compositeState$split2[0],
|
|
17381
|
+
rest = _arrayLikeToArray(_compositeState$split2).slice(1);
|
|
17382
|
+
if (!Object.hasOwn(this.states, state)) throw new Error("rehydrate: unknown state \"".concat(state, "\" in FSM \"").concat(this.id, "\". Valid states: ").concat(Object.keys(this.states).join(", ")));
|
|
17383
|
+
if (rest.length > 0) {
|
|
17384
|
+
var _this$states$state;
|
|
17385
|
+
const childPath = rest.join(".");
|
|
17386
|
+
const childLink = (_this$states$state = this.states[state]) === null || _this$states$state === void 0 ? void 0 : _this$states$state._child;
|
|
17387
|
+
if (!childLink) throw new Error("rehydrate: state \"".concat(state, "\" in FSM \"").concat(this.id, "\" has no _child, but composite path \"").concat(compositeState, "\" requires one."));
|
|
17388
|
+
childLink.rehydrate(client, childPath);
|
|
17389
|
+
}
|
|
17390
|
+
if (!this.clients.has(client)) this.knownClients.add(new WeakRef(client));
|
|
17391
|
+
this.clients.set(client, {
|
|
17392
|
+
state,
|
|
17393
|
+
deferredQueue: []
|
|
17394
|
+
});
|
|
17395
|
+
}
|
|
17396
|
+
/**
|
|
17397
|
+
* Walks every declared state's `_child`, dehydrating each one that has ever
|
|
17398
|
+
* seen `client`. The same child instance can be declared under multiple
|
|
17399
|
+
* state names (shared child) — it's dehydrated once and the result is
|
|
17400
|
+
* reused under every declaring state name, matching how the engine already
|
|
17401
|
+
* treats shared children elsewhere (dispose, event subscriptions).
|
|
17402
|
+
*
|
|
17403
|
+
* Declaring names are grouped by `childLink.instance` (the actual FSM
|
|
17404
|
+
* instance, not the `ChildLink` wrapper — `wrapChildLinks()` mints a fresh
|
|
17405
|
+
* wrapper per declaring state, so grouping by wrapper would never hit for a
|
|
17406
|
+
* shared child) BEFORE any `onPath` is computed or any child is dehydrated.
|
|
17407
|
+
* A shared child's `onPath` is the OR of `stateName === activeState` across
|
|
17408
|
+
* every declaring name in its group: at most one declaring name can ever
|
|
17409
|
+
* equal `activeState`, so this combined flag is correct and, critically,
|
|
17410
|
+
* independent of `Object.keys(this.states)` iteration order — computing
|
|
17411
|
+
* `onPath` per-declaring-name and caching whichever one happened to run
|
|
17412
|
+
* first would silently launder an on-path client's Fsm-child throw through
|
|
17413
|
+
* an unrelated off-path declaring name.
|
|
17414
|
+
*
|
|
17415
|
+
* An off-path Fsm child (declared only at states other than `activeState`,
|
|
17416
|
+
* OR nested anywhere under a `BehavioralFsm` child that is itself off-path
|
|
17417
|
+
* relative to the true root) is skipped entirely rather than dehydrated:
|
|
17418
|
+
* Fsm state isn't tracked per-client to begin with, so an off-path Fsm
|
|
17419
|
+
* child has nothing to lose by being skipped — unlike a BehavioralFsm
|
|
17420
|
+
* child's off-path meta, which is real per-client data. `isOnActivePath`
|
|
17421
|
+
* is the inherited "am I even reachable from the root" flag; combining it
|
|
17422
|
+
* with the group's `stateNames.includes(activeState)` check (rather than
|
|
17423
|
+
* using that check alone) is what keeps a nested Fsm grandchild from
|
|
17424
|
+
* throwing when its immediate BehavioralFsm parent is itself off-path —
|
|
17425
|
+
* the parent's own dormant `activeState` is irrelevant once the parent
|
|
17426
|
+
* isn't reachable. This keeps one Fsm child anywhere in the hierarchy from
|
|
17427
|
+
* disabling `dehydrate()` for every client, only for clients actually on
|
|
17428
|
+
* that branch.
|
|
17429
|
+
*/
|
|
17430
|
+
collectChildSnapshots(client, activeState, isOnActivePath) {
|
|
17431
|
+
const declaringNamesByInstance = /* @__PURE__ */new Map();
|
|
17432
|
+
for (const stateName of Object.keys(this.states)) {
|
|
17433
|
+
var _this$states$stateNam2;
|
|
17434
|
+
const childLink = (_this$states$stateNam2 = this.states[stateName]) === null || _this$states$stateNam2 === void 0 ? void 0 : _this$states$stateNam2._child;
|
|
17435
|
+
if (!childLink) continue;
|
|
17436
|
+
const entry = declaringNamesByInstance.get(childLink.instance);
|
|
17437
|
+
if (entry) entry.stateNames.push(stateName);else declaringNamesByInstance.set(childLink.instance, {
|
|
17438
|
+
childLink,
|
|
17439
|
+
stateNames: [stateName]
|
|
17440
|
+
});
|
|
17441
|
+
}
|
|
17442
|
+
let children;
|
|
17443
|
+
for (const _ref of declaringNamesByInstance.values()) {
|
|
17444
|
+
const childLink = _ref.childLink;
|
|
17445
|
+
const stateNames = _ref.stateNames;
|
|
17446
|
+
const onPath = isOnActivePath && stateNames.includes(activeState);
|
|
17447
|
+
if (childLink.instance[MACHINA_TYPE] === "Fsm" && !onPath) continue;
|
|
17448
|
+
const childSnapshot = childLink.dehydrate(client, onPath);
|
|
17449
|
+
if (childSnapshot) {
|
|
17450
|
+
children !== null && children !== void 0 ? children : children = {};
|
|
17451
|
+
for (const stateName of stateNames) children[stateName] = childSnapshot;
|
|
17452
|
+
}
|
|
17453
|
+
}
|
|
17454
|
+
return children;
|
|
17455
|
+
}
|
|
17456
|
+
snapshotDeferredInput(item) {
|
|
17457
|
+
let clonedArgs;
|
|
17458
|
+
try {
|
|
17459
|
+
clonedArgs = cloneJsonSafe(item.args, "args");
|
|
17460
|
+
} catch (err) {
|
|
17461
|
+
if (!(err instanceof NonSerializableValueError)) throw err;
|
|
17462
|
+
const untilPart = item.untilState ? " (until \"".concat(item.untilState, "\")") : "";
|
|
17463
|
+
throw new Error("dehydrate: deferred input \"".concat(item.inputName, "\"").concat(untilPart, " in FSM \"").concat(this.id, "\" has a non-serializable value at ").concat(err.path, " (").concat(err.label, ")"));
|
|
17464
|
+
}
|
|
17465
|
+
const snapshot = {
|
|
17466
|
+
inputName: item.inputName,
|
|
17467
|
+
args: clonedArgs
|
|
17468
|
+
};
|
|
17469
|
+
if (item.untilState !== void 0) snapshot.untilState = item.untilState;
|
|
17470
|
+
return snapshot;
|
|
17471
|
+
}
|
|
17472
|
+
on(eventName, callback) {
|
|
17473
|
+
if (this.disposed) return {
|
|
17474
|
+
off() {}
|
|
17475
|
+
};
|
|
17476
|
+
return this.emitter.on(eventName, callback);
|
|
17477
|
+
}
|
|
17478
|
+
/**
|
|
17479
|
+
* Emit a custom event through the FSM. Built-in lifecycle events are
|
|
17480
|
+
* emitted automatically — this is for user-defined events from handlers.
|
|
17481
|
+
* No-ops when disposed.
|
|
17482
|
+
*/
|
|
17483
|
+
emit(eventName, data) {
|
|
17484
|
+
if (this.disposed) return;
|
|
17485
|
+
this.emitter.emit(eventName, data);
|
|
17486
|
+
}
|
|
17487
|
+
/**
|
|
17488
|
+
* Permanently shut down this FSM. Irreversible — all subsequent method
|
|
17489
|
+
* calls become silent no-ops. Tears down child subscriptions, clears all
|
|
17490
|
+
* listeners, and cascades disposal to child FSMs (unless `preserveChildren`
|
|
17491
|
+
* is set). The same child appearing in multiple states is disposed once.
|
|
17492
|
+
*/
|
|
17493
|
+
dispose(options) {
|
|
17494
|
+
this.disposed = true;
|
|
17495
|
+
for (const sub of this.childSubscriptions) sub.off();
|
|
17496
|
+
if (!(options !== null && options !== void 0 && options.preserveChildren)) {
|
|
17497
|
+
const seen = /* @__PURE__ */new Set();
|
|
17498
|
+
for (const stateName of Object.keys(this.states)) {
|
|
17499
|
+
var _this$states$stateNam3;
|
|
17500
|
+
const childLink = (_this$states$stateNam3 = this.states[stateName]) === null || _this$states$stateNam3 === void 0 ? void 0 : _this$states$stateNam3._child;
|
|
17501
|
+
if (childLink && !seen.has(childLink.instance)) {
|
|
17502
|
+
seen.add(childLink.instance);
|
|
17503
|
+
childLink.dispose();
|
|
17504
|
+
}
|
|
17505
|
+
}
|
|
17506
|
+
}
|
|
17507
|
+
this.emitter.clear();
|
|
17508
|
+
}
|
|
17509
|
+
/**
|
|
17510
|
+
* Walks all states at construction time, detects raw FSM instances assigned
|
|
17511
|
+
* to _child, and wraps them into ChildLink adapters via createChildLink().
|
|
17512
|
+
* Must run BEFORE setupChildSubscriptions() so the subscriptions see
|
|
17513
|
+
* ChildLink objects, not raw FSM instances.
|
|
17514
|
+
*/
|
|
17515
|
+
wrapChildLinks() {
|
|
17516
|
+
for (const stateName of Object.keys(this.states)) {
|
|
17517
|
+
const stateObj = this.states[stateName];
|
|
17518
|
+
const rawChild = stateObj === null || stateObj === void 0 ? void 0 : stateObj._child;
|
|
17519
|
+
if (!rawChild) continue;
|
|
17520
|
+
if (typeof rawChild !== "object") throw new Error("State \"".concat(stateName, "\"._child: expected an Fsm or BehavioralFsm instance, got ").concat(String(rawChild)));
|
|
17521
|
+
if (!(MACHINA_TYPE in rawChild)) throw new Error("State \"".concat(stateName, "\"._child: expected an Fsm or BehavioralFsm instance, got a plain object"));
|
|
17522
|
+
stateObj._child = createChildLink(rawChild);
|
|
17523
|
+
}
|
|
17524
|
+
}
|
|
17525
|
+
/**
|
|
17526
|
+
* Walks all states at construction time, finds states with _child, and
|
|
17527
|
+
* subscribes once to each unique child's wildcard events. Subscriptions are
|
|
17528
|
+
* stored for cleanup in dispose(). We deduplicate by `childLink.instance`
|
|
17529
|
+
* (the underlying Fsm/BehavioralFsm instance), not the `ChildLink` wrapper —
|
|
17530
|
+
* `wrapChildLinks()` mints a fresh wrapper per declaring state, so a child
|
|
17531
|
+
* shared across states would otherwise get one subscription PER declaring
|
|
17532
|
+
* state, each independently walking known clients and relaying events. That
|
|
17533
|
+
* silently double-fires client-less relays (Fsm-child events, or a
|
|
17534
|
+
* BehavioralFsm child's custom `emit()` with no `client` in the payload)
|
|
17535
|
+
* whenever two different clients are active on two different declaring
|
|
17536
|
+
* names at once — the same wrapper-vs-instance identity bug
|
|
17537
|
+
* `collectChildSnapshots()` had before its #184 fix.
|
|
17538
|
+
*/
|
|
17539
|
+
setupChildSubscriptions() {
|
|
17540
|
+
const seenInstances = /* @__PURE__ */new Set();
|
|
17541
|
+
for (const stateName of Object.keys(this.states)) {
|
|
17542
|
+
var _this$states$stateNam4;
|
|
17543
|
+
const childLink = (_this$states$stateNam4 = this.states[stateName]) === null || _this$states$stateNam4 === void 0 ? void 0 : _this$states$stateNam4._child;
|
|
17544
|
+
if (!childLink || seenInstances.has(childLink.instance)) continue;
|
|
17545
|
+
seenInstances.add(childLink.instance);
|
|
17546
|
+
const sub = childLink.onAny((eventName, data) => {
|
|
17547
|
+
if (eventName === "nohandler") {
|
|
17548
|
+
var _payload$args;
|
|
17549
|
+
const payload = data;
|
|
17550
|
+
if (payload.client !== void 0) this.bubbleNohandler(payload.client, childLink, payload.inputName, (_payload$args = payload.args) !== null && _payload$args !== void 0 ? _payload$args : []);else for (const ref of this.knownClients) {
|
|
17551
|
+
var _payload$args2;
|
|
17552
|
+
const client = ref.deref();
|
|
17553
|
+
if (client === void 0) {
|
|
17554
|
+
this.knownClients.delete(ref);
|
|
17555
|
+
continue;
|
|
17556
|
+
}
|
|
17557
|
+
this.bubbleNohandler(client, childLink, payload.inputName, (_payload$args2 = payload.args) !== null && _payload$args2 !== void 0 ? _payload$args2 : []);
|
|
17558
|
+
}
|
|
17559
|
+
return;
|
|
17560
|
+
}
|
|
17561
|
+
const payload = data;
|
|
17562
|
+
if (payload && typeof payload === "object" && "client" in payload) {
|
|
17563
|
+
if (this.isChildActiveForClient(payload.client, childLink)) this.emitter.emit(eventName, data);
|
|
17564
|
+
} else for (const ref of this.knownClients) {
|
|
17565
|
+
const client = ref.deref();
|
|
17566
|
+
if (!client) {
|
|
17567
|
+
this.knownClients.delete(ref);
|
|
17568
|
+
continue;
|
|
17569
|
+
}
|
|
17570
|
+
if (this.isChildActiveForClient(client, childLink)) {
|
|
17571
|
+
this.emitter.emit(eventName, data);
|
|
17572
|
+
break;
|
|
17573
|
+
}
|
|
17574
|
+
}
|
|
17575
|
+
});
|
|
17576
|
+
this.childSubscriptions.push(sub);
|
|
17577
|
+
}
|
|
17578
|
+
}
|
|
17579
|
+
/**
|
|
17580
|
+
* Bubbles a child nohandler to the parent for the given client.
|
|
17581
|
+
* Only fires if the client is currently in a state that has this childLink.
|
|
17582
|
+
* Extracted from the lambda in setupChildSubscriptions to keep it readable.
|
|
17583
|
+
*/
|
|
17584
|
+
bubbleNohandler(client, childLink, inputName, args) {
|
|
17585
|
+
if (!this.isChildActiveForClient(client, childLink)) return;
|
|
17586
|
+
const meta = this.clients.get(client);
|
|
17587
|
+
meta.currentActionArgs = args;
|
|
17588
|
+
this.handleLocally(client, inputName, args, meta);
|
|
17589
|
+
}
|
|
17590
|
+
/**
|
|
17591
|
+
* Returns true if the given client is currently in a parent state whose
|
|
17592
|
+
* _child resolves to the same underlying instance as childLink. Returns
|
|
17593
|
+
* false if the client has no meta (never initialized) or is in a state
|
|
17594
|
+
* with a different (or no) child.
|
|
17595
|
+
*
|
|
17596
|
+
* Compares `.instance`, not the `ChildLink` wrapper itself — setupChildSubscriptions()
|
|
17597
|
+
* dedupes subscriptions by instance and keeps only ONE representative wrapper
|
|
17598
|
+
* per shared child, so the client's actual active declaring state may hold a
|
|
17599
|
+
* DIFFERENT wrapper for that same instance (wrapChildLinks() mints one per
|
|
17600
|
+
* declaring state). Comparing wrappers directly would only ever match the one
|
|
17601
|
+
* declaring state whose wrapper happened to be kept for the subscription,
|
|
17602
|
+
* silently breaking relay for every other declaring name of a shared child.
|
|
17603
|
+
*/
|
|
17604
|
+
isChildActiveForClient(client, childLink) {
|
|
17605
|
+
var _this$states$meta$sta3;
|
|
17606
|
+
const meta = this.clients.get(client);
|
|
17607
|
+
if (!meta) return false;
|
|
17608
|
+
return ((_this$states$meta$sta3 = this.states[meta.state]) === null || _this$states$meta$sta3 === void 0 || (_this$states$meta$sta3 = _this$states$meta$sta3._child) === null || _this$states$meta$sta3 === void 0 ? void 0 : _this$states$meta$sta3.instance) === childLink.instance;
|
|
17609
|
+
}
|
|
17610
|
+
/**
|
|
17611
|
+
* The inner handler dispatch — no delegation, no initialization side effects
|
|
17612
|
+
* beyond what getOrCreateClientMeta already did. Called by handle() after
|
|
17613
|
+
* the delegation check, and by the nohandler child listener for bubbling.
|
|
17614
|
+
*/
|
|
17615
|
+
handleLocally(client, inputName, args, meta) {
|
|
17616
|
+
var _stateObj$inputName2;
|
|
17617
|
+
const stateObj = this.states[meta.state];
|
|
17618
|
+
const handler = (_stateObj$inputName2 = stateObj === null || stateObj === void 0 ? void 0 : stateObj[inputName]) !== null && _stateObj$inputName2 !== void 0 ? _stateObj$inputName2 : stateObj === null || stateObj === void 0 ? void 0 : stateObj["*"];
|
|
17619
|
+
if (!handler) {
|
|
17620
|
+
this.emitter.emit("nohandler", {
|
|
17621
|
+
inputName,
|
|
17622
|
+
args,
|
|
17623
|
+
client
|
|
17624
|
+
});
|
|
17625
|
+
meta.currentActionArgs = void 0;
|
|
17626
|
+
return;
|
|
17627
|
+
}
|
|
17628
|
+
try {
|
|
17629
|
+
this.emitter.emit("handling", {
|
|
17630
|
+
inputName,
|
|
17631
|
+
client
|
|
17632
|
+
});
|
|
17633
|
+
const handlerArgs = this.buildHandlerArgs(client, inputName, meta);
|
|
17634
|
+
let targetState = void 0;
|
|
17635
|
+
if (typeof handler === "string") targetState = handler;else if (typeof handler === "function") targetState = handler(handlerArgs, ...args);
|
|
17636
|
+
this.emitter.emit("handled", {
|
|
17637
|
+
inputName,
|
|
17638
|
+
client
|
|
17639
|
+
});
|
|
17640
|
+
if (typeof targetState === "string") this.transition(client, targetState);
|
|
17641
|
+
} finally {
|
|
17642
|
+
meta.currentActionArgs = void 0;
|
|
17643
|
+
}
|
|
17644
|
+
}
|
|
17645
|
+
getOrCreateClientMeta(client) {
|
|
17646
|
+
let meta = this.clients.get(client);
|
|
17647
|
+
if (meta) return meta;
|
|
17648
|
+
meta = {
|
|
17649
|
+
state: void 0,
|
|
17650
|
+
deferredQueue: []
|
|
17651
|
+
};
|
|
17652
|
+
this.clients.set(client, meta);
|
|
17653
|
+
this.knownClients.add(new WeakRef(client));
|
|
17654
|
+
this.transition(client, this.initialState);
|
|
17655
|
+
return meta;
|
|
17656
|
+
}
|
|
17657
|
+
buildHandlerArgs(client, inputName, meta) {
|
|
17658
|
+
return {
|
|
17659
|
+
ctx: client,
|
|
17660
|
+
inputName,
|
|
17661
|
+
defer: opts => {
|
|
17662
|
+
if (!meta.currentActionArgs) return;
|
|
17663
|
+
const deferred = {
|
|
17664
|
+
inputName,
|
|
17665
|
+
args: [...meta.currentActionArgs],
|
|
17666
|
+
untilState: opts === null || opts === void 0 ? void 0 : opts.until
|
|
17667
|
+
};
|
|
17668
|
+
meta.deferredQueue.push(deferred);
|
|
17669
|
+
this.emitter.emit("deferred", {
|
|
17670
|
+
inputName,
|
|
17671
|
+
client
|
|
17672
|
+
});
|
|
17673
|
+
},
|
|
17674
|
+
emit: (evtName, evtData) => {
|
|
17675
|
+
this.emitter.emit(evtName, evtData);
|
|
17676
|
+
}
|
|
17677
|
+
};
|
|
17678
|
+
}
|
|
17679
|
+
processQueue(client, meta) {
|
|
17680
|
+
const toReplay = [];
|
|
17681
|
+
const remaining = [];
|
|
17682
|
+
for (const item of meta.deferredQueue) if (item.untilState === void 0 || item.untilState === meta.state) toReplay.push(item);else remaining.push(item);
|
|
17683
|
+
meta.deferredQueue = remaining;
|
|
17684
|
+
for (const item of toReplay) this.handle(client, item.inputName, ...item.args);
|
|
17685
|
+
}
|
|
17686
|
+
};
|
|
17687
|
+
/**
|
|
17688
|
+
* Internal factory called by `wrapChildLinks()` during construction.
|
|
17689
|
+
* Wraps a raw Fsm or BehavioralFsm instance in a uniform `ChildLink` adapter
|
|
17690
|
+
* so the parent engine doesn't need to know which type it's talking to.
|
|
17691
|
+
*
|
|
17692
|
+
* Users never call this directly — they assign an FSM instance to `_child`
|
|
17693
|
+
* in their state config and `wrapChildLinks()` handles the wrapping.
|
|
17694
|
+
*/
|
|
17695
|
+
function createChildLink(child) {
|
|
17696
|
+
if (!child || typeof child !== "object") throw new Error("createChildLink: expected an Fsm or BehavioralFsm instance, got ".concat(String(child)));
|
|
17697
|
+
const childType = child[MACHINA_TYPE];
|
|
17698
|
+
if (childType === "BehavioralFsm") return {
|
|
17699
|
+
instance: child,
|
|
17700
|
+
canHandle(client, inputName) {
|
|
17701
|
+
return child.canHandle(client, inputName);
|
|
17702
|
+
},
|
|
17703
|
+
handle(client, inputName) {
|
|
17704
|
+
for (var _len2 = arguments.length, args = new Array(_len2 > 2 ? _len2 - 2 : 0), _key2 = 2; _key2 < _len2; _key2++) {
|
|
17705
|
+
args[_key2 - 2] = arguments[_key2];
|
|
17706
|
+
}
|
|
17707
|
+
child.handle(client, inputName, ...args);
|
|
17708
|
+
},
|
|
17709
|
+
reset(client) {
|
|
17710
|
+
child.transition(client, child.initialState);
|
|
17711
|
+
},
|
|
17712
|
+
onAny(callback) {
|
|
17713
|
+
return child.on("*", callback);
|
|
17714
|
+
},
|
|
17715
|
+
compositeState(client) {
|
|
17716
|
+
return child.compositeState(client);
|
|
17717
|
+
},
|
|
17718
|
+
rehydrate(client, compositeState) {
|
|
17719
|
+
child.rehydrate(client, compositeState);
|
|
17720
|
+
},
|
|
17721
|
+
dehydrate(client, isOnActivePath) {
|
|
17722
|
+
return child.dehydrate(client, isOnActivePath);
|
|
17723
|
+
},
|
|
17724
|
+
planRehydrate(client, snapshot) {
|
|
17725
|
+
return child.planSnapshotWrites(client, snapshot);
|
|
17726
|
+
},
|
|
17727
|
+
dispose() {
|
|
17728
|
+
child.dispose();
|
|
17729
|
+
}
|
|
17730
|
+
};
|
|
17731
|
+
if (childType === "Fsm") return {
|
|
17732
|
+
instance: child,
|
|
17733
|
+
canHandle(_client, inputName) {
|
|
17734
|
+
return child.canHandle(inputName);
|
|
17735
|
+
},
|
|
17736
|
+
handle(_client, inputName) {
|
|
17737
|
+
for (var _len3 = arguments.length, args = new Array(_len3 > 2 ? _len3 - 2 : 0), _key3 = 2; _key3 < _len3; _key3++) {
|
|
17738
|
+
args[_key3 - 2] = arguments[_key3];
|
|
17739
|
+
}
|
|
17740
|
+
child.handle(inputName, ...args);
|
|
17741
|
+
},
|
|
17742
|
+
reset(_client) {
|
|
17743
|
+
child.reset();
|
|
17744
|
+
},
|
|
17745
|
+
onAny(callback) {
|
|
17746
|
+
return child.on("*", callback);
|
|
17747
|
+
},
|
|
17748
|
+
compositeState(_client) {
|
|
17749
|
+
return child.compositeState();
|
|
17750
|
+
},
|
|
17751
|
+
rehydrate(_client, _compositeState) {
|
|
17752
|
+
throw new Error("rehydrate: cannot rehydrate an Fsm child. Fsm owns its own context; rehydrate is only valid for BehavioralFsm hierarchies.");
|
|
17753
|
+
},
|
|
17754
|
+
dehydrate(_client, _isOnActivePath) {
|
|
17755
|
+
throw new Error("dehydrate: cannot dehydrate an Fsm child. Fsm owns its own context; dehydrate is only valid for BehavioralFsm hierarchies.");
|
|
17756
|
+
},
|
|
17757
|
+
planRehydrate(_client, _snapshot) {
|
|
17758
|
+
throw new Error("rehydrate: cannot rehydrate an Fsm child. Fsm owns its own context; rehydrate is only valid for BehavioralFsm hierarchies.");
|
|
17759
|
+
},
|
|
17760
|
+
dispose() {
|
|
17761
|
+
child.dispose();
|
|
17762
|
+
}
|
|
17763
|
+
};
|
|
17764
|
+
throw new Error("createChildLink: expected an Fsm or BehavioralFsm instance, got [MACHINA_TYPE] = ".concat(String(childType !== null && childType !== void 0 ? childType : "undefined")));
|
|
17765
|
+
}
|
|
17766
|
+
|
|
17767
|
+
//#endregion
|
|
17768
|
+
//#region src/fsm.ts
|
|
17769
|
+
/**
|
|
17770
|
+
* Single-client FSM. Wraps a BehavioralFsm and uses the config's `context`
|
|
17771
|
+
* object as the implicit client, so callers never pass a client argument.
|
|
17772
|
+
*
|
|
17773
|
+
* Prefer `createFsm()` over constructing this directly — the factory infers
|
|
17774
|
+
* all generic parameters from the config object.
|
|
17775
|
+
*
|
|
17776
|
+
* All public methods silently no-op after `dispose()` is called.
|
|
17777
|
+
*
|
|
17778
|
+
* @typeParam TCtx - The context type, inferred from `config.context`.
|
|
17779
|
+
* @typeParam TStateNames - String literal union of valid state names.
|
|
17780
|
+
* @typeParam TInputNames - String literal union of valid input names.
|
|
17781
|
+
* @typeParam TBubbles - String literal union of inputs this FSM declares via
|
|
17782
|
+
* `bubbles`. Type-only — carried so `BubblesOfInstance` can extract it from
|
|
17783
|
+
* a constructed instance; nothing at runtime reads this generic.
|
|
17784
|
+
*/
|
|
17785
|
+
var Fsm = class Fsm {
|
|
17786
|
+
constructor(config) {
|
|
17787
|
+
var _config$context;
|
|
17788
|
+
_defineProperty(this, "id", void 0);
|
|
17789
|
+
_defineProperty(this, "initialState", void 0);
|
|
17790
|
+
_defineProperty(this, MACHINA_TYPE, "Fsm");
|
|
17791
|
+
_defineProperty(this, "states", void 0);
|
|
17792
|
+
_defineProperty(this, "bfsm", void 0);
|
|
17793
|
+
_defineProperty(this, "context", void 0);
|
|
17794
|
+
_defineProperty(this, "emitter", new Emitter());
|
|
17795
|
+
_defineProperty(this, "disposed", false);
|
|
17796
|
+
this.id = config.id;
|
|
17797
|
+
this.initialState = config.initialState;
|
|
17798
|
+
this.context = (_config$context = config.context) !== null && _config$context !== void 0 ? _config$context : {};
|
|
17799
|
+
this.bfsm = new BehavioralFsm(config);
|
|
17800
|
+
this.states = config.states;
|
|
17801
|
+
this.bfsm.on("*", (eventName, data) => {
|
|
17802
|
+
if (data && typeof data === "object" && "client" in data) {
|
|
17803
|
+
data.client;
|
|
17804
|
+
const payload = _objectWithoutProperties(data, _excluded);
|
|
17805
|
+
this.emitter.emit(eventName, payload);
|
|
17806
|
+
} else this.emitter.emit(eventName, data);
|
|
17807
|
+
});
|
|
17808
|
+
this.bfsm.transition(this.context, config.initialState);
|
|
17809
|
+
}
|
|
17810
|
+
/**
|
|
17811
|
+
* Dispatch an input to the current state's handler.
|
|
17812
|
+
* If a `_child` FSM in the current state can handle it, delegation occurs
|
|
17813
|
+
* there first; unhandled inputs bubble up to the parent.
|
|
17814
|
+
* No-ops silently when disposed.
|
|
17815
|
+
*/
|
|
17816
|
+
handle(inputName) {
|
|
17817
|
+
if (this.disposed) return;
|
|
17818
|
+
for (var _len4 = arguments.length, args = new Array(_len4 > 1 ? _len4 - 1 : 0), _key4 = 1; _key4 < _len4; _key4++) {
|
|
17819
|
+
args[_key4 - 1] = arguments[_key4];
|
|
17820
|
+
}
|
|
17821
|
+
this.bfsm.handle(this.context, inputName, ...args);
|
|
17822
|
+
}
|
|
17823
|
+
/**
|
|
17824
|
+
* Returns true if the current state has a handler for `inputName`
|
|
17825
|
+
* (or a catch-all `"*"` handler), or if the current state's `_child`
|
|
17826
|
+
* chain can handle it (checked recursively, matching `handle()`'s
|
|
17827
|
+
* delegation reach). Does not trigger initialization or any side
|
|
17828
|
+
* effects. Returns false when disposed.
|
|
17829
|
+
*/
|
|
17830
|
+
canHandle(inputName) {
|
|
17831
|
+
if (this.disposed) return false;
|
|
17832
|
+
return this.bfsm.canHandle(this.context, inputName);
|
|
17833
|
+
}
|
|
17834
|
+
/**
|
|
17835
|
+
* Transition back to `initialState`, firing `_onEnter` and lifecycle
|
|
17836
|
+
* events as if entering it fresh. No-ops silently when disposed.
|
|
17837
|
+
*/
|
|
17838
|
+
reset() {
|
|
17839
|
+
if (this.disposed) return;
|
|
17840
|
+
this.bfsm.reset(this.context);
|
|
17841
|
+
}
|
|
17842
|
+
/**
|
|
17843
|
+
* Returns the current state name. Always defined — Fsm eagerly
|
|
17844
|
+
* initializes into `initialState` during construction.
|
|
17845
|
+
*/
|
|
17846
|
+
currentState() {
|
|
17847
|
+
return this.bfsm.currentState(this.context);
|
|
17848
|
+
}
|
|
17849
|
+
/**
|
|
17850
|
+
* Directly transition to `toState`, firing `_onExit`, `_onEnter`, and
|
|
17851
|
+
* lifecycle events. Same-state transitions are silently ignored.
|
|
17852
|
+
* No-ops when disposed.
|
|
17853
|
+
*/
|
|
17854
|
+
transition(toState) {
|
|
17855
|
+
if (this.disposed) return;
|
|
17856
|
+
this.bfsm.transition(this.context, toState);
|
|
17857
|
+
}
|
|
17858
|
+
/**
|
|
17859
|
+
* Returns the current state as a dot-delimited path that includes
|
|
17860
|
+
* any active child FSM states (e.g. `"active.connecting.retrying"`).
|
|
17861
|
+
* Returns just the current state name when no child is active.
|
|
17862
|
+
*/
|
|
17863
|
+
compositeState() {
|
|
17864
|
+
return this.bfsm.compositeState(this.context);
|
|
17865
|
+
}
|
|
17866
|
+
on(eventName, callback) {
|
|
17867
|
+
if (this.disposed) return {
|
|
17868
|
+
off() {}
|
|
17869
|
+
};
|
|
17870
|
+
return this.emitter.on(eventName, callback);
|
|
17871
|
+
}
|
|
17872
|
+
/**
|
|
17873
|
+
* Emit a custom event through the FSM. Built-in lifecycle events are
|
|
17874
|
+
* emitted automatically — this is for user-defined events from handlers.
|
|
17875
|
+
* Routes through the BehavioralFsm so all relay paths are consistent.
|
|
17876
|
+
* No-ops when disposed.
|
|
17877
|
+
*/
|
|
17878
|
+
emit(eventName, data) {
|
|
17879
|
+
if (this.disposed) return;
|
|
17880
|
+
this.bfsm.emit(eventName, data);
|
|
17881
|
+
}
|
|
17882
|
+
/**
|
|
17883
|
+
* Permanently shut down this FSM. Irreversible — all subsequent method
|
|
17884
|
+
* calls become silent no-ops. Clears all listeners and cascades disposal
|
|
17885
|
+
* to child FSMs (unless `preserveChildren` is set).
|
|
17886
|
+
*/
|
|
17887
|
+
dispose(options) {
|
|
17888
|
+
this.disposed = true;
|
|
17889
|
+
this.bfsm.dispose(options);
|
|
17890
|
+
this.emitter.clear();
|
|
17891
|
+
}
|
|
17892
|
+
};
|
|
17893
|
+
/**
|
|
17894
|
+
* Create a single-client FSM from a config object.
|
|
17895
|
+
*
|
|
17896
|
+
* Generic parameters are inferred automatically:
|
|
17897
|
+
* - `TCtx` comes from `config.context` (defaults to `{}` if omitted).
|
|
17898
|
+
* - `TStates` is captured with `const` inference to preserve string literal
|
|
17899
|
+
* types, enabling compile-time validation of transition targets and `handle()`
|
|
17900
|
+
* input names.
|
|
17901
|
+
*
|
|
17902
|
+
* State names, input names, and all handler signatures derive from `TStates`.
|
|
17903
|
+
*
|
|
17904
|
+
* @example
|
|
17905
|
+
* ```ts
|
|
17906
|
+
* const light = createFsm({
|
|
17907
|
+
* id: "traffic-light",
|
|
17908
|
+
* initialState: "green",
|
|
17909
|
+
* context: { tickCount: 0 },
|
|
17910
|
+
* states: {
|
|
17911
|
+
* green: { timeout: "yellow" },
|
|
17912
|
+
* yellow: { timeout: "red" },
|
|
17913
|
+
* red: { timeout: "green" },
|
|
17914
|
+
* },
|
|
17915
|
+
* });
|
|
17916
|
+
*
|
|
17917
|
+
* light.handle("timeout"); // transitions green → yellow
|
|
17918
|
+
* ```
|
|
17919
|
+
*/
|
|
17920
|
+
function createFsm(config) {
|
|
17921
|
+
return new Fsm(config);
|
|
17922
|
+
}/**
|
|
17923
|
+
* Declares a handler for one input, restoring the payload typing that machina's `...unknown[]`
|
|
17924
|
+
* handler arguments give up.
|
|
17925
|
+
*/
|
|
17926
|
+
function on(handler) {
|
|
17927
|
+
return handler;
|
|
17928
|
+
}
|
|
17929
|
+
/**
|
|
17930
|
+
* Whether a transport-originated input belongs to the attempt that currently owns the session.
|
|
17931
|
+
* Inputs from a superseded attempt are dropped: its transport is already being replaced, so it can
|
|
17932
|
+
* neither declare the session live nor take it down.
|
|
17933
|
+
*/
|
|
17934
|
+
function isCurrentAttempt(ctx, event) {
|
|
17935
|
+
return event.attemptId === ctx.attemptId;
|
|
17936
|
+
}
|
|
17937
|
+
const attemptEstablished = on((_ref, event) => {
|
|
17938
|
+
let ctx = _ref.ctx;
|
|
17939
|
+
if (!isCurrentAttempt(ctx, event)) {
|
|
17940
|
+
return;
|
|
17941
|
+
}
|
|
17942
|
+
return 'connected';
|
|
17943
|
+
});
|
|
17944
|
+
const startConnect = on(_ref2 => {
|
|
17945
|
+
let ctx = _ref2.ctx;
|
|
17946
|
+
ctx.attemptId += 1;
|
|
17947
|
+
ctx.lastError = undefined;
|
|
17948
|
+
return 'connecting';
|
|
17949
|
+
});
|
|
17950
|
+
const startReconnect = on(_ref3 => {
|
|
17951
|
+
let ctx = _ref3.ctx;
|
|
17952
|
+
ctx.attemptId += 1;
|
|
17953
|
+
ctx.lastError = undefined;
|
|
17954
|
+
return 'reconnecting';
|
|
17955
|
+
});
|
|
17956
|
+
const requestClose = on((_ref4, event) => {
|
|
17957
|
+
let ctx = _ref4.ctx;
|
|
17958
|
+
ctx.closeReason = event.reason;
|
|
17959
|
+
return 'disconnecting';
|
|
17960
|
+
});
|
|
17961
|
+
const signalStates = {
|
|
17962
|
+
new: {
|
|
17963
|
+
connect: startConnect,
|
|
17964
|
+
close: requestClose
|
|
17965
|
+
},
|
|
17966
|
+
connecting: {
|
|
17967
|
+
connectComplete: attemptEstablished,
|
|
17968
|
+
// An initial connect has no session to fall back on, so failure is terminal.
|
|
17969
|
+
connectFailed: on((_ref5, event) => {
|
|
17970
|
+
let ctx = _ref5.ctx;
|
|
17971
|
+
ctx.lastError = event.error;
|
|
17972
|
+
return 'closed';
|
|
17973
|
+
}),
|
|
17974
|
+
close: requestClose
|
|
17975
|
+
},
|
|
17976
|
+
connected: {
|
|
17977
|
+
reconnect: startReconnect,
|
|
17978
|
+
transportFailed: on((_ref6, event) => {
|
|
17979
|
+
let ctx = _ref6.ctx;
|
|
17980
|
+
if (!isCurrentAttempt(ctx, event)) {
|
|
17981
|
+
// a transport that has already been replaced, reporting its close late
|
|
17982
|
+
return;
|
|
17983
|
+
}
|
|
17984
|
+
ctx.lastError = event.reason;
|
|
17985
|
+
return 'offline';
|
|
17986
|
+
}),
|
|
17987
|
+
close: requestClose
|
|
17988
|
+
},
|
|
17989
|
+
offline: {
|
|
17990
|
+
connect: startConnect,
|
|
17991
|
+
reconnect: startReconnect,
|
|
17992
|
+
close: requestClose
|
|
17993
|
+
},
|
|
17994
|
+
reconnecting: {
|
|
17995
|
+
reconnectComplete: attemptEstablished,
|
|
17996
|
+
reconnectFailed: on((_ref7, event) => {
|
|
17997
|
+
let ctx = _ref7.ctx;
|
|
17998
|
+
ctx.lastError = event.error;
|
|
17999
|
+
return event.recoverable ? 'offline' : 'closed';
|
|
18000
|
+
}),
|
|
18001
|
+
close: requestClose
|
|
18002
|
+
},
|
|
18003
|
+
// Owns the transport until the close handshake settles. Every path into this state is followed
|
|
18004
|
+
// by a `closeComplete`, so it cannot become a trap.
|
|
18005
|
+
disconnecting: {
|
|
18006
|
+
closeComplete: 'closed'
|
|
18007
|
+
},
|
|
18008
|
+
// No transport, but the session may still be resumable: the engine resumes after an unexpected
|
|
18009
|
+
// close just as it does from `offline`.
|
|
18010
|
+
closed: {
|
|
18011
|
+
connect: startConnect,
|
|
18012
|
+
reconnect: startReconnect
|
|
18013
|
+
}
|
|
18014
|
+
};
|
|
18015
|
+
/**
|
|
18016
|
+
* Lifecycle model of the signal connection.
|
|
18017
|
+
*
|
|
18018
|
+
* The machine deliberately does not own connection attempts: `SignalClient` performs the
|
|
18019
|
+
* asynchronous work and reports the outcome. It also does not decide *whether* to reconnect —
|
|
18020
|
+
* that policy (backoff, resume vs. full reconnect, region failover, giving up) belongs to
|
|
18021
|
+
* `RTCEngine`, so transport loss lands in `offline` rather than starting a reconnect on its own.
|
|
18022
|
+
*
|
|
18023
|
+
* Each client gets its own instance: the context is mutable and per-connection.
|
|
18024
|
+
*/
|
|
18025
|
+
function createSignalMachine() {
|
|
18026
|
+
let initialState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'new';
|
|
18027
|
+
const context = {
|
|
18028
|
+
attemptId: 0
|
|
18029
|
+
};
|
|
18030
|
+
return createFsm({
|
|
18031
|
+
id: 'signal',
|
|
18032
|
+
initialState: initialState,
|
|
18033
|
+
context,
|
|
18034
|
+
states: signalStates
|
|
18035
|
+
});
|
|
16623
18036
|
}/**
|
|
16624
18037
|
* [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) with [Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API)
|
|
16625
18038
|
*
|
|
@@ -16657,7 +18070,14 @@ class WebSocketStream {
|
|
|
16657
18070
|
let data = _ref2.data;
|
|
16658
18071
|
return controller.enqueue(data);
|
|
16659
18072
|
};
|
|
16660
|
-
ws.onerror = e => controller.error(e);
|
|
18073
|
+
ws.onerror = e => controller.error(ConnectionError.websocket(e instanceof Error ? "".concat(e.name, ": ").concat(e.message) : "Encountered unknown websocket error: ".concat(String(e))));
|
|
18074
|
+
ws.onclose = ev => {
|
|
18075
|
+
if (ev.wasClean) {
|
|
18076
|
+
controller.close();
|
|
18077
|
+
} else {
|
|
18078
|
+
controller.error(ConnectionError.websocket("WS closed unexpectedly with code ".concat(ev.code)));
|
|
18079
|
+
}
|
|
18080
|
+
};
|
|
16661
18081
|
},
|
|
16662
18082
|
cancel: closeWithInfo
|
|
16663
18083
|
}),
|
|
@@ -16696,7 +18116,7 @@ class WebSocketStream {
|
|
|
16696
18116
|
resolve(reason);
|
|
16697
18117
|
}
|
|
16698
18118
|
});
|
|
16699
|
-
ws.
|
|
18119
|
+
ws.addEventListener('close', _ref3 => {
|
|
16700
18120
|
let code = _ref3.code,
|
|
16701
18121
|
reason = _ref3.reason;
|
|
16702
18122
|
resolve({
|
|
@@ -16704,7 +18124,7 @@ class WebSocketStream {
|
|
|
16704
18124
|
reason
|
|
16705
18125
|
});
|
|
16706
18126
|
ws.removeEventListener('error', rejectHandler);
|
|
16707
|
-
};
|
|
18127
|
+
});
|
|
16708
18128
|
ws.addEventListener('error', rejectHandler);
|
|
16709
18129
|
});
|
|
16710
18130
|
if (options.signal) {
|
|
@@ -16713,6 +18133,16 @@ class WebSocketStream {
|
|
|
16713
18133
|
this.close = closeWithInfo;
|
|
16714
18134
|
}
|
|
16715
18135
|
}const passThroughQueueSignals = ['syncState', 'trickle', 'offer', 'answer', 'simulate', 'leave'];
|
|
18136
|
+
/**
|
|
18137
|
+
* Whether a failed resume may be followed by another one. Mirrors how `RTCEngine` classifies these
|
|
18138
|
+
* errors: a server leave ends the session, and an expired token cannot be recovered by retrying.
|
|
18139
|
+
*/
|
|
18140
|
+
function isRecoverableReconnectError(error) {
|
|
18141
|
+
if (error instanceof ConnectionError) {
|
|
18142
|
+
return error.reason !== ConnectionErrorReason.LeaveRequest && error.reason !== ConnectionErrorReason.NotAllowed;
|
|
18143
|
+
}
|
|
18144
|
+
return true;
|
|
18145
|
+
}
|
|
16716
18146
|
function canPassThroughQueue(req) {
|
|
16717
18147
|
const canPass = passThroughQueueSignals.indexOf(req.case) >= 0;
|
|
16718
18148
|
livekitLogger.trace('request allowed to bypass queue:', {
|
|
@@ -16729,6 +18159,24 @@ var SignalConnectionState;
|
|
|
16729
18159
|
SignalConnectionState[SignalConnectionState["DISCONNECTING"] = 3] = "DISCONNECTING";
|
|
16730
18160
|
SignalConnectionState[SignalConnectionState["DISCONNECTED"] = 4] = "DISCONNECTED";
|
|
16731
18161
|
})(SignalConnectionState || (SignalConnectionState = {}));
|
|
18162
|
+
/**
|
|
18163
|
+
* Public projection of the lifecycle machine's states. `new`, `offline` and `closed` are all
|
|
18164
|
+
* reported as `DISCONNECTED`: they differ in what may happen next
|
|
18165
|
+
*/
|
|
18166
|
+
function lifecycleToConnectionState(lifecycle) {
|
|
18167
|
+
switch (lifecycle) {
|
|
18168
|
+
case 'connected':
|
|
18169
|
+
return SignalConnectionState.CONNECTED;
|
|
18170
|
+
case 'connecting':
|
|
18171
|
+
return SignalConnectionState.CONNECTING;
|
|
18172
|
+
case 'reconnecting':
|
|
18173
|
+
return SignalConnectionState.RECONNECTING;
|
|
18174
|
+
case 'disconnecting':
|
|
18175
|
+
return SignalConnectionState.DISCONNECTING;
|
|
18176
|
+
default:
|
|
18177
|
+
return SignalConnectionState.DISCONNECTED;
|
|
18178
|
+
}
|
|
18179
|
+
}
|
|
16732
18180
|
/** specifies how much time (in ms) we allow for the ws to close its connection gracefully before continuing */
|
|
16733
18181
|
const MAX_WS_CLOSE_TIME = 250;
|
|
16734
18182
|
/**
|
|
@@ -16738,13 +18186,41 @@ const JOIN_RESPONSE_TIMEOUT = 5000;
|
|
|
16738
18186
|
/** @internal */
|
|
16739
18187
|
class SignalClient {
|
|
16740
18188
|
get currentState() {
|
|
16741
|
-
return this.
|
|
18189
|
+
return lifecycleToConnectionState(this.lifecycleState);
|
|
16742
18190
|
}
|
|
16743
18191
|
get isDisconnected() {
|
|
16744
|
-
|
|
18192
|
+
const state = this.currentState;
|
|
18193
|
+
return state === SignalConnectionState.DISCONNECTING || state === SignalConnectionState.DISCONNECTED;
|
|
18194
|
+
}
|
|
18195
|
+
/** Runtime lifecycle state, finer grained than the public {@link currentState} projection. */
|
|
18196
|
+
get lifecycleState() {
|
|
18197
|
+
return this.machine.currentState();
|
|
18198
|
+
}
|
|
18199
|
+
/** Id of the current connection attempt and of the transport it owns. */
|
|
18200
|
+
get attemptId() {
|
|
18201
|
+
return this.machine.context.attemptId;
|
|
18202
|
+
}
|
|
18203
|
+
/**
|
|
18204
|
+
* Applies a lifecycle input and reports whether it moved the machine
|
|
18205
|
+
*/
|
|
18206
|
+
sendLifecycleInput(input) {
|
|
18207
|
+
const before = this.lifecycleState;
|
|
18208
|
+
this.machine.handle(input.type, input);
|
|
18209
|
+
return this.lifecycleState !== before;
|
|
18210
|
+
}
|
|
18211
|
+
/**
|
|
18212
|
+
* Waits out a close that is already in flight. Establishing a session while one is tearing down
|
|
18213
|
+
* would race it for the transport — the teardown can close the socket the new attempt just
|
|
18214
|
+
* opened.
|
|
18215
|
+
*/
|
|
18216
|
+
settleInFlightClose() {
|
|
18217
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
18218
|
+
this.log.debug('waiting for an in-flight close to settle before establishing a session');
|
|
18219
|
+
(yield this.closingLock.lock())();
|
|
18220
|
+
});
|
|
16745
18221
|
}
|
|
16746
18222
|
get isEstablishingConnection() {
|
|
16747
|
-
return this.
|
|
18223
|
+
return this.lifecycleState === 'connecting' || this.lifecycleState === 'reconnecting';
|
|
16748
18224
|
}
|
|
16749
18225
|
getNextRequestId() {
|
|
16750
18226
|
this._requestId += 1;
|
|
@@ -16756,7 +18232,6 @@ class SignalClient {
|
|
|
16756
18232
|
var _a;
|
|
16757
18233
|
/** signal rtt in milliseconds */
|
|
16758
18234
|
this.rtt = 0;
|
|
16759
|
-
this.state = SignalConnectionState.DISCONNECTED;
|
|
16760
18235
|
this.log = livekitLogger;
|
|
16761
18236
|
this._requestId = 0;
|
|
16762
18237
|
this.useV0SignalPath = false;
|
|
@@ -16782,7 +18257,18 @@ class SignalClient {
|
|
|
16782
18257
|
this.queuedRequests = [];
|
|
16783
18258
|
this.closingLock = new _();
|
|
16784
18259
|
this.connectionLock = new _();
|
|
16785
|
-
this.
|
|
18260
|
+
this.machine = createSignalMachine();
|
|
18261
|
+
this.machine.on('transitioned', _ref => {
|
|
18262
|
+
let fromState = _ref.fromState,
|
|
18263
|
+
toState = _ref.toState;
|
|
18264
|
+
this.log.debug("signal lifecycle: ".concat(fromState, " -> ").concat(toState));
|
|
18265
|
+
});
|
|
18266
|
+
this.machine.on('nohandler', _ref2 => {
|
|
18267
|
+
let inputName = _ref2.inputName;
|
|
18268
|
+
this.log.debug("ignoring signal lifecycle input ".concat(inputName, " in state ").concat(this.lifecycleState));
|
|
18269
|
+
});
|
|
18270
|
+
// no-op unless a development tool asked for it; see utils/machineInspector
|
|
18271
|
+
announceMachine('signal', this.machine);
|
|
16786
18272
|
}
|
|
16787
18273
|
get logContext() {
|
|
16788
18274
|
var _a, _b;
|
|
@@ -16794,12 +18280,30 @@ class SignalClient {
|
|
|
16794
18280
|
let useV0Path = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
|
|
16795
18281
|
let publisherOffer = arguments.length > 5 ? arguments[5] : undefined;
|
|
16796
18282
|
return function* () {
|
|
16797
|
-
|
|
16798
|
-
|
|
16799
|
-
|
|
18283
|
+
if (_this.lifecycleState === 'disconnecting') {
|
|
18284
|
+
yield _this.settleInFlightClose();
|
|
18285
|
+
}
|
|
18286
|
+
if (!_this.sendLifecycleInput({
|
|
18287
|
+
type: 'connect'
|
|
18288
|
+
})) {
|
|
18289
|
+
// Proceeding would open a transport the lifecycle does not own: its completion would be
|
|
18290
|
+
// discarded and the session left claiming a transport that had been torn down. Every caller
|
|
18291
|
+
// that restarts a session closes the previous one first (see RTCEngine.restartConnection).
|
|
18292
|
+
throw ConnectionError.internal("cannot establish a signal session from '".concat(_this.lifecycleState, "', close the current one first"));
|
|
18293
|
+
}
|
|
16800
18294
|
_this.options = opts;
|
|
16801
|
-
|
|
16802
|
-
|
|
18295
|
+
try {
|
|
18296
|
+
const res = yield _this.connect(url, token, opts, abortSignal, useV0Path, publisherOffer);
|
|
18297
|
+
return res;
|
|
18298
|
+
} catch (e) {
|
|
18299
|
+
// reported here rather than at each rejection site inside connect(), so that no failure
|
|
18300
|
+
// path can leave the machine stuck in `connecting`
|
|
18301
|
+
_this.sendLifecycleInput({
|
|
18302
|
+
type: 'connectFailed',
|
|
18303
|
+
error: e
|
|
18304
|
+
});
|
|
18305
|
+
throw e;
|
|
18306
|
+
}
|
|
16803
18307
|
}();
|
|
16804
18308
|
});
|
|
16805
18309
|
}
|
|
@@ -16809,15 +18313,31 @@ class SignalClient {
|
|
|
16809
18313
|
this.log.warn('attempted to reconnect without signal options being set, ignoring');
|
|
16810
18314
|
return;
|
|
16811
18315
|
}
|
|
16812
|
-
this.
|
|
18316
|
+
if (this.lifecycleState === 'disconnecting') {
|
|
18317
|
+
yield this.settleInFlightClose();
|
|
18318
|
+
}
|
|
18319
|
+
if (!this.sendLifecycleInput({
|
|
18320
|
+
type: 'reconnect'
|
|
18321
|
+
})) {
|
|
18322
|
+
throw ConnectionError.internal("cannot resume the signal session from '".concat(this.lifecycleState, "'"));
|
|
18323
|
+
}
|
|
16813
18324
|
// clear ping interval and restart it once reconnected
|
|
16814
18325
|
this.clearPingInterval();
|
|
16815
|
-
|
|
16816
|
-
|
|
16817
|
-
|
|
16818
|
-
|
|
16819
|
-
|
|
16820
|
-
|
|
18326
|
+
try {
|
|
18327
|
+
const res = yield this.connect(url, token, Object.assign(Object.assign({}, this.options), {
|
|
18328
|
+
reconnect: true,
|
|
18329
|
+
sid,
|
|
18330
|
+
reconnectReason: reason
|
|
18331
|
+
}), undefined, this.useV0SignalPath);
|
|
18332
|
+
return res;
|
|
18333
|
+
} catch (e) {
|
|
18334
|
+
this.sendLifecycleInput({
|
|
18335
|
+
type: 'reconnectFailed',
|
|
18336
|
+
error: e,
|
|
18337
|
+
recoverable: isRecoverableReconnectError(e)
|
|
18338
|
+
});
|
|
18339
|
+
throw e;
|
|
18340
|
+
}
|
|
16821
18341
|
});
|
|
16822
18342
|
}
|
|
16823
18343
|
connect(url_1, token_1, opts_1, abortSignal_1) {
|
|
@@ -16854,7 +18374,11 @@ class SignalClient {
|
|
|
16854
18374
|
this.close();
|
|
16855
18375
|
}
|
|
16856
18376
|
cleanupAbortHandlers();
|
|
16857
|
-
|
|
18377
|
+
// The caller may already have classified this failure: the connect timeout hands us a
|
|
18378
|
+
// Timeout error. Only a genuine abort is a cancellation, and reporting a stalled connect as
|
|
18379
|
+
// one makes the engine read it as user intent — skipping region failover and never
|
|
18380
|
+
// recording the attempt against the backoff strategy (see Room.connect).
|
|
18381
|
+
reject(eventOrError instanceof ConnectionError ? eventOrError : ConnectionError.cancelled(reason));
|
|
16858
18382
|
});
|
|
16859
18383
|
abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.addEventListener('abort', abortHandler);
|
|
16860
18384
|
const cleanupAbortHandlers = () => {
|
|
@@ -16864,48 +18388,62 @@ class SignalClient {
|
|
|
16864
18388
|
const wsTimeout = setTimeout(() => {
|
|
16865
18389
|
abortHandler(ConnectionError.timeout('room connection has timed out (signal)'));
|
|
16866
18390
|
}, opts.websocketTimeout);
|
|
16867
|
-
const handleSignalConnected = (connection, firstMessage) => {
|
|
16868
|
-
this.handleSignalConnected(connection, wsTimeout, firstMessage);
|
|
16869
|
-
};
|
|
16870
18391
|
const redactedUrl = new URL(rtcUrl);
|
|
16871
18392
|
if (redactedUrl.searchParams.has('access_token')) {
|
|
16872
18393
|
redactedUrl.searchParams.set('access_token', '<redacted>');
|
|
16873
18394
|
}
|
|
16874
18395
|
if (this.ws) {
|
|
16875
18396
|
const startClose = performance.now();
|
|
16876
|
-
yield this.
|
|
18397
|
+
yield this.teardownTransport('replaced by a new connection attempt');
|
|
16877
18398
|
this.log.debug("closed previous ws connection in ".concat(performance.now() - startClose, "ms"));
|
|
16878
18399
|
}
|
|
18400
|
+
// the transport created below belongs to this attempt; events arriving from it after a
|
|
18401
|
+
// newer attempt has started are dropped by the machine
|
|
18402
|
+
const attemptId = this.attemptId;
|
|
16879
18403
|
this.log.info("signal connecting to ".concat(redactedUrl), {
|
|
16880
18404
|
reconnect: opts.reconnect,
|
|
16881
18405
|
reconnectReason: opts.reconnectReason
|
|
16882
18406
|
});
|
|
16883
18407
|
this.ws = new WebSocketStream(rtcUrl);
|
|
18408
|
+
// A failed upgrade closes the socket, so `opened` and `closed` settle for the same cause — but
|
|
18409
|
+
// only the `opened` path can classify it (a 401 is known after asking the validate endpoint).
|
|
18410
|
+
// Noting the failure *before* the close handler is registered means it runs first, so a close
|
|
18411
|
+
// that merely reflects a failed upgrade stands down and lets the classified error reach the
|
|
18412
|
+
// caller. Any other close still fails the attempt straight away: after a successful upgrade
|
|
18413
|
+
// nothing else would, short of the first-message timeout.
|
|
18414
|
+
let upgradeFailed = false;
|
|
18415
|
+
this.ws.opened.catch(() => {
|
|
18416
|
+
upgradeFailed = true;
|
|
18417
|
+
});
|
|
16884
18418
|
try {
|
|
16885
18419
|
this.ws.closed.then(closeInfo => {
|
|
16886
|
-
if (this.isEstablishingConnection) {
|
|
18420
|
+
if (this.isEstablishingConnection && !upgradeFailed) {
|
|
16887
18421
|
reject(ConnectionError.internal("Websocket got closed during a (re)connection attempt: ".concat(closeInfo.reason)));
|
|
16888
18422
|
}
|
|
16889
|
-
|
|
16890
|
-
|
|
16891
|
-
|
|
16892
|
-
|
|
16893
|
-
|
|
16894
|
-
|
|
16895
|
-
|
|
16896
|
-
|
|
16897
|
-
|
|
16898
|
-
|
|
16899
|
-
|
|
18423
|
+
this.log.debug('websocket closed', {
|
|
18424
|
+
reason: closeInfo.reason,
|
|
18425
|
+
code: closeInfo.closeCode,
|
|
18426
|
+
attemptId,
|
|
18427
|
+
state: this.lifecycleState
|
|
18428
|
+
});
|
|
18429
|
+
// Every close of the live transport is reported, including a clean 1000 one: a server
|
|
18430
|
+
// that drops signalling closes cleanly — a migration that never sends its
|
|
18431
|
+
// `Leave{action=RESUME}` does exactly that — and treating that as "nothing happened"
|
|
18432
|
+
// leaves the client believing it is still connected until something else notices.
|
|
18433
|
+
// Closes we caused ourselves, and closes from a transport that has since been
|
|
18434
|
+
// replaced, are dropped by the machine's state and attempt guards below rather than by
|
|
18435
|
+
// a close-code test.
|
|
18436
|
+
this.handleOnClose(closeInfo.reason || (closeInfo.closeCode === 1000 ? 'server closed the signal connection' : 'Unexpected WS error'), attemptId);
|
|
16900
18437
|
return;
|
|
16901
18438
|
}).catch(reason => {
|
|
16902
|
-
if (this.isEstablishingConnection) {
|
|
18439
|
+
if (this.isEstablishingConnection && !upgradeFailed) {
|
|
16903
18440
|
reject(ConnectionError.internal("Websocket error during a (re)connection attempt: ".concat(reason)));
|
|
16904
18441
|
}
|
|
16905
18442
|
});
|
|
16906
18443
|
const connection = yield this.ws.opened.catch(reason => __awaiter(this, void 0, void 0, function* () {
|
|
16907
|
-
if (this.
|
|
16908
|
-
|
|
18444
|
+
if (this.lifecycleState !== 'connected') {
|
|
18445
|
+
// claimed synchronously, before the await below: the socket's close event is already
|
|
18446
|
+
// on its way and would otherwise reject first with a less useful error
|
|
16909
18447
|
clearTimeout(wsTimeout);
|
|
16910
18448
|
const error = yield this.handleConnectionError(reason, validateUrl);
|
|
16911
18449
|
reject(error);
|
|
@@ -16970,7 +18508,7 @@ class SignalClient {
|
|
|
16970
18508
|
}
|
|
16971
18509
|
// Handle successful connection
|
|
16972
18510
|
const firstMessageToProcess = validation.shouldProcessFirstMessage ? firstSignalResponse : undefined;
|
|
16973
|
-
handleSignalConnected(connection, firstMessageToProcess);
|
|
18511
|
+
this.handleSignalConnected(connection, wsTimeout, attemptId, firstMessageToProcess);
|
|
16974
18512
|
resolve(validation.response);
|
|
16975
18513
|
} catch (e) {
|
|
16976
18514
|
reject(e);
|
|
@@ -16989,18 +18527,27 @@ class SignalClient {
|
|
|
16989
18527
|
if (firstMessage) {
|
|
16990
18528
|
this.handleSignalResponse(firstMessage);
|
|
16991
18529
|
}
|
|
18530
|
+
const attemptId = this.attemptId;
|
|
16992
18531
|
while (true) {
|
|
16993
18532
|
if (this.signalLatency) {
|
|
16994
18533
|
yield sleep(this.signalLatency);
|
|
16995
18534
|
}
|
|
16996
|
-
|
|
16997
|
-
|
|
16998
|
-
|
|
16999
|
-
|
|
18535
|
+
try {
|
|
18536
|
+
const _yield$signalReader$r = yield signalReader.read(),
|
|
18537
|
+
done = _yield$signalReader$r.done,
|
|
18538
|
+
value = _yield$signalReader$r.value;
|
|
18539
|
+
if (done) {
|
|
18540
|
+
break;
|
|
18541
|
+
}
|
|
18542
|
+
const resp = parseSignalResponse(value);
|
|
18543
|
+
this.handleSignalResponse(resp);
|
|
18544
|
+
} catch (e) {
|
|
18545
|
+
this.log.error("error reading from signal stream", {
|
|
18546
|
+
error: e
|
|
18547
|
+
});
|
|
18548
|
+
yield this.handleOnClose('error in reading loop', attemptId);
|
|
17000
18549
|
break;
|
|
17001
18550
|
}
|
|
17002
|
-
const resp = parseSignalResponse(value);
|
|
17003
|
-
this.handleSignalResponse(resp);
|
|
17004
18551
|
}
|
|
17005
18552
|
});
|
|
17006
18553
|
}
|
|
@@ -17010,40 +18557,53 @@ class SignalClient {
|
|
|
17010
18557
|
let updateState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
|
|
17011
18558
|
let reason = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'Close method called on signal client';
|
|
17012
18559
|
return function* () {
|
|
17013
|
-
|
|
17014
|
-
|
|
17015
|
-
|
|
17016
|
-
|
|
18560
|
+
// when the lifecycle is already shutting down (or another close owns it), only the transport
|
|
18561
|
+
// teardown below still applies — it is idempotent, so callers can always await a close
|
|
18562
|
+
const drivesLifecycle = updateState && _this3.sendLifecycleInput({
|
|
18563
|
+
type: 'close',
|
|
18564
|
+
reason
|
|
18565
|
+
});
|
|
17017
18566
|
const unlock = yield _this3.closingLock.lock();
|
|
17018
18567
|
try {
|
|
17019
|
-
_this3.
|
|
17020
|
-
if (updateState) {
|
|
17021
|
-
_this3.state = SignalConnectionState.DISCONNECTING;
|
|
17022
|
-
}
|
|
17023
|
-
if (_this3.ws) {
|
|
17024
|
-
_this3.ws.close({
|
|
17025
|
-
closeCode: 1000,
|
|
17026
|
-
reason
|
|
17027
|
-
});
|
|
17028
|
-
// calling `ws.close()` only starts the closing handshake (CLOSING state), prefer to wait until state is actually CLOSED
|
|
17029
|
-
const closePromise = _this3.ws.closed;
|
|
17030
|
-
_this3.ws = undefined;
|
|
17031
|
-
_this3.streamWriter = undefined;
|
|
17032
|
-
yield Promise.race([closePromise, sleep(MAX_WS_CLOSE_TIME)]);
|
|
17033
|
-
}
|
|
17034
|
-
} catch (e) {
|
|
17035
|
-
_this3.log.debug('websocket error while closing', {
|
|
17036
|
-
error: e
|
|
17037
|
-
});
|
|
18568
|
+
yield _this3.teardownTransport(reason);
|
|
17038
18569
|
} finally {
|
|
17039
|
-
if (
|
|
17040
|
-
_this3.
|
|
18570
|
+
if (drivesLifecycle) {
|
|
18571
|
+
_this3.sendLifecycleInput({
|
|
18572
|
+
type: 'closeComplete'
|
|
18573
|
+
});
|
|
17041
18574
|
}
|
|
17042
18575
|
unlock();
|
|
17043
18576
|
}
|
|
17044
18577
|
}();
|
|
17045
18578
|
});
|
|
17046
18579
|
}
|
|
18580
|
+
/**
|
|
18581
|
+
* Releases the transport and everything tied to it, without touching the lifecycle state. Used
|
|
18582
|
+
* both by {@link close} and by paths that replace the transport under a live lifecycle (a new
|
|
18583
|
+
* attempt, or an unexpected close that leaves the client in `offline`).
|
|
18584
|
+
*/
|
|
18585
|
+
teardownTransport(reason) {
|
|
18586
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
18587
|
+
try {
|
|
18588
|
+
this.clearPingInterval();
|
|
18589
|
+
if (this.ws) {
|
|
18590
|
+
this.ws.close({
|
|
18591
|
+
closeCode: 1000,
|
|
18592
|
+
reason
|
|
18593
|
+
});
|
|
18594
|
+
// calling `ws.close()` only starts the closing handshake (CLOSING state), prefer to wait until state is actually CLOSED
|
|
18595
|
+
const closePromise = this.ws.closed;
|
|
18596
|
+
this.ws = undefined;
|
|
18597
|
+
this.streamWriter = undefined;
|
|
18598
|
+
yield Promise.race([closePromise, sleep(MAX_WS_CLOSE_TIME)]);
|
|
18599
|
+
}
|
|
18600
|
+
} catch (e) {
|
|
18601
|
+
this.log.debug('websocket error while closing', {
|
|
18602
|
+
error: e
|
|
18603
|
+
});
|
|
18604
|
+
}
|
|
18605
|
+
});
|
|
18606
|
+
}
|
|
17047
18607
|
// initial offer after joining
|
|
17048
18608
|
sendOffer(offer, offerId) {
|
|
17049
18609
|
this.log.debug('sending offer', {
|
|
@@ -17222,7 +18782,8 @@ class SignalClient {
|
|
|
17222
18782
|
// capture all requests while reconnecting and put them in a queue
|
|
17223
18783
|
// unless the request originates from the queue, then don't enqueue again
|
|
17224
18784
|
const canQueue = !fromQueue && !canPassThroughQueue(message);
|
|
17225
|
-
|
|
18785
|
+
const isHoldingRequests = _this5.lifecycleState === 'reconnecting' || _this5.queuedRequests.length > 0;
|
|
18786
|
+
if (canQueue && isHoldingRequests) {
|
|
17226
18787
|
_this5.queuedRequests.push(() => __awaiter(_this5, void 0, void 0, function* () {
|
|
17227
18788
|
yield this.sendRequest(message, true);
|
|
17228
18789
|
}));
|
|
@@ -17235,7 +18796,11 @@ class SignalClient {
|
|
|
17235
18796
|
if (_this5.signalLatency) {
|
|
17236
18797
|
yield sleep(_this5.signalLatency);
|
|
17237
18798
|
}
|
|
17238
|
-
|
|
18799
|
+
// `leave` is the one request whose purpose is to be sent on the way out (an aborted connect
|
|
18800
|
+
// attempt tells the server before tearing down), so it is allowed through for as long as the
|
|
18801
|
+
// transport is still there
|
|
18802
|
+
const isLeaveOnShutdown = message.case === 'leave' && !!_this5.streamWriter;
|
|
18803
|
+
if (_this5.isDisconnected && !isLeaveOnShutdown) {
|
|
17239
18804
|
// Skip requests if the signal layer is disconnected
|
|
17240
18805
|
// This can happen if an event is sent in the mist of room.connect() initializing
|
|
17241
18806
|
_this5.log.debug("skipping signal request (type: ".concat(message.case, ") - SignalClient disconnected"));
|
|
@@ -17389,17 +18954,39 @@ class SignalClient {
|
|
|
17389
18954
|
}
|
|
17390
18955
|
}
|
|
17391
18956
|
}
|
|
17392
|
-
|
|
17393
|
-
|
|
17394
|
-
|
|
17395
|
-
|
|
17396
|
-
|
|
17397
|
-
|
|
17398
|
-
|
|
17399
|
-
|
|
17400
|
-
|
|
17401
|
-
onCloseCallback
|
|
17402
|
-
|
|
18957
|
+
/**
|
|
18958
|
+
* Handles a transport we lost without asking to. The client goes to `offline` rather than
|
|
18959
|
+
* `closed`: whether this session gets resumed, restarted or given up on is the engine's call.
|
|
18960
|
+
*/
|
|
18961
|
+
handleOnClose(reason_1) {
|
|
18962
|
+
return __awaiter(this, arguments, void 0, function (reason) {
|
|
18963
|
+
var _this6 = this;
|
|
18964
|
+
let attemptId = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.attemptId;
|
|
18965
|
+
return function* () {
|
|
18966
|
+
const onCloseCallback = _this6.onClose;
|
|
18967
|
+
if (!_this6.sendLifecycleInput({
|
|
18968
|
+
type: 'transportFailed',
|
|
18969
|
+
attemptId,
|
|
18970
|
+
reason
|
|
18971
|
+
})) {
|
|
18972
|
+
// a close we caused ourselves, or one from a transport that has since been replaced: logged
|
|
18973
|
+
// rather than dropped silently, because a close that goes unnoticed leaves the client writing
|
|
18974
|
+
// to a dead socket
|
|
18975
|
+
_this6.log.debug("ignoring transport close in state ".concat(_this6.lifecycleState), {
|
|
18976
|
+
reason,
|
|
18977
|
+
attemptId,
|
|
18978
|
+
currentAttemptId: _this6.attemptId
|
|
18979
|
+
});
|
|
18980
|
+
return;
|
|
18981
|
+
}
|
|
18982
|
+
yield _this6.teardownTransport(reason);
|
|
18983
|
+
_this6.log.info("websocket connection closed: ".concat(reason), {
|
|
18984
|
+
reason
|
|
18985
|
+
});
|
|
18986
|
+
if (onCloseCallback) {
|
|
18987
|
+
onCloseCallback(reason);
|
|
18988
|
+
}
|
|
18989
|
+
}();
|
|
17403
18990
|
});
|
|
17404
18991
|
}
|
|
17405
18992
|
handleWSError(error) {
|
|
@@ -17456,10 +19043,27 @@ class SignalClient {
|
|
|
17456
19043
|
* @param firstMessage Optional first message to process
|
|
17457
19044
|
* @internal
|
|
17458
19045
|
*/
|
|
17459
|
-
handleSignalConnected(connection, timeoutHandle, firstMessage) {
|
|
17460
|
-
this.state = SignalConnectionState.CONNECTED;
|
|
17461
|
-
this.log.info('signal connected');
|
|
19046
|
+
handleSignalConnected(connection, timeoutHandle, attemptId, firstMessage) {
|
|
17462
19047
|
clearTimeout(timeoutHandle);
|
|
19048
|
+
const established = this.sendLifecycleInput(this.lifecycleState === 'reconnecting' ? {
|
|
19049
|
+
type: 'reconnectComplete',
|
|
19050
|
+
attemptId
|
|
19051
|
+
} : {
|
|
19052
|
+
type: 'connectComplete',
|
|
19053
|
+
attemptId
|
|
19054
|
+
});
|
|
19055
|
+
if (!established) {
|
|
19056
|
+
// The attempt was abandoned while we waited for the server's first message: it was closed, or
|
|
19057
|
+
// a newer attempt superseded it. Arming the ping interval and the read loop here would outlive
|
|
19058
|
+
// the session that owns them, so leave the transport to whoever holds the lifecycle now.
|
|
19059
|
+
this.log.debug('discarding a connection whose attempt no longer owns the session', {
|
|
19060
|
+
attemptId,
|
|
19061
|
+
currentAttemptId: this.attemptId,
|
|
19062
|
+
state: this.lifecycleState
|
|
19063
|
+
});
|
|
19064
|
+
return;
|
|
19065
|
+
}
|
|
19066
|
+
this.log.info('signal connected');
|
|
17463
19067
|
this.startPingInterval();
|
|
17464
19068
|
this.startReadingLoop(connection.readable.getReader(), firstMessage);
|
|
17465
19069
|
}
|
|
@@ -17477,7 +19081,7 @@ class SignalClient {
|
|
|
17477
19081
|
isValid: true,
|
|
17478
19082
|
response: firstSignalResponse.message.value
|
|
17479
19083
|
};
|
|
17480
|
-
} else if (this.
|
|
19084
|
+
} else if (this.lifecycleState === 'reconnecting' && ((_b = firstSignalResponse.message) === null || _b === void 0 ? void 0 : _b.case) !== 'leave') {
|
|
17481
19085
|
if (((_c = firstSignalResponse.message) === null || _c === void 0 ? void 0 : _c.case) === 'reconnect') {
|
|
17482
19086
|
return {
|
|
17483
19087
|
isValid: true,
|
|
@@ -18968,8 +20572,10 @@ class PCTransport extends eventsExports.EventEmitter {
|
|
|
18968
20572
|
var _a;
|
|
18969
20573
|
return (_a = this.pc) === null || _a === void 0 ? void 0 : _a.remoteDescription;
|
|
18970
20574
|
}
|
|
20575
|
+
/** stats of the underlying connection, `undefined` when there is none */
|
|
18971
20576
|
getStats() {
|
|
18972
|
-
|
|
20577
|
+
var _a;
|
|
20578
|
+
return (_a = this._pc) === null || _a === void 0 ? void 0 : _a.getStats();
|
|
18973
20579
|
}
|
|
18974
20580
|
getMaxMessageSize() {
|
|
18975
20581
|
var _a, _b;
|
|
@@ -19496,13 +21102,21 @@ class PCTransportManager {
|
|
|
19496
21102
|
this.updateState();
|
|
19497
21103
|
});
|
|
19498
21104
|
}
|
|
21105
|
+
/**
|
|
21106
|
+
* Restarts ICE on the transports that need it. Only the publisher: the server restarts the
|
|
21107
|
+
* subscriber's ICE itself and follows with a fresh offer.
|
|
21108
|
+
*
|
|
21109
|
+
* The subscriber deliberately does NOT enter `restartingIce` here. Queueing its remote
|
|
21110
|
+
* candidates would guard against candidates for a new generation arriving before the offer
|
|
21111
|
+
* that introduces it, but the server does not send them in that order -- on a same-node
|
|
21112
|
+
* resume it buffers them until the offer has gone out, and on a reconnect that lands on
|
|
21113
|
+
* another node it withholds subscriber candidates until immediately before creating the
|
|
21114
|
+
* offer. Setting the flag only risks withholding candidates during the window that decides
|
|
21115
|
+
* whether the reconnect succeeded.
|
|
21116
|
+
*/
|
|
19499
21117
|
triggerIceRestart() {
|
|
19500
21118
|
return __awaiter(this, void 0, void 0, function* () {
|
|
19501
21119
|
this.iceLog.warn('triggering ICE restart');
|
|
19502
|
-
if (this.subscriber) {
|
|
19503
|
-
this.subscriber.restartingIce = true;
|
|
19504
|
-
}
|
|
19505
|
-
// only restart publisher if it's needed
|
|
19506
21120
|
if (this.needsPublisher) {
|
|
19507
21121
|
yield this.createAndSendPublisherOffer({
|
|
19508
21122
|
iceRestart: true
|
|
@@ -21259,12 +22873,17 @@ class LocalTrack extends Track {
|
|
|
21259
22873
|
type: 'audio',
|
|
21260
22874
|
streamId: v.id,
|
|
21261
22875
|
packetsSent: v.packetsSent,
|
|
21262
|
-
packetsLost: v.packetsLost,
|
|
21263
22876
|
bytesSent: v.bytesSent,
|
|
21264
|
-
timestamp: v.timestamp
|
|
21265
|
-
roundTripTime: v.roundTripTime,
|
|
21266
|
-
jitter: v.jitter
|
|
22877
|
+
timestamp: v.timestamp
|
|
21267
22878
|
};
|
|
22879
|
+
// loss, jitter and RTT are only known from what the remote reports back,
|
|
22880
|
+
// the same way the video sender picks them up
|
|
22881
|
+
const remote = stats.get(v.remoteId);
|
|
22882
|
+
if (remote) {
|
|
22883
|
+
audioStats.packetsLost = remote.packetsLost;
|
|
22884
|
+
audioStats.jitter = remote.jitter;
|
|
22885
|
+
audioStats.roundTripTime = remote.roundTripTime;
|
|
22886
|
+
}
|
|
21268
22887
|
}
|
|
21269
22888
|
});
|
|
21270
22889
|
return audioStats;
|
|
@@ -22606,7 +24225,7 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
22606
24225
|
const disconnect = duration => {
|
|
22607
24226
|
this.log.warn("could not recover connection after ".concat(this.reconnectAttempts, " attempts, ").concat(duration, "ms. giving up"));
|
|
22608
24227
|
this.emit(EngineEvent.Disconnected);
|
|
22609
|
-
this.close();
|
|
24228
|
+
this.close("gave up reconnecting after ".concat(this.reconnectAttempts, " attempts, ").concat(duration, "ms"));
|
|
22610
24229
|
};
|
|
22611
24230
|
const duration = Date.now() - this.reconnectStart;
|
|
22612
24231
|
let delay = this.getNextRetryDelay({
|
|
@@ -22847,7 +24466,12 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
22847
24466
|
}();
|
|
22848
24467
|
});
|
|
22849
24468
|
}
|
|
22850
|
-
|
|
24469
|
+
/**
|
|
24470
|
+
* @param reason why the session is ending, recorded by the signal lifecycle. Worth passing
|
|
24471
|
+
* wherever the caller knows more than "someone called close" — the server's leave reason, or
|
|
24472
|
+
* having given up on reconnecting.
|
|
24473
|
+
*/
|
|
24474
|
+
close(reason) {
|
|
22851
24475
|
return __awaiter(this, void 0, void 0, function* () {
|
|
22852
24476
|
const unlock = yield this.closingLock.lock();
|
|
22853
24477
|
if (this.isClosed) {
|
|
@@ -22864,7 +24488,7 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
22864
24488
|
this.clearLostQualityTimeout();
|
|
22865
24489
|
this.cleanupLossyDataStats();
|
|
22866
24490
|
yield this.cleanupPeerConnections();
|
|
22867
|
-
yield this.cleanupClient();
|
|
24491
|
+
yield this.cleanupClient(reason);
|
|
22868
24492
|
} finally {
|
|
22869
24493
|
unlock();
|
|
22870
24494
|
}
|
|
@@ -22884,9 +24508,9 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
22884
24508
|
cleanupLossyDataStats() {
|
|
22885
24509
|
this.lossyChannel.stopThresholdTuning();
|
|
22886
24510
|
}
|
|
22887
|
-
cleanupClient() {
|
|
24511
|
+
cleanupClient(reason) {
|
|
22888
24512
|
return __awaiter(this, void 0, void 0, function* () {
|
|
22889
|
-
yield this.client.close();
|
|
24513
|
+
yield this.client.close(true, reason);
|
|
22890
24514
|
this.client.resetCallbacks();
|
|
22891
24515
|
// Any in-flight addTrack requests are orphaned by the signal reconnect — the new session
|
|
22892
24516
|
// won't deliver `trackPublishedResponse` for them, so reject the pending resolvers and
|
|
@@ -23121,6 +24745,7 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
23121
24745
|
this.handleDisconnect('signal', ReconnectReason.RR_SIGNAL_DISCONNECTED);
|
|
23122
24746
|
};
|
|
23123
24747
|
this.client.onLeave = leave => {
|
|
24748
|
+
var _a;
|
|
23124
24749
|
this.log.info("client leave request received (action=".concat(leave === null || leave === void 0 ? void 0 : leave.action, ")"), {
|
|
23125
24750
|
reason: leave === null || leave === void 0 ? void 0 : leave.reason
|
|
23126
24751
|
});
|
|
@@ -23131,7 +24756,7 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
23131
24756
|
switch (leave.action) {
|
|
23132
24757
|
case LeaveRequest_Action.DISCONNECT:
|
|
23133
24758
|
this.emit(EngineEvent.Disconnected, leave === null || leave === void 0 ? void 0 : leave.reason);
|
|
23134
|
-
this.close();
|
|
24759
|
+
this.close("server leave: ".concat((_a = DisconnectReason[leave.reason]) !== null && _a !== void 0 ? _a : leave.reason));
|
|
23135
24760
|
break;
|
|
23136
24761
|
case LeaveRequest_Action.RECONNECT:
|
|
23137
24762
|
this.fullReconnectOnNext = true;
|
|
@@ -23486,7 +25111,7 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
23486
25111
|
} else {
|
|
23487
25112
|
this.log.info("could not recover connection after ".concat(this.reconnectAttempts, " attempts, ").concat(Date.now() - this.reconnectStart, "ms. giving up"));
|
|
23488
25113
|
this.emit(EngineEvent.Disconnected);
|
|
23489
|
-
yield this.close();
|
|
25114
|
+
yield this.close("gave up reconnecting after ".concat(this.reconnectAttempts, " attempts, ").concat(Date.now() - this.reconnectStart, "ms"));
|
|
23490
25115
|
}
|
|
23491
25116
|
} finally {
|
|
23492
25117
|
this.attemptingReconnect = false;
|
|
@@ -24322,8 +25947,11 @@ const DEFAULT_MAX_PAYLOAD_BYTE_LENGTH = 5000000000;class BaseStreamReader {
|
|
|
24322
25947
|
this._info = info;
|
|
24323
25948
|
this.bytesReceived = 0;
|
|
24324
25949
|
}
|
|
24325
|
-
|
|
24326
|
-
|
|
25950
|
+
/**
|
|
25951
|
+
* Counts a chunk's bytes against `totalByteSize` and reports progress. Chunk ordering and
|
|
25952
|
+
* de-duplication happen upstream in the manager's `ensureOrderedChunks`, so every chunk reaching
|
|
25953
|
+
* here is new and in order.
|
|
25954
|
+
*/
|
|
24327
25955
|
handleChunkReceived(chunk) {
|
|
24328
25956
|
var _a;
|
|
24329
25957
|
this.bytesReceived += chunk.content.byteLength;
|
|
@@ -24331,6 +25959,8 @@ class ByteStreamReader extends BaseStreamReader {
|
|
|
24331
25959
|
const currentProgress = this.totalByteSize ? this.bytesReceived / this.totalByteSize : undefined;
|
|
24332
25960
|
(_a = this.onProgress) === null || _a === void 0 ? void 0 : _a.call(this, currentProgress);
|
|
24333
25961
|
}
|
|
25962
|
+
}
|
|
25963
|
+
class ByteStreamReader extends BaseStreamReader {
|
|
24334
25964
|
[Symbol.asyncIterator]() {
|
|
24335
25965
|
const reader = this.reader.getReader();
|
|
24336
25966
|
// Suppress unhandled rejection on reader.closed — errors are
|
|
@@ -24438,35 +26068,18 @@ class ByteStreamReader extends BaseStreamReader {
|
|
|
24438
26068
|
}
|
|
24439
26069
|
}
|
|
24440
26070
|
/**
|
|
24441
|
-
* A class to read chunks from a ReadableStream and
|
|
26071
|
+
* A class to read chunks from a ReadableStream and decode them as UTF-8 text.
|
|
26072
|
+
*
|
|
26073
|
+
* NOTE: chunk-level `version` (resending a chunk at an already-received `chunkIndex` to supersede
|
|
26074
|
+
* it) is not supported. The reader used to rebuild the whole string from a per-index chunk map and
|
|
26075
|
+
* yield it as `TextStreamChunk.collected`, which made superseding work; 5d4a6346 (#1410, text auto
|
|
26076
|
+
* chunking) changed the iterator to yield each chunk's text as it arrives, and a streaming reader
|
|
26077
|
+
* cannot retract text it has already handed to the consumer. No sender emits a versioned chunk.
|
|
24442
26078
|
*/
|
|
24443
26079
|
class TextStreamReader extends BaseStreamReader {
|
|
24444
|
-
/**
|
|
24445
|
-
* A TextStreamReader instance can be used as an AsyncIterator that returns the entire string
|
|
24446
|
-
* that has been received up to the current point in time.
|
|
24447
|
-
*/
|
|
24448
|
-
constructor(info, stream, totalChunkCount) {
|
|
24449
|
-
super(info, stream, totalChunkCount);
|
|
24450
|
-
this.receivedChunks = new Map();
|
|
24451
|
-
}
|
|
24452
|
-
handleChunkReceived(chunk) {
|
|
24453
|
-
var _a;
|
|
24454
|
-
const index = bigIntToNumber(chunk.chunkIndex);
|
|
24455
|
-
const previousChunkAtIndex = this.receivedChunks.get(index);
|
|
24456
|
-
if (previousChunkAtIndex && previousChunkAtIndex.version > chunk.version) {
|
|
24457
|
-
// we have a newer version already, dropping the old one
|
|
24458
|
-
return;
|
|
24459
|
-
}
|
|
24460
|
-
this.receivedChunks.set(index, chunk);
|
|
24461
|
-
this.bytesReceived += chunk.content.byteLength;
|
|
24462
|
-
this.validateBytesReceived();
|
|
24463
|
-
const currentProgress = this.totalByteSize ? this.bytesReceived / this.totalByteSize : undefined;
|
|
24464
|
-
(_a = this.onProgress) === null || _a === void 0 ? void 0 : _a.call(this, currentProgress);
|
|
24465
|
-
}
|
|
24466
26080
|
/**
|
|
24467
26081
|
* Async iterator implementation to allow usage of `for await...of` syntax.
|
|
24468
|
-
* Yields
|
|
24469
|
-
*
|
|
26082
|
+
* Yields each chunk's decoded text as it arrives - a delta, not the string accumulated so far.
|
|
24470
26083
|
*/
|
|
24471
26084
|
[Symbol.asyncIterator]() {
|
|
24472
26085
|
const reader = this.reader.getReader();
|
|
@@ -24737,7 +26350,7 @@ class TextStreamReader extends BaseStreamReader {
|
|
|
24737
26350
|
});
|
|
24738
26351
|
}
|
|
24739
26352
|
});
|
|
24740
|
-
streamHandlerCallback(new ByteStreamReader(info, compressed ? inflateRawByteChunkStream(stream, streamHeader.streamId, this.maxPayloadByteLength) : stream,
|
|
26353
|
+
streamHandlerCallback(new ByteStreamReader(info, compressed ? inflateRawByteChunkStream(stream, streamHeader.streamId, this.maxPayloadByteLength) : stream.pipeThrough(ensureOrderedChunks(streamHeader.streamId)),
|
|
24741
26354
|
// `totalLength` is the pre-compression size, and the reader counts decompressed bytes,
|
|
24742
26355
|
// so it applies to both paths (mirrors text).
|
|
24743
26356
|
bigIntToNumber(streamHeader.totalLength)), {
|
|
@@ -24809,7 +26422,7 @@ class TextStreamReader extends BaseStreamReader {
|
|
|
24809
26422
|
});
|
|
24810
26423
|
}
|
|
24811
26424
|
});
|
|
24812
|
-
streamHandlerCallback(new TextStreamReader(info, compressed ? inflateRawChunkStream(stream, streamHeader.streamId, this.maxPayloadByteLength) : stream,
|
|
26425
|
+
streamHandlerCallback(new TextStreamReader(info, compressed ? inflateRawChunkStream(stream, streamHeader.streamId, this.maxPayloadByteLength) : stream.pipeThrough(ensureOrderedChunks(streamHeader.streamId)),
|
|
24813
26426
|
// `totalLength` is the pre-compression size, and the reader sees decompressed bytes, so
|
|
24814
26427
|
// it applies to both paths.
|
|
24815
26428
|
bigIntToNumber(streamHeader.totalLength)), {
|
|
@@ -24825,7 +26438,7 @@ class TextStreamReader extends BaseStreamReader {
|
|
|
24825
26438
|
if (fileBuffer.info.encryptionType !== encryptionType) {
|
|
24826
26439
|
fileBuffer.controller.error(new DataStreamError("Encryption type mismatch for stream ".concat(chunk.streamId, ". Expected ").concat(encryptionType, ", got ").concat(fileBuffer.info.encryptionType), DataStreamErrorReason.EncryptionTypeMismatch));
|
|
24827
26440
|
this.byteStreamControllers.delete(chunk.streamId);
|
|
24828
|
-
} else
|
|
26441
|
+
} else {
|
|
24829
26442
|
fileBuffer.controller.enqueue(chunk);
|
|
24830
26443
|
}
|
|
24831
26444
|
}
|
|
@@ -24834,7 +26447,7 @@ class TextStreamReader extends BaseStreamReader {
|
|
|
24834
26447
|
if (textBuffer.info.encryptionType !== encryptionType) {
|
|
24835
26448
|
textBuffer.controller.error(new DataStreamError("Encryption type mismatch for stream ".concat(chunk.streamId, ". Expected ").concat(encryptionType, ", got ").concat(textBuffer.info.encryptionType), DataStreamErrorReason.EncryptionTypeMismatch));
|
|
24836
26449
|
this.textStreamControllers.delete(chunk.streamId);
|
|
24837
|
-
} else
|
|
26450
|
+
} else {
|
|
24838
26451
|
textBuffer.controller.enqueue(chunk);
|
|
24839
26452
|
}
|
|
24840
26453
|
}
|
|
@@ -24882,26 +26495,24 @@ class TextStreamReader extends BaseStreamReader {
|
|
|
24882
26495
|
function createInlineStream(streamId, content) {
|
|
24883
26496
|
return new ReadableStream({
|
|
24884
26497
|
start: controller => __awaiter(this, void 0, void 0, function* () {
|
|
24885
|
-
|
|
24886
|
-
|
|
24887
|
-
|
|
24888
|
-
|
|
24889
|
-
|
|
24890
|
-
|
|
24891
|
-
|
|
24892
|
-
controller.close();
|
|
24893
|
-
} catch (err) {
|
|
24894
|
-
controller.error(err);
|
|
24895
|
-
}
|
|
26498
|
+
const bytes = yield content;
|
|
26499
|
+
controller.enqueue(new DataStream_Chunk({
|
|
26500
|
+
streamId,
|
|
26501
|
+
chunkIndex: BigInt(0),
|
|
26502
|
+
content: bytes
|
|
26503
|
+
}));
|
|
26504
|
+
controller.close();
|
|
24896
26505
|
})
|
|
24897
26506
|
});
|
|
24898
26507
|
}
|
|
24899
26508
|
/**
|
|
24900
26509
|
* Validates that chunks are received in order, dropping duplicates and throwing if gaps are found.
|
|
24901
26510
|
*
|
|
24902
|
-
*
|
|
24903
|
-
* dropped (with a warning - in-order delivery is expected
|
|
24904
|
-
* handling may replay) and a gap is a hard error.
|
|
26511
|
+
* Reassembly (and, for compressed streams, a stateful decompressor) silently corrupts on duplicated
|
|
26512
|
+
* or out-of-order input, so duplicates are dropped (with a warning - in-order delivery is expected
|
|
26513
|
+
* on the reliable channel, but reconnect handling may replay) and a gap is a hard error. Empty
|
|
26514
|
+
* chunks consume their index and are then dropped, so they never reach the reader. Applied to every
|
|
26515
|
+
* chunked stream, compressed or not.
|
|
24905
26516
|
*/
|
|
24906
26517
|
function ensureOrderedChunks(streamId) {
|
|
24907
26518
|
let lastChunkIndex = -1;
|
|
@@ -24909,13 +26520,16 @@ function ensureOrderedChunks(streamId) {
|
|
|
24909
26520
|
transform: (value, controller) => {
|
|
24910
26521
|
const index = bigIntToNumber(value.chunkIndex);
|
|
24911
26522
|
if (index <= lastChunkIndex) {
|
|
24912
|
-
livekitLogger.warn("ignoring duplicate chunk ".concat(index, " for
|
|
26523
|
+
livekitLogger.warn("ignoring duplicate chunk ".concat(index, " ").concat(value.version > 0 ? "(version ".concat(value.version, ")") : '', " for data stream ").concat(streamId, " (last processed: ").concat(lastChunkIndex, ")"));
|
|
24913
26524
|
return;
|
|
24914
26525
|
}
|
|
24915
26526
|
if (index > lastChunkIndex + 1) {
|
|
24916
|
-
throw new DataStreamError("Missing chunk(s) ".concat(lastChunkIndex + 1, "..").concat(index - 1, " for
|
|
26527
|
+
throw new DataStreamError("Missing chunk(s) ".concat(lastChunkIndex + 1, "..").concat(index - 1, " for data stream ").concat(streamId, " - cannot reassemble payload"), DataStreamErrorReason.Incomplete);
|
|
24917
26528
|
}
|
|
24918
26529
|
lastChunkIndex = index;
|
|
26530
|
+
if (value.content.length === 0) {
|
|
26531
|
+
return;
|
|
26532
|
+
}
|
|
24919
26533
|
controller.enqueue(value);
|
|
24920
26534
|
}
|
|
24921
26535
|
});
|
|
@@ -31731,6 +33345,7 @@ class DeferrableMap extends Map {
|
|
|
31731
33345
|
ConnectionState["SignalReconnecting"] = "signalReconnecting";
|
|
31732
33346
|
})(ConnectionState || (ConnectionState = {}));
|
|
31733
33347
|
const CONNECTION_RECONCILE_FREQUENCY_MS = 4 * 1000;
|
|
33348
|
+
const STATS_LOG_FREQUENCY_MS = 30 * 1000;
|
|
31734
33349
|
/**
|
|
31735
33350
|
* In LiveKit, a room is the logical grouping for a list of participants.
|
|
31736
33351
|
* Participants in a room can publish tracks, and subscribe to others' tracks.
|
|
@@ -31749,7 +33364,7 @@ class Room extends eventsExports.EventEmitter {
|
|
|
31749
33364
|
*/
|
|
31750
33365
|
constructor(options) {
|
|
31751
33366
|
var _this;
|
|
31752
|
-
var _a, _b, _c, _d, _e, _f;
|
|
33367
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
31753
33368
|
super();
|
|
31754
33369
|
_this = this;
|
|
31755
33370
|
this.state = ConnectionState.Disconnected;
|
|
@@ -31764,6 +33379,7 @@ class Room extends eventsExports.EventEmitter {
|
|
|
31764
33379
|
this.e2eeStateMutex = new _();
|
|
31765
33380
|
this.isVideoPlaybackBlocked = false;
|
|
31766
33381
|
this.log = livekitLogger;
|
|
33382
|
+
this.statsLog = livekitLogger;
|
|
31767
33383
|
this.bufferedEvents = [];
|
|
31768
33384
|
this.isResuming = false;
|
|
31769
33385
|
this.pendingTrackAddedCallbacks = new Map();
|
|
@@ -32196,7 +33812,7 @@ class Room extends eventsExports.EventEmitter {
|
|
|
32196
33812
|
let remoteParticipant = this.remoteParticipants.get(info.identity);
|
|
32197
33813
|
// when it's disconnected, send updates
|
|
32198
33814
|
if (info.state === ParticipantInfo_State.DISCONNECTED) {
|
|
32199
|
-
this.handleParticipantDisconnected(info.identity, remoteParticipant);
|
|
33815
|
+
this.handleParticipantDisconnected(info.identity, remoteParticipant, info.disconnectReason === DisconnectReason.UNKNOWN_REASON ? undefined : info.disconnectReason);
|
|
32200
33816
|
} else {
|
|
32201
33817
|
// create participant if doesn't exist
|
|
32202
33818
|
this.getOrCreateParticipant(info.identity, info);
|
|
@@ -32448,6 +34064,34 @@ class Room extends eventsExports.EventEmitter {
|
|
|
32448
34064
|
this.getAllRemoteParticipantIdentities = () => {
|
|
32449
34065
|
return Array.from(this.remoteParticipants.keys());
|
|
32450
34066
|
};
|
|
34067
|
+
/**
|
|
34068
|
+
* Dumps stats of both peer connections.
|
|
34069
|
+
*/
|
|
34070
|
+
this.logWebRTCStats = () => __awaiter(this, void 0, void 0, function* () {
|
|
34071
|
+
var _a, _b, _c, _d;
|
|
34072
|
+
const pcManager = (_a = this.engine) === null || _a === void 0 ? void 0 : _a.pcManager;
|
|
34073
|
+
if (!pcManager) {
|
|
34074
|
+
return;
|
|
34075
|
+
}
|
|
34076
|
+
try {
|
|
34077
|
+
const _yield$Promise$all = yield Promise.all([pcManager.publisher.getStats(), (_b = pcManager.subscriber) === null || _b === void 0 ? void 0 : _b.getStats()]),
|
|
34078
|
+
_yield$Promise$all2 = _slicedToArray(_yield$Promise$all, 2),
|
|
34079
|
+
publisher = _yield$Promise$all2[0],
|
|
34080
|
+
subscriber = _yield$Promise$all2[1];
|
|
34081
|
+
const publisherStats = publisher && summarizeStatsReport(publisher);
|
|
34082
|
+
const subscriberStats = subscriber && summarizeStatsReport(subscriber);
|
|
34083
|
+
this.statsLog.info("webrtc stats", {
|
|
34084
|
+
publisher: publisherStats === null || publisherStats === void 0 ? void 0 : publisherStats.connection,
|
|
34085
|
+
subscriber: subscriberStats === null || subscriberStats === void 0 ? void 0 : subscriberStats.connection,
|
|
34086
|
+
inbound: [...((_c = publisherStats === null || publisherStats === void 0 ? void 0 : publisherStats.inbound) !== null && _c !== void 0 ? _c : []), ...((_d = subscriberStats === null || subscriberStats === void 0 ? void 0 : subscriberStats.inbound) !== null && _d !== void 0 ? _d : [])],
|
|
34087
|
+
outbound: publisherStats === null || publisherStats === void 0 ? void 0 : publisherStats.outbound
|
|
34088
|
+
});
|
|
34089
|
+
} catch (error) {
|
|
34090
|
+
this.statsLog.debug('could not collect webrtc stats', {
|
|
34091
|
+
error
|
|
34092
|
+
});
|
|
34093
|
+
}
|
|
34094
|
+
});
|
|
32451
34095
|
this.onLocalParticipantMetadataChanged = metadata => {
|
|
32452
34096
|
this.emit(RoomEvent.ParticipantMetadataChanged, metadata, this.localParticipant);
|
|
32453
34097
|
};
|
|
@@ -32518,12 +34162,15 @@ class Room extends eventsExports.EventEmitter {
|
|
|
32518
34162
|
this.sidToIdentity = new Map();
|
|
32519
34163
|
this.options = Object.assign(Object.assign({}, roomOptionDefaults), options);
|
|
32520
34164
|
this.log = getLogger((_a = this.options.loggerName) !== null && _a !== void 0 ? _a : LoggerNames.Room, () => this.logContext);
|
|
34165
|
+
// its own logger name, so the stats dumps can be silenced or routed
|
|
34166
|
+
// separately from the rest of the room's logs
|
|
34167
|
+
this.statsLog = getLogger(LoggerNames.Stats, () => this.logContext);
|
|
32521
34168
|
this.transcriptionReceivedTimes = new Map();
|
|
32522
34169
|
this.options.audioCaptureDefaults = Object.assign(Object.assign({}, audioDefaults), options === null || options === void 0 ? void 0 : options.audioCaptureDefaults);
|
|
32523
34170
|
this.options.videoCaptureDefaults = Object.assign(Object.assign({}, videoDefaults), options === null || options === void 0 ? void 0 : options.videoCaptureDefaults);
|
|
32524
34171
|
this.options.publishDefaults = Object.assign(Object.assign({}, publishDefaults), options === null || options === void 0 ? void 0 : options.publishDefaults);
|
|
32525
34172
|
this.maybeCreateEngine();
|
|
32526
|
-
this.incomingDataStreamManager = new IncomingDataStreamManager();
|
|
34173
|
+
this.incomingDataStreamManager = new IncomingDataStreamManager((_b = this.options.dataStream) === null || _b === void 0 ? void 0 : _b.maxPayloadByteLength);
|
|
32527
34174
|
this.outgoingDataStreamManager = new OutgoingDataStreamManager(this.engine, this.log, this.getRemoteParticipantClientProtocol, this.getRemoteParticipantCapabilities, this.getAllRemoteParticipantIdentities);
|
|
32528
34175
|
this.incomingDataTrackManager = new IncomingDataTrackManager({
|
|
32529
34176
|
e2eeManager: this.e2eeManager
|
|
@@ -32586,15 +34233,15 @@ class Room extends eventsExports.EventEmitter {
|
|
|
32586
34233
|
this.setupE2EE();
|
|
32587
34234
|
}
|
|
32588
34235
|
this.engine.e2eeManager = this.e2eeManager;
|
|
32589
|
-
this.incomingDataTrackManager.updateE2eeManager((
|
|
32590
|
-
this.outgoingDataTrackManager.updateE2eeManager((
|
|
34236
|
+
this.incomingDataTrackManager.updateE2eeManager((_c = this.e2eeManager) !== null && _c !== void 0 ? _c : null);
|
|
34237
|
+
this.outgoingDataTrackManager.updateE2eeManager((_d = this.e2eeManager) !== null && _d !== void 0 ? _d : null);
|
|
32591
34238
|
if (this.options.videoCaptureDefaults.deviceId) {
|
|
32592
34239
|
this.localParticipant.activeDeviceMap.set('videoinput', unwrapConstraint(this.options.videoCaptureDefaults.deviceId));
|
|
32593
34240
|
}
|
|
32594
34241
|
if (this.options.audioCaptureDefaults.deviceId) {
|
|
32595
34242
|
this.localParticipant.activeDeviceMap.set('audioinput', unwrapConstraint(this.options.audioCaptureDefaults.deviceId));
|
|
32596
34243
|
}
|
|
32597
|
-
if ((
|
|
34244
|
+
if ((_e = this.options.audioOutput) === null || _e === void 0 ? void 0 : _e.deviceId) {
|
|
32598
34245
|
this.switchActiveDevice('audiooutput', unwrapConstraint(this.options.audioOutput.deviceId)).catch(e => this.log.warn("Could not set audio output: ".concat(e.message)));
|
|
32599
34246
|
}
|
|
32600
34247
|
if (isWeb()) {
|
|
@@ -32621,7 +34268,7 @@ class Room extends eventsExports.EventEmitter {
|
|
|
32621
34268
|
onDeviceChange = this.handleDeviceChange;
|
|
32622
34269
|
}
|
|
32623
34270
|
// in order to catch device changes prior to room connection we need to register the event in the constructor
|
|
32624
|
-
(
|
|
34271
|
+
(_g = (_f = navigator.mediaDevices) === null || _f === void 0 ? void 0 : _f.addEventListener) === null || _g === void 0 ? void 0 : _g.call(_f, 'devicechange', onDeviceChange, {
|
|
32625
34272
|
signal: cleanupController.signal
|
|
32626
34273
|
});
|
|
32627
34274
|
}
|
|
@@ -33442,7 +35089,7 @@ class Room extends eventsExports.EventEmitter {
|
|
|
33442
35089
|
this.emit(RoomEvent.Disconnected, reason);
|
|
33443
35090
|
}
|
|
33444
35091
|
}
|
|
33445
|
-
handleParticipantDisconnected(identity, participant) {
|
|
35092
|
+
handleParticipantDisconnected(identity, participant, disconnectReason) {
|
|
33446
35093
|
// remove and send event
|
|
33447
35094
|
this.remoteParticipants.delete(identity);
|
|
33448
35095
|
if (!participant) {
|
|
@@ -33453,7 +35100,7 @@ class Room extends eventsExports.EventEmitter {
|
|
|
33453
35100
|
participant.trackPublications.forEach(publication => {
|
|
33454
35101
|
participant.unpublishTrack(publication.trackSid, true);
|
|
33455
35102
|
});
|
|
33456
|
-
this.emit(RoomEvent.ParticipantDisconnected, participant);
|
|
35103
|
+
this.emit(RoomEvent.ParticipantDisconnected, participant, disconnectReason);
|
|
33457
35104
|
participant.setDisconnected();
|
|
33458
35105
|
this.rpcClientManager.handleParticipantDisconnected(participant.identity);
|
|
33459
35106
|
}
|
|
@@ -33696,6 +35343,19 @@ class Room extends eventsExports.EventEmitter {
|
|
|
33696
35343
|
}();
|
|
33697
35344
|
}));
|
|
33698
35345
|
}
|
|
35346
|
+
setStatsLogging(enabled) {
|
|
35347
|
+
if (enabled) {
|
|
35348
|
+
if (!this.statsLogInterval) {
|
|
35349
|
+
this.statsLogInterval = CriticalTimers.setInterval(() => {
|
|
35350
|
+
// logWebRTCStats handles its own errors, nothing to await here
|
|
35351
|
+
this.logWebRTCStats();
|
|
35352
|
+
}, STATS_LOG_FREQUENCY_MS);
|
|
35353
|
+
}
|
|
35354
|
+
} else if (this.statsLogInterval) {
|
|
35355
|
+
CriticalTimers.clearInterval(this.statsLogInterval);
|
|
35356
|
+
this.statsLogInterval = undefined;
|
|
35357
|
+
}
|
|
35358
|
+
}
|
|
33699
35359
|
registerConnectionReconcile() {
|
|
33700
35360
|
this.clearConnectionReconcile();
|
|
33701
35361
|
let consecutiveFailures = 0;
|
|
@@ -33746,6 +35406,7 @@ class Room extends eventsExports.EventEmitter {
|
|
|
33746
35406
|
this.log.info("connection state changed: ".concat(this.state, " -> ").concat(state));
|
|
33747
35407
|
this.state = state;
|
|
33748
35408
|
this.incomingDataStreamManager.setConnected(state === ConnectionState.Connected);
|
|
35409
|
+
this.setStatsLogging(state === ConnectionState.Connected);
|
|
33749
35410
|
this.emit(RoomEvent.ConnectionStateChanged, this.state);
|
|
33750
35411
|
return true;
|
|
33751
35412
|
}
|
|
@@ -34918,15 +36579,22 @@ function isObject(input) {
|
|
|
34918
36579
|
const ONE_MINUTE_IN_MILLISECONDS = 60 * ONE_SECOND_IN_MILLISECONDS;
|
|
34919
36580
|
function isResponseTokenValid(response) {
|
|
34920
36581
|
const jwtPayload = decodeTokenPayload(response.participantToken);
|
|
34921
|
-
|
|
34922
|
-
|
|
36582
|
+
// Missing exp: TokenSourceCached would otherwise return this response forever.
|
|
36583
|
+
// nbf is optional (RFC 7519); do not skip the exp check when it is absent.
|
|
36584
|
+
if (!(jwtPayload === null || jwtPayload === void 0 ? void 0 : jwtPayload.exp)) {
|
|
36585
|
+
return false;
|
|
34923
36586
|
}
|
|
34924
36587
|
const now = new Date();
|
|
34925
|
-
|
|
34926
|
-
|
|
36588
|
+
if (jwtPayload.nbf) {
|
|
36589
|
+
const nbfInMilliseconds = jwtPayload.nbf * ONE_SECOND_IN_MILLISECONDS;
|
|
36590
|
+
const nbfDate = new Date(nbfInMilliseconds);
|
|
36591
|
+
if (nbfDate > now) {
|
|
36592
|
+
return false;
|
|
36593
|
+
}
|
|
36594
|
+
}
|
|
34927
36595
|
const expInMilliseconds = jwtPayload.exp * ONE_SECOND_IN_MILLISECONDS;
|
|
34928
36596
|
const expDate = new Date(expInMilliseconds - ONE_MINUTE_IN_MILLISECONDS);
|
|
34929
|
-
return
|
|
36597
|
+
return expDate > now;
|
|
34930
36598
|
}
|
|
34931
36599
|
/** Given a LiveKit generated participant token, decodes and returns the associated {@link TokenPayload} data. */
|
|
34932
36600
|
function decodeTokenPayload(token) {
|
|
@@ -35313,4 +36981,4 @@ const serializers = {
|
|
|
35313
36981
|
json,
|
|
35314
36982
|
raw,
|
|
35315
36983
|
custom
|
|
35316
|
-
};export{AudioPresets,BackupCodecPolicy,BaseKeyProvider,CLIENT_PROTOCOL_DATA_STREAM_RPC,CLIENT_PROTOCOL_DATA_STREAM_V2,CLIENT_PROTOCOL_DEFAULT,CheckStatus,Checker,ConnectionCheck,ConnectionError,ConnectionErrorReason,ConnectionQuality,ConnectionState,CriticalTimers,CryptorError,CryptorErrorReason,CryptorEvent,DataPacket_Kind,DataStreamError,DataStreamErrorReason,DataTrackPacket,DefaultReconnectPolicy,DeviceUnsupportedError,DisconnectReason,EncryptionEvent,Encryption_Type,EngineEvent,ExternalE2EEKeyProvider,FrameMetadataManager,KeyHandlerEvent,KeyProviderEvent,LivekitError,LivekitReasonedError,LocalAudioTrack,LocalDataTrack,LocalParticipant,LocalTrack,LocalTrackPublication,LocalTrackRecorder,LocalVideoTrack,LogLevel,LoggerNames,MediaDeviceFailure,_ as Mutex,NegotiationError,PacketTrailerManager,Participant,ParticipantEvent,ParticipantInfo_Kind as ParticipantKind,PublishDataError,PublishTrackError,RemoteAudioTrack,RemoteDataTrack,RemoteParticipant,RemoteTrack,RemoteTrackPublication,RemoteVideoTrack,Room,RoomEvent,RpcError,ScreenSharePresets,SignalReconnectError,SignalRequestError,SimulatedError,SubscriptionError,TokenSource,TokenSourceConfigurable,TokenSourceFixed,Track,TrackEvent,TrackInvalidError,TrackPublication,TrackType,UnexpectedConnectionState,UnsupportedServer,VideoPreset,VideoPresets,VideoPresets43,VideoQuality,areTokenSourceFetchOptionsEqual,asEncryptablePacket,attachToElement,attributeTypings as attributes,audioCodecs,clientProtocol,compareVersions,createAudioAnalyser,createE2EEKey,createKeyMaterialFromBuffer,createKeyMaterialFromString,createLocalAudioTrack,createLocalScreenTracks,createLocalTracks,createLocalVideoTrack,decodeTokenPayload,deriveKeys,detachTrack,facingModeFromDeviceLabel,facingModeFromLocalTrack,getBrowser,getEmptyAudioStreamTrack,getEmptyVideoStreamTrack,getLogger,importKey,isAudioCodec,isAudioTrack,isBackupCodec,isBackupVideoCodec,isBrowserSupported,isE2EESupported,isInsertableStreamSupported,isLocalParticipant,isLocalTrack,isRemoteParticipant,isRemoteTrack,isScriptTransformSupported,isSerializer,isVideoCodec,isVideoFrame,isVideoTrack,needsRbspUnescaping,parseRbsp,protocolVersion,ratchet,serializers,setLogExtension,setLogLevel,supportsAV1,supportsAdaptiveStream,supportsAudioOutputSelection,supportsDynacast,supportsVP9,version,videoCodecs,writeRbsp};//# sourceMappingURL=livekit-client.esm.mjs.map
|
|
36984
|
+
};export{AudioPresets,BackupCodecPolicy,BaseKeyProvider,CLIENT_PROTOCOL_DATA_STREAM_RPC,CLIENT_PROTOCOL_DATA_STREAM_V2,CLIENT_PROTOCOL_DEFAULT,CheckStatus,Checker,ConnectionCheck,ConnectionError,ConnectionErrorReason,ConnectionQuality,ConnectionState,CriticalTimers,CryptorError,CryptorErrorReason,CryptorEvent,DataPacket_Kind,DataStreamError,DataStreamErrorReason,DataTrackPacket,DefaultReconnectPolicy,DeviceUnsupportedError,DisconnectReason,EncryptionEvent,Encryption_Type,EngineEvent,ExternalE2EEKeyProvider,FrameMetadataManager,KeyHandlerEvent,KeyProviderEvent,LivekitError,LivekitReasonedError,LocalAudioTrack,LocalDataTrack,LocalParticipant,LocalTrack,LocalTrackPublication,LocalTrackRecorder,LocalVideoTrack,LogLevel,LoggerNames,MediaDeviceFailure,_ as Mutex,NegotiationError,PacketTrailerManager,Participant,ParticipantEvent,ParticipantInfo_Kind as ParticipantKind,PublishDataError,PublishTrackError,RemoteAudioTrack,RemoteDataTrack,RemoteParticipant,RemoteTrack,RemoteTrackPublication,RemoteVideoTrack,Room,RoomEvent,RpcError,ScreenSharePresets,SignalReconnectError,SignalRequestError,SimulatedError,SubscriptionError,TokenSource,TokenSourceConfigurable,TokenSourceFixed,Track,TrackEvent,TrackInvalidError,TrackPublication,TrackType,UnexpectedConnectionState,UnsupportedServer,VideoPreset,VideoPresets,VideoPresets43,VideoQuality,areTokenSourceFetchOptionsEqual,asEncryptablePacket,attachToElement,attributeTypings as attributes,audioCodecs,clientProtocol,compareVersions,createAudioAnalyser,createE2EEKey,createKeyMaterialFromBuffer,createKeyMaterialFromString,createLocalAudioTrack,createLocalScreenTracks,createLocalTracks,createLocalVideoTrack,decodeTokenPayload,deriveKeys,detachTrack,facingModeFromDeviceLabel,facingModeFromLocalTrack,getBrowser,getEmptyAudioStreamTrack,getEmptyVideoStreamTrack,getLogger,importKey,isAudioCodec,isAudioTrack,isBackupCodec,isBackupVideoCodec,isBrowserSupported,isE2EESupported,isInsertableStreamSupported,isLocalParticipant,isLocalTrack,isRemoteParticipant,isRemoteTrack,isSVCCodec,isScriptTransformSupported,isSerializer,isVideoCodec,isVideoFrame,isVideoTrack,needsRbspUnescaping,parseRbsp,protocolVersion,ratchet,serializers,setLogExtension,setLogLevel,supportsAV1,supportsAdaptiveStream,supportsAudioOutputSelection,supportsDynacast,supportsH265,supportsVP9,version,videoCodecs,writeRbsp};//# sourceMappingURL=livekit-client.esm.mjs.map
|