@stream-io/video-client 1.59.1 → 1.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/dist/index.browser.es.js +495 -105
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +495 -105
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +495 -105
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +23 -3
- package/dist/src/coordinator/connection/types.d.ts +16 -0
- package/dist/src/events/call.d.ts +0 -11
- package/dist/src/gen/coordinator/index.d.ts +73 -0
- package/dist/src/reporting/ClientEventReporter.d.ts +11 -1
- package/dist/src/ringing/RingStatePoller.d.ts +46 -0
- package/dist/src/ringing/RingTimeout.d.ts +27 -0
- package/dist/src/ringing/index.d.ts +4 -0
- package/dist/src/ringing/reconcileRingState.d.ts +18 -0
- package/dist/src/ringing/resolveOwnRingOutcome.d.ts +32 -0
- package/dist/src/stats/utils.d.ts +6 -0
- package/dist/src/store/CallState.d.ts +10 -1
- package/package.json +1 -1
- package/src/Call.ts +93 -61
- package/src/StreamSfuClient.ts +6 -2
- package/src/__tests__/Call.autodrop.test.ts +116 -64
- package/src/__tests__/Call.getRingState.test.ts +87 -0
- package/src/__tests__/Call.lifecycle.test.ts +23 -0
- package/src/__tests__/Call.ringSettled.test.ts +71 -0
- package/src/__tests__/StreamSfuClient.test.ts +48 -0
- package/src/coordinator/connection/types.ts +19 -0
- package/src/events/__tests__/call.test.ts +2 -217
- package/src/events/call.ts +1 -73
- package/src/events/callEventHandlers.ts +12 -23
- package/src/gen/coordinator/index.ts +67 -0
- package/src/reporting/ClientEventReporter.ts +30 -4
- package/src/reporting/__tests__/ClientEventReporter.test.ts +184 -7
- package/src/ringing/RingStatePoller.ts +177 -0
- package/src/ringing/RingTimeout.ts +90 -0
- package/src/ringing/__tests__/RingStatePoller.test.ts +462 -0
- package/src/ringing/__tests__/RingTimeout.test.ts +199 -0
- package/src/ringing/__tests__/reconcileRingState.test.ts +330 -0
- package/src/ringing/__tests__/resolveOwnRingOutcome.test.ts +103 -0
- package/src/ringing/index.ts +4 -0
- package/src/ringing/reconcileRingState.ts +108 -0
- package/src/ringing/resolveOwnRingOutcome.ts +58 -0
- package/src/stats/__tests__/utils.test.ts +33 -0
- package/src/stats/utils.ts +15 -0
- package/src/store/CallState.ts +25 -0
- package/src/store/__tests__/CallState.test.ts +59 -0
package/dist/index.cjs.js
CHANGED
|
@@ -5101,7 +5101,7 @@ const toIceCandidate = (iceTrickle) => {
|
|
|
5101
5101
|
}
|
|
5102
5102
|
};
|
|
5103
5103
|
|
|
5104
|
-
const version = "1.
|
|
5104
|
+
const version = "1.60.0";
|
|
5105
5105
|
const [major, minor, patch] = version.split('.');
|
|
5106
5106
|
let sdkInfo = {
|
|
5107
5107
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -7134,6 +7134,30 @@ class CallState {
|
|
|
7134
7134
|
this.setCurrentValue(this.captioningSubject, call.captioning);
|
|
7135
7135
|
this.setCurrentValue(this.thumbnailsSubject, call.thumbnails);
|
|
7136
7136
|
};
|
|
7137
|
+
/**
|
|
7138
|
+
* Merges a polled ring state into the current session. Only the ring fields
|
|
7139
|
+
* are touched: the endpoint does not return the session roster.
|
|
7140
|
+
*
|
|
7141
|
+
* @internal
|
|
7142
|
+
*
|
|
7143
|
+
* @param ringState the ring state as returned by the coordinator.
|
|
7144
|
+
*/
|
|
7145
|
+
updateFromRingState = (ringState) => {
|
|
7146
|
+
this.setCurrentValue(this.sessionSubject, (session) => {
|
|
7147
|
+
if (!session || session.id !== ringState.session_id)
|
|
7148
|
+
return session;
|
|
7149
|
+
return {
|
|
7150
|
+
...session,
|
|
7151
|
+
accepted_by: ringState.accepted_by,
|
|
7152
|
+
rejected_by: ringState.rejected_by,
|
|
7153
|
+
missed_by: ringState.missed_by,
|
|
7154
|
+
ended_at: ringState.session_ended_at ?? session.ended_at,
|
|
7155
|
+
};
|
|
7156
|
+
});
|
|
7157
|
+
if (ringState.call_ended_at) {
|
|
7158
|
+
this.setEndedAt(new Date(ringState.call_ended_at));
|
|
7159
|
+
}
|
|
7160
|
+
};
|
|
7137
7161
|
/**
|
|
7138
7162
|
* Updates the call state with the data received from the SFU server.
|
|
7139
7163
|
*
|
|
@@ -7445,6 +7469,19 @@ const getSdkName = (sdk) => {
|
|
|
7445
7469
|
const getSdkVersion = (sdk) => {
|
|
7446
7470
|
return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
|
|
7447
7471
|
};
|
|
7472
|
+
/**
|
|
7473
|
+
* Returns the identifier the SFU receives in the `X-Stream-Client` header,
|
|
7474
|
+
* e.g. `stream-video-react-v1.43.1`. Matches the format the coordinator
|
|
7475
|
+
* already gets through the client's user agent.
|
|
7476
|
+
*/
|
|
7477
|
+
const getStreamClientId = (sdk) => {
|
|
7478
|
+
const sdkPlatforms = {
|
|
7479
|
+
[SdkType.REACT]: 'react',
|
|
7480
|
+
[SdkType.REACT_NATIVE]: 'react-native',
|
|
7481
|
+
};
|
|
7482
|
+
const platform = (sdk && sdkPlatforms[sdk.type]) || 'js';
|
|
7483
|
+
return `stream-video-${platform}-v${getSdkVersion(sdk)}`;
|
|
7484
|
+
};
|
|
7448
7485
|
|
|
7449
7486
|
/**
|
|
7450
7487
|
* Creates a new StatsReporter instance that collects metrics about the ongoing call and reports them to the state store
|
|
@@ -10326,7 +10363,10 @@ class StreamSfuClient {
|
|
|
10326
10363
|
this.rpc = createSignalClient({
|
|
10327
10364
|
baseUrl: server.url,
|
|
10328
10365
|
interceptors: [
|
|
10329
|
-
withHeaders({
|
|
10366
|
+
withHeaders({
|
|
10367
|
+
Authorization: `Bearer ${token}`,
|
|
10368
|
+
'X-Stream-Client': getStreamClientId(getSdkInfo()),
|
|
10369
|
+
}),
|
|
10330
10370
|
this.tracer && withRequestTracer(this.tracer.trace),
|
|
10331
10371
|
this.logger.getLogLevel() === 'trace' && withRequestLogger(this.logger),
|
|
10332
10372
|
withTimeout(rpcRequestTimeout, this.tracer?.trace),
|
|
@@ -10663,64 +10703,370 @@ class StreamSfuClient {
|
|
|
10663
10703
|
}
|
|
10664
10704
|
|
|
10665
10705
|
/**
|
|
10666
|
-
*
|
|
10667
|
-
*
|
|
10668
|
-
|
|
10669
|
-
|
|
10670
|
-
|
|
10671
|
-
|
|
10672
|
-
|
|
10673
|
-
|
|
10674
|
-
|
|
10675
|
-
|
|
10676
|
-
|
|
10677
|
-
|
|
10706
|
+
* Decides what a ringing call should do next, based on the current call state.
|
|
10707
|
+
*
|
|
10708
|
+
* The `call.accepted`, `call.rejected` and `call.missed` handlers and the ring
|
|
10709
|
+
* state poller both run this. They differ only in how the state got there: the
|
|
10710
|
+
* handlers rely on `CallState.updateFromEvent`, which runs before them, and the
|
|
10711
|
+
* poller applies the polled ring state itself.
|
|
10712
|
+
*
|
|
10713
|
+
* @param call the call to reconcile.
|
|
10714
|
+
* @param joinSource which of the two triggered this run, reported on the
|
|
10715
|
+
* caller's join: `ring-ws` for the event handlers, `ring-poll-api` for the
|
|
10716
|
+
* poller.
|
|
10717
|
+
* @returns whether the ring reached a terminal state. A failed join is not
|
|
10718
|
+
* terminal: the caller should keep trying while the ring is open.
|
|
10719
|
+
*/
|
|
10720
|
+
const reconcileRingState = async (call, joinSource) => {
|
|
10721
|
+
if (call.state.callingState !== exports.CallingState.RINGING)
|
|
10722
|
+
return true;
|
|
10723
|
+
return call.isCreatedByMe
|
|
10724
|
+
? reconcileAsCaller(call, joinSource)
|
|
10725
|
+
: reconcileAsCallee(call);
|
|
10726
|
+
};
|
|
10727
|
+
const reconcileAsCaller = async (call, joinSource) => {
|
|
10728
|
+
const { session, members, endedAt } = call.state;
|
|
10729
|
+
const currentUserId = call.currentUserId;
|
|
10730
|
+
// checked before `accepted_by`: an ended session cannot be joined
|
|
10731
|
+
if (endedAt || session?.ended_at) {
|
|
10732
|
+
call.logger.info('ring: the call has ended, leaving');
|
|
10733
|
+
// `leave` reports the remote end to callingx off `reason: 'ended'`
|
|
10734
|
+
return leave(call, {
|
|
10735
|
+
reject: false,
|
|
10736
|
+
reason: 'ended',
|
|
10737
|
+
message: 'ring: call ended',
|
|
10738
|
+
});
|
|
10739
|
+
}
|
|
10740
|
+
const acceptedBy = session?.accepted_by ?? {};
|
|
10741
|
+
if (Object.keys(acceptedBy).some((userId) => userId !== currentUserId)) {
|
|
10742
|
+
call.logger.info('ring: the call was accepted, joining');
|
|
10743
|
+
try {
|
|
10744
|
+
await call.join({ joinSource });
|
|
10678
10745
|
}
|
|
10679
|
-
|
|
10746
|
+
catch (err) {
|
|
10747
|
+
// `doJoin` restores the ringing state when a join fails, so the ring is
|
|
10748
|
+
// still open. Report it unsettled and let the next poll retry.
|
|
10749
|
+
call.logger.error('Failed to join an accepted call', err);
|
|
10750
|
+
return false;
|
|
10751
|
+
}
|
|
10752
|
+
return true;
|
|
10753
|
+
}
|
|
10754
|
+
const otherMembers = members
|
|
10755
|
+
.filter((member) => member.user_id !== currentUserId)
|
|
10756
|
+
.map((member) => member.user_id);
|
|
10757
|
+
if (otherMembers.length === 0)
|
|
10758
|
+
return false;
|
|
10759
|
+
const rejectedBy = session?.rejected_by ?? {};
|
|
10760
|
+
if (otherMembers.every((userId) => rejectedBy[userId])) {
|
|
10761
|
+
call.logger.info('ring: everyone rejected, leaving');
|
|
10762
|
+
return leave(call, {
|
|
10763
|
+
reject: true,
|
|
10764
|
+
reason: 'cancel',
|
|
10765
|
+
message: 'ring: everyone rejected',
|
|
10766
|
+
});
|
|
10767
|
+
}
|
|
10768
|
+
return false;
|
|
10680
10769
|
};
|
|
10770
|
+
// the current user's own accept or reject, on this or another device, is
|
|
10771
|
+
// handled by `resolveOwnRingOutcome`, and `call.ended` by `watchCallEnded`.
|
|
10772
|
+
const reconcileAsCallee = async (call) => {
|
|
10773
|
+
const createdById = call.state.createdBy?.id;
|
|
10774
|
+
const rejectedBy = call.state.session?.rejected_by ?? {};
|
|
10775
|
+
if (createdById && rejectedBy[createdById]) {
|
|
10776
|
+
call.logger.info('ring: the caller cancelled, leaving');
|
|
10777
|
+
return leave(call, {
|
|
10778
|
+
reason: 'ended',
|
|
10779
|
+
message: 'ring: creator rejected',
|
|
10780
|
+
});
|
|
10781
|
+
}
|
|
10782
|
+
return false;
|
|
10783
|
+
};
|
|
10784
|
+
// `false` when the call could not be left, so the ring stays open for a retry.
|
|
10785
|
+
const leave = async (call, options) => {
|
|
10786
|
+
try {
|
|
10787
|
+
await call.leave(options);
|
|
10788
|
+
return true;
|
|
10789
|
+
}
|
|
10790
|
+
catch (err) {
|
|
10791
|
+
call.logger.error('Failed to leave a ringing call', err);
|
|
10792
|
+
return false;
|
|
10793
|
+
}
|
|
10794
|
+
};
|
|
10795
|
+
|
|
10796
|
+
// `doJoin` restores the previous calling state when a join fails, so JOINING can
|
|
10797
|
+
// go back to RINGING. Only a state the ring cannot return from ends the poller.
|
|
10798
|
+
const ringIsOver = (callingState) => callingState !== exports.CallingState.RINGING &&
|
|
10799
|
+
callingState !== exports.CallingState.JOINING;
|
|
10681
10800
|
/**
|
|
10682
|
-
*
|
|
10683
|
-
*
|
|
10801
|
+
* Polls the coordinator for the outcome of a ring the current user started.
|
|
10802
|
+
*
|
|
10803
|
+
* `call.accepted`, `call.rejected` and `call.missed` are delivered best-effort,
|
|
10804
|
+
* with no store-and-forward, so a caller that drops one is left on a ringing
|
|
10805
|
+
* screen while the callee is already in the call. After a quiet period this
|
|
10806
|
+
* reads the ring state until a terminal outcome or the end of the ring window.
|
|
10684
10807
|
*/
|
|
10685
|
-
|
|
10686
|
-
|
|
10687
|
-
|
|
10688
|
-
|
|
10808
|
+
class RingStatePoller {
|
|
10809
|
+
logger = videoLoggerSystem.getLogger('RingStatePoller');
|
|
10810
|
+
call;
|
|
10811
|
+
startAfterMs;
|
|
10812
|
+
intervalMs;
|
|
10813
|
+
sessionId;
|
|
10814
|
+
deadlineAt = 0;
|
|
10815
|
+
idleTimeoutId;
|
|
10816
|
+
intervalId;
|
|
10817
|
+
stopped = false;
|
|
10818
|
+
wasPolling = false;
|
|
10819
|
+
inFlight = false;
|
|
10820
|
+
unsubscribe = [];
|
|
10821
|
+
constructor(call, options = {}) {
|
|
10822
|
+
this.call = call;
|
|
10823
|
+
this.startAfterMs = options.startAfterMs ?? 15_000;
|
|
10824
|
+
this.intervalMs = options.intervalMs ?? 5_000;
|
|
10825
|
+
}
|
|
10826
|
+
/**
|
|
10827
|
+
* Starts polling. Does nothing when the call has no session yet.
|
|
10828
|
+
*/
|
|
10829
|
+
start = () => {
|
|
10830
|
+
if (this.stopped || this.sessionId)
|
|
10689
10831
|
return;
|
|
10690
|
-
|
|
10691
|
-
|
|
10692
|
-
|
|
10693
|
-
|
|
10832
|
+
if (this.call.state.callingState !== exports.CallingState.RINGING)
|
|
10833
|
+
return;
|
|
10834
|
+
// captured once: `call.ended` clears the call's current session
|
|
10835
|
+
const sessionId = this.call.state.session?.id;
|
|
10836
|
+
if (!sessionId) {
|
|
10837
|
+
this.logger.warn('the call has no session');
|
|
10694
10838
|
return;
|
|
10695
10839
|
}
|
|
10696
|
-
|
|
10697
|
-
const
|
|
10698
|
-
|
|
10699
|
-
|
|
10840
|
+
this.sessionId = sessionId;
|
|
10841
|
+
const ring = this.call.state.settings?.ring;
|
|
10842
|
+
const maxDurationMs = ring?.auto_cancel_timeout_ms || ring?.missed_call_timeout_ms || 30_000;
|
|
10843
|
+
this.deadlineAt = Date.now() + maxDurationMs;
|
|
10844
|
+
// an incoming event proves the socket is alive, so the quiet period starts
|
|
10845
|
+
// over: in a group ring a single rejection does not settle the ring.
|
|
10846
|
+
this.unsubscribe.push(this.call.on('call.accepted', () => this.armIdleWindow()), this.call.on('call.rejected', () => this.armIdleWindow()), this.call.on('call.missed', () => this.armIdleWindow()),
|
|
10847
|
+
// the ring is over, whichever way it went. Joining an accepted call
|
|
10848
|
+
// never goes through `leave`, so this is the only signal for it.
|
|
10849
|
+
createSubscription(this.call.state.callingState$, (callingState) => {
|
|
10850
|
+
if (ringIsOver(callingState))
|
|
10851
|
+
this.stop();
|
|
10852
|
+
}));
|
|
10853
|
+
this.armIdleWindow();
|
|
10854
|
+
};
|
|
10855
|
+
/**
|
|
10856
|
+
* Pauses polling, keeping the captured session, the deadline and the event
|
|
10857
|
+
* subscriptions. Resuming does not extend the ring window.
|
|
10858
|
+
*/
|
|
10859
|
+
pause = () => {
|
|
10860
|
+
if (this.stopped)
|
|
10861
|
+
return;
|
|
10862
|
+
this.wasPolling = this.intervalId !== undefined;
|
|
10863
|
+
const timers = getTimers();
|
|
10864
|
+
timers.clearTimeout(this.idleTimeoutId);
|
|
10865
|
+
timers.clearInterval(this.intervalId);
|
|
10866
|
+
this.idleTimeoutId = undefined;
|
|
10867
|
+
this.intervalId = undefined;
|
|
10868
|
+
};
|
|
10869
|
+
/**
|
|
10870
|
+
* Resumes a paused poller. Goes straight back to polling if it was already
|
|
10871
|
+
* past the quiet period, so a pause does not buy the ring another one.
|
|
10872
|
+
*/
|
|
10873
|
+
resume = () => {
|
|
10874
|
+
if (this.stopped || !this.sessionId)
|
|
10875
|
+
return;
|
|
10876
|
+
if (this.idleTimeoutId !== undefined || this.intervalId !== undefined) {
|
|
10700
10877
|
return;
|
|
10701
10878
|
}
|
|
10702
|
-
if (
|
|
10703
|
-
|
|
10704
|
-
|
|
10705
|
-
.every((m) => rejectedBy[m.user_id]);
|
|
10706
|
-
if (everyoneElseRejected) {
|
|
10707
|
-
call.logger.info('everyone rejected, leaving the call');
|
|
10708
|
-
await call.leave({
|
|
10709
|
-
reject: true,
|
|
10710
|
-
reason: 'cancel',
|
|
10711
|
-
message: 'ring: everyone rejected',
|
|
10712
|
-
});
|
|
10713
|
-
}
|
|
10879
|
+
if (!this.wasPolling) {
|
|
10880
|
+
this.armIdleWindow();
|
|
10881
|
+
return;
|
|
10714
10882
|
}
|
|
10715
|
-
|
|
10716
|
-
|
|
10717
|
-
|
|
10718
|
-
|
|
10719
|
-
|
|
10883
|
+
this.intervalId = getTimers().setInterval(this.runTick, this.intervalMs);
|
|
10884
|
+
this.runTick();
|
|
10885
|
+
};
|
|
10886
|
+
/**
|
|
10887
|
+
* Stops polling. The poller cannot be restarted.
|
|
10888
|
+
*/
|
|
10889
|
+
stop = () => {
|
|
10890
|
+
if (this.stopped)
|
|
10891
|
+
return;
|
|
10892
|
+
this.stopped = true;
|
|
10893
|
+
const timers = getTimers();
|
|
10894
|
+
timers.clearTimeout(this.idleTimeoutId);
|
|
10895
|
+
timers.clearInterval(this.intervalId);
|
|
10896
|
+
this.idleTimeoutId = undefined;
|
|
10897
|
+
this.intervalId = undefined;
|
|
10898
|
+
this.unsubscribe.forEach((off) => off());
|
|
10899
|
+
this.unsubscribe = [];
|
|
10900
|
+
};
|
|
10901
|
+
armIdleWindow = () => {
|
|
10902
|
+
if (this.stopped)
|
|
10903
|
+
return;
|
|
10904
|
+
const timers = getTimers();
|
|
10905
|
+
timers.clearTimeout(this.idleTimeoutId);
|
|
10906
|
+
timers.clearInterval(this.intervalId);
|
|
10907
|
+
this.intervalId = undefined;
|
|
10908
|
+
this.idleTimeoutId = timers.setTimeout(() => {
|
|
10909
|
+
this.idleTimeoutId = undefined;
|
|
10910
|
+
if (this.stopped)
|
|
10911
|
+
return;
|
|
10912
|
+
this.intervalId = timers.setInterval(this.runTick, this.intervalMs);
|
|
10913
|
+
this.runTick();
|
|
10914
|
+
}, this.startAfterMs);
|
|
10915
|
+
};
|
|
10916
|
+
runTick = () => {
|
|
10917
|
+
this.tick().catch((err) => {
|
|
10918
|
+
this.logger.warn('Failed to poll the ring state', err);
|
|
10919
|
+
});
|
|
10920
|
+
};
|
|
10921
|
+
tick = async () => {
|
|
10922
|
+
if (this.stopped || this.inFlight || !this.sessionId)
|
|
10923
|
+
return;
|
|
10924
|
+
const { callingState } = this.call.state;
|
|
10925
|
+
if (ringIsOver(callingState) || Date.now() >= this.deadlineAt) {
|
|
10926
|
+
this.stop();
|
|
10927
|
+
return;
|
|
10928
|
+
}
|
|
10929
|
+
// mid-join: nothing to reconcile until we know whether it succeeded
|
|
10930
|
+
if (callingState !== exports.CallingState.RINGING)
|
|
10931
|
+
return;
|
|
10932
|
+
this.inFlight = true;
|
|
10933
|
+
try {
|
|
10934
|
+
const ringState = await this.call.getRingState(this.sessionId);
|
|
10935
|
+
if (this.stopped)
|
|
10936
|
+
return;
|
|
10937
|
+
this.call.state.updateFromRingState(ringState);
|
|
10938
|
+
if (await reconcileRingState(this.call, 'ring-poll-api'))
|
|
10939
|
+
this.stop();
|
|
10940
|
+
}
|
|
10941
|
+
catch (err) {
|
|
10942
|
+
// a missing session, or one of another call, will never resolve
|
|
10943
|
+
const status = err instanceof ErrorFromResponse ? err.status : undefined;
|
|
10944
|
+
if (status === 400 || status === 404) {
|
|
10945
|
+
this.logger.warn('Stopped polling the ring state', err);
|
|
10946
|
+
this.stop();
|
|
10720
10947
|
}
|
|
10948
|
+
else {
|
|
10949
|
+
this.logger.debug('Failed to poll the ring state', err);
|
|
10950
|
+
}
|
|
10951
|
+
}
|
|
10952
|
+
finally {
|
|
10953
|
+
this.inFlight = false;
|
|
10721
10954
|
}
|
|
10722
10955
|
};
|
|
10956
|
+
}
|
|
10957
|
+
|
|
10958
|
+
/**
|
|
10959
|
+
* Drops a call that has been ringing for too long: the caller cancels after
|
|
10960
|
+
* `auto_cancel_timeout_ms`, a callee after `incoming_call_timeout_ms`.
|
|
10961
|
+
*
|
|
10962
|
+
* Either timeout being `0` means the call rings until something else settles it.
|
|
10963
|
+
*/
|
|
10964
|
+
class RingTimeout {
|
|
10965
|
+
call;
|
|
10966
|
+
timeoutId;
|
|
10967
|
+
deadlineAt;
|
|
10968
|
+
stopped = false;
|
|
10969
|
+
constructor(call) {
|
|
10970
|
+
this.call = call;
|
|
10971
|
+
}
|
|
10972
|
+
/**
|
|
10973
|
+
* Schedules an auto-drop timeout based on the call settings.
|
|
10974
|
+
* Applicable only for ringing calls.
|
|
10975
|
+
*/
|
|
10976
|
+
start = () => {
|
|
10977
|
+
if (this.stopped || this.timeoutId !== undefined)
|
|
10978
|
+
return;
|
|
10979
|
+
// ignore if the call is not ringing
|
|
10980
|
+
if (this.call.state.callingState !== exports.CallingState.RINGING)
|
|
10981
|
+
return;
|
|
10982
|
+
const ring = this.call.state.settings?.ring;
|
|
10983
|
+
if (!ring)
|
|
10984
|
+
return;
|
|
10985
|
+
const isCaller = this.call.isCreatedByMe;
|
|
10986
|
+
const timeoutMs = isCaller
|
|
10987
|
+
? ring.auto_cancel_timeout_ms
|
|
10988
|
+
: ring.incoming_call_timeout_ms;
|
|
10989
|
+
// 0 means no auto-drop
|
|
10990
|
+
if (timeoutMs <= 0)
|
|
10991
|
+
return;
|
|
10992
|
+
const now = Date.now();
|
|
10993
|
+
this.deadlineAt ??= now + timeoutMs;
|
|
10994
|
+
const delayMs = Math.max(0, this.deadlineAt - now);
|
|
10995
|
+
const timers = getTimers();
|
|
10996
|
+
this.timeoutId = timers.setTimeout(() => {
|
|
10997
|
+
this.timeoutId = undefined;
|
|
10998
|
+
// A failed timeout-triggered leave can arm a fresh retry window.
|
|
10999
|
+
this.deadlineAt = undefined;
|
|
11000
|
+
if (this.stopped)
|
|
11001
|
+
return;
|
|
11002
|
+
// the call might have stopped ringing by this point, e.g. it was already
|
|
11003
|
+
// accepted and joined
|
|
11004
|
+
if (this.call.state.callingState !== exports.CallingState.RINGING)
|
|
11005
|
+
return;
|
|
11006
|
+
this.call
|
|
11007
|
+
.leave({
|
|
11008
|
+
reject: true,
|
|
11009
|
+
reason: 'timeout',
|
|
11010
|
+
message: `ringing timeout - ${isCaller
|
|
11011
|
+
? 'no one accepted'
|
|
11012
|
+
: `user didn't interact with incoming call screen`}`,
|
|
11013
|
+
})
|
|
11014
|
+
.catch((err) => {
|
|
11015
|
+
videoLoggerSystem
|
|
11016
|
+
.getLogger('RingTimeout')
|
|
11017
|
+
.error('Failed to drop the call', err);
|
|
11018
|
+
});
|
|
11019
|
+
}, delayMs);
|
|
11020
|
+
};
|
|
11021
|
+
/**
|
|
11022
|
+
* Pauses the timeout while preserving its original deadline.
|
|
11023
|
+
*/
|
|
11024
|
+
pause = () => {
|
|
11025
|
+
if (this.stopped || this.timeoutId === undefined)
|
|
11026
|
+
return;
|
|
11027
|
+
getTimers().clearTimeout(this.timeoutId);
|
|
11028
|
+
this.timeoutId = undefined;
|
|
11029
|
+
};
|
|
11030
|
+
/**
|
|
11031
|
+
* Cancels a scheduled auto-drop timeout. It cannot be armed again.
|
|
11032
|
+
*/
|
|
11033
|
+
stop = () => {
|
|
11034
|
+
if (this.stopped)
|
|
11035
|
+
return;
|
|
11036
|
+
this.stopped = true;
|
|
11037
|
+
getTimers().clearTimeout(this.timeoutId);
|
|
11038
|
+
this.timeoutId = undefined;
|
|
11039
|
+
this.deadlineAt = undefined;
|
|
11040
|
+
};
|
|
11041
|
+
}
|
|
11042
|
+
|
|
11043
|
+
/**
|
|
11044
|
+
* Decides what a ringing call should do about the current user's own accept or
|
|
11045
|
+
* reject, which may have happened on another device.
|
|
11046
|
+
*
|
|
11047
|
+
* Accepting on *this* device also lands in `accepted_by`, so an acceptance only
|
|
11048
|
+
* means another device took the call when this one is still ringing.
|
|
11049
|
+
*
|
|
11050
|
+
* Only meaningful for a ringing call; the caller checks that.
|
|
11051
|
+
*/
|
|
11052
|
+
const resolveOwnRingOutcome = ({ session, currentUserId, callingState, }) => {
|
|
11053
|
+
if (!currentUserId)
|
|
11054
|
+
return { settledByMe: false };
|
|
11055
|
+
const acceptedByMe = Boolean(session?.accepted_by[currentUserId]);
|
|
11056
|
+
const rejectedByMe = Boolean(session?.rejected_by[currentUserId]);
|
|
11057
|
+
if (!acceptedByMe && !rejectedByMe)
|
|
11058
|
+
return { settledByMe: false };
|
|
11059
|
+
const answeredElsewhere = acceptedByMe && callingState === exports.CallingState.RINGING;
|
|
11060
|
+
return {
|
|
11061
|
+
settledByMe: true,
|
|
11062
|
+
leaveReason: answeredElsewhere
|
|
11063
|
+
? 'answeredElsewhere'
|
|
11064
|
+
: rejectedByMe
|
|
11065
|
+
? 'rejected'
|
|
11066
|
+
: undefined,
|
|
11067
|
+
};
|
|
10723
11068
|
};
|
|
11069
|
+
|
|
10724
11070
|
/**
|
|
10725
11071
|
* Event handler that watches the delivery of `call.ended` Websocket event.
|
|
10726
11072
|
*/
|
|
@@ -11121,14 +11467,18 @@ const registerEventHandlers = (call, dispatcher) => {
|
|
|
11121
11467
|
* @param call the call to register event handlers for.
|
|
11122
11468
|
*/
|
|
11123
11469
|
const registerRingingCallEventHandlers = (call) => {
|
|
11124
|
-
const
|
|
11125
|
-
'
|
|
11126
|
-
|
|
11470
|
+
const reconcile = () => {
|
|
11471
|
+
reconcileRingState(call, 'ring-ws').catch((err) => {
|
|
11472
|
+
call.logger.error('Failed to reconcile the ring state', err);
|
|
11473
|
+
});
|
|
11127
11474
|
};
|
|
11128
|
-
|
|
11129
|
-
|
|
11130
|
-
|
|
11131
|
-
|
|
11475
|
+
// each event needs its own closure. `call.missed` is deliberately absent:
|
|
11476
|
+
// nothing in the reconciler acts on `missed_by`, so the auto-drop owns the
|
|
11477
|
+
// "nobody answered" case until the server-owned ring timeout lands.
|
|
11478
|
+
const eventHandlers = [
|
|
11479
|
+
call.on('call.accepted', () => reconcile()),
|
|
11480
|
+
call.on('call.rejected', () => reconcile()),
|
|
11481
|
+
];
|
|
11132
11482
|
return () => {
|
|
11133
11483
|
eventHandlers.forEach((unsubscribe) => unsubscribe());
|
|
11134
11484
|
};
|
|
@@ -15176,7 +15526,8 @@ class Call {
|
|
|
15176
15526
|
statsReporter;
|
|
15177
15527
|
sfuStatsReporter;
|
|
15178
15528
|
lastStatsOptions;
|
|
15179
|
-
|
|
15529
|
+
ringTimeout;
|
|
15530
|
+
ringStatePoller;
|
|
15180
15531
|
clientStore;
|
|
15181
15532
|
streamClient;
|
|
15182
15533
|
clientEventReporter;
|
|
@@ -15354,22 +15705,19 @@ class Call {
|
|
|
15354
15705
|
createSubscription(this.state.session$, (session) => {
|
|
15355
15706
|
if (!this.ringing)
|
|
15356
15707
|
return;
|
|
15357
|
-
const
|
|
15358
|
-
|
|
15359
|
-
|
|
15360
|
-
|
|
15361
|
-
|
|
15362
|
-
if (
|
|
15708
|
+
const { settledByMe, leaveReason } = resolveOwnRingOutcome({
|
|
15709
|
+
session,
|
|
15710
|
+
currentUserId: this.currentUserId,
|
|
15711
|
+
callingState: this.state.callingState,
|
|
15712
|
+
});
|
|
15713
|
+
if (settledByMe)
|
|
15363
15714
|
this.cancelAutoDrop();
|
|
15364
|
-
|
|
15365
|
-
|
|
15366
|
-
|
|
15367
|
-
|
|
15368
|
-
|
|
15369
|
-
|
|
15370
|
-
this.logger.error('Could not leave a call that was accepted or rejected elsewhere');
|
|
15371
|
-
});
|
|
15372
|
-
}
|
|
15715
|
+
if (!leaveReason || hasPending(this.joinLeaveConcurrencyTag))
|
|
15716
|
+
return;
|
|
15717
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(this, leaveReason);
|
|
15718
|
+
this.leave().catch(() => {
|
|
15719
|
+
this.logger.error('Could not leave a call that was accepted or rejected elsewhere');
|
|
15720
|
+
});
|
|
15373
15721
|
}));
|
|
15374
15722
|
};
|
|
15375
15723
|
handleRingingCall = () => {
|
|
@@ -15415,6 +15763,7 @@ class Call {
|
|
|
15415
15763
|
this.state.setCallingState(exports.CallingState.RINGING);
|
|
15416
15764
|
}
|
|
15417
15765
|
this.scheduleAutoDrop();
|
|
15766
|
+
this.scheduleRingStatePolling();
|
|
15418
15767
|
this.leaveCallHooks.add(registerRingingCallEventHandlers(this));
|
|
15419
15768
|
}
|
|
15420
15769
|
};
|
|
@@ -15501,6 +15850,12 @@ class Call {
|
|
|
15501
15850
|
if (this.state.callingState === exports.CallingState.LEFT) {
|
|
15502
15851
|
throw new Error('Cannot leave call that has already been left.');
|
|
15503
15852
|
}
|
|
15853
|
+
// before the first await: the calling state stays RINGING well into the
|
|
15854
|
+
// teardown, so pause both watchdogs before they can race this leave. They
|
|
15855
|
+
// keep their deadlines, so a failed leave resumes them without handing the
|
|
15856
|
+
// ring another window.
|
|
15857
|
+
this.ringTimeout?.pause();
|
|
15858
|
+
this.ringStatePoller?.pause();
|
|
15504
15859
|
await withoutConcurrency(this.joinLeaveConcurrencyTag, async () => {
|
|
15505
15860
|
const callingState = this.state.callingState;
|
|
15506
15861
|
if (callingState === exports.CallingState.LEFT) {
|
|
@@ -15590,6 +15945,7 @@ class Call {
|
|
|
15590
15945
|
this.unifiedSessionId = undefined;
|
|
15591
15946
|
this.ringingSubject.next(false);
|
|
15592
15947
|
this.cancelAutoDrop();
|
|
15948
|
+
this.cancelRingStatePolling();
|
|
15593
15949
|
this.clientStore.unregisterCall(this);
|
|
15594
15950
|
globalThis.streamRNVideoSDK?.callManager.stop({
|
|
15595
15951
|
isRingingTypeCall: this.ringing,
|
|
@@ -15628,6 +15984,15 @@ class Call {
|
|
|
15628
15984
|
this.logger.warn('Failed to dispose media engine', err);
|
|
15629
15985
|
});
|
|
15630
15986
|
}
|
|
15987
|
+
}).catch((err) => {
|
|
15988
|
+
if (!hasPending(this.joinLeaveConcurrencyTag) &&
|
|
15989
|
+
this.state.callingState === exports.CallingState.RINGING) {
|
|
15990
|
+
// resume, never re-arm: a fresh watchdog would restart the ring
|
|
15991
|
+
// window this leave was already most of the way through
|
|
15992
|
+
this.ringTimeout?.start();
|
|
15993
|
+
this.ringStatePoller?.resume();
|
|
15994
|
+
}
|
|
15995
|
+
throw err;
|
|
15631
15996
|
});
|
|
15632
15997
|
};
|
|
15633
15998
|
/**
|
|
@@ -15778,6 +16143,21 @@ class Call {
|
|
|
15778
16143
|
ring = async (data = {}) => {
|
|
15779
16144
|
return this.streamClient.post(`${this.streamClientBasePath}/ring`, data);
|
|
15780
16145
|
};
|
|
16146
|
+
/**
|
|
16147
|
+
* Returns who accepted, rejected or missed the ring for a call session.
|
|
16148
|
+
* Safe to poll: it performs no writes and emits no events.
|
|
16149
|
+
*
|
|
16150
|
+
* @param callSessionId the call session to read. Defaults to the current one.
|
|
16151
|
+
* Pass it explicitly to read a session that has already ended, as ending a
|
|
16152
|
+
* call clears its current session.
|
|
16153
|
+
*/
|
|
16154
|
+
getRingState = async (callSessionId) => {
|
|
16155
|
+
const sessionId = callSessionId ?? this.state.session?.id;
|
|
16156
|
+
if (!sessionId) {
|
|
16157
|
+
throw new Error('Cannot read the ring state: the call has no session');
|
|
16158
|
+
}
|
|
16159
|
+
return this.streamClient.get(`${this.streamClientBasePath}/ring_state`, { call_session_id: sessionId });
|
|
16160
|
+
};
|
|
15781
16161
|
/**
|
|
15782
16162
|
* A shortcut for {@link Call.get} with `notify` parameter set to `true`.
|
|
15783
16163
|
* Will send a `call.notification` event to the call members.
|
|
@@ -15818,7 +16198,7 @@ class Call {
|
|
|
15818
16198
|
*
|
|
15819
16199
|
* @returns a promise which resolves once the call join-flow has finished.
|
|
15820
16200
|
*/
|
|
15821
|
-
join = singleFlight(async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, allowOwnTracksLoopback = false, ...data } = {}) => {
|
|
16201
|
+
join = singleFlight(async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, allowOwnTracksLoopback = false, joinSource, ...data } = {}) => {
|
|
15822
16202
|
const callingState = this.state.callingState;
|
|
15823
16203
|
if ([exports.CallingState.JOINED, exports.CallingState.JOINING].includes(callingState)) {
|
|
15824
16204
|
throw new Error(`Illegal State: call.join() shall be called only once`);
|
|
@@ -15850,7 +16230,7 @@ class Call {
|
|
|
15850
16230
|
const joinData = data;
|
|
15851
16231
|
maxJoinRetries = Math.max(maxJoinRetries, 1);
|
|
15852
16232
|
try {
|
|
15853
|
-
await this.clientEventReporter.withJoinLifecycle(this.cid, 'first-attempt', async () => {
|
|
16233
|
+
await this.clientEventReporter.withJoinLifecycle(this.cid, { joinReason: 'first-attempt', joinSource }, async () => {
|
|
15854
16234
|
for (let attempt = 0; attempt < maxJoinRetries; attempt++) {
|
|
15855
16235
|
try {
|
|
15856
16236
|
this.logger.trace(`Joining call (${attempt})`, this.cid);
|
|
@@ -16576,7 +16956,7 @@ class Call {
|
|
|
16576
16956
|
const joinReason = this.reconnectReason === ReconnectReason.NETWORK_BACK_ONLINE
|
|
16577
16957
|
? 'network-available'
|
|
16578
16958
|
: 'full-rejoin';
|
|
16579
|
-
await this.clientEventReporter.withJoinLifecycle(this.cid, joinReason, () => this.doJoin(this.joinCallData));
|
|
16959
|
+
await this.clientEventReporter.withJoinLifecycle(this.cid, { joinReason }, () => this.doJoin(this.joinCallData));
|
|
16580
16960
|
await this.restorePublishedTracks();
|
|
16581
16961
|
this.restoreSubscribedTracks();
|
|
16582
16962
|
this.sfuStatsReporter?.sendReconnectionTime(WebsocketReconnectStrategy.REJOIN, (Date.now() - reconnectStartTime) / 1000);
|
|
@@ -16600,7 +16980,7 @@ class Call {
|
|
|
16600
16980
|
const migrationTask = makeSafePromise(currentSfuClient.enterMigration());
|
|
16601
16981
|
try {
|
|
16602
16982
|
const currentSfu = currentSfuClient.edgeName;
|
|
16603
|
-
await this.clientEventReporter.withJoinLifecycle(this.cid, 'migration', () => this.doJoin({
|
|
16983
|
+
await this.clientEventReporter.withJoinLifecycle(this.cid, { joinReason: 'migration' }, () => this.doJoin({
|
|
16604
16984
|
...this.joinCallData,
|
|
16605
16985
|
migrating_from: currentSfu,
|
|
16606
16986
|
migrating_from_list: [currentSfu],
|
|
@@ -17373,40 +17753,36 @@ class Call {
|
|
|
17373
17753
|
*/
|
|
17374
17754
|
scheduleAutoDrop = () => {
|
|
17375
17755
|
this.cancelAutoDrop();
|
|
17376
|
-
|
|
17377
|
-
|
|
17378
|
-
return;
|
|
17379
|
-
// ignore if the call is not ringing
|
|
17380
|
-
if (this.state.callingState !== exports.CallingState.RINGING)
|
|
17381
|
-
return;
|
|
17382
|
-
const timeoutInMs = this.isCreatedByMe
|
|
17383
|
-
? settings.ring.auto_cancel_timeout_ms
|
|
17384
|
-
: settings.ring.incoming_call_timeout_ms;
|
|
17385
|
-
// 0 means no auto-drop
|
|
17386
|
-
if (timeoutInMs <= 0)
|
|
17387
|
-
return;
|
|
17388
|
-
this.dropTimeout = setTimeout(() => {
|
|
17389
|
-
// the call might have stopped ringing by this point,
|
|
17390
|
-
// e.g. it was already accepted and joined
|
|
17391
|
-
if (this.state.callingState !== exports.CallingState.RINGING)
|
|
17392
|
-
return;
|
|
17393
|
-
this.leave({
|
|
17394
|
-
reject: true,
|
|
17395
|
-
reason: 'timeout',
|
|
17396
|
-
message: `ringing timeout - ${this.isCreatedByMe
|
|
17397
|
-
? 'no one accepted'
|
|
17398
|
-
: `user didn't interact with incoming call screen`}`,
|
|
17399
|
-
}).catch((err) => {
|
|
17400
|
-
this.logger.error('Failed to drop call', err);
|
|
17401
|
-
});
|
|
17402
|
-
}, timeoutInMs);
|
|
17756
|
+
this.ringTimeout = new RingTimeout(this);
|
|
17757
|
+
this.ringTimeout.start();
|
|
17403
17758
|
};
|
|
17404
17759
|
/**
|
|
17405
17760
|
* Cancels a scheduled auto-drop timeout.
|
|
17406
17761
|
*/
|
|
17407
17762
|
cancelAutoDrop = () => {
|
|
17408
|
-
|
|
17409
|
-
this.
|
|
17763
|
+
this.ringTimeout?.stop();
|
|
17764
|
+
this.ringTimeout = undefined;
|
|
17765
|
+
};
|
|
17766
|
+
/**
|
|
17767
|
+
* Starts polling for the ring outcome. Applicable only to ringing calls the
|
|
17768
|
+
* current user created.
|
|
17769
|
+
*/
|
|
17770
|
+
scheduleRingStatePolling = () => {
|
|
17771
|
+
this.cancelRingStatePolling();
|
|
17772
|
+
if (!this.isCreatedByMe)
|
|
17773
|
+
return;
|
|
17774
|
+
const options = this.streamClient.options.ringStatePolling;
|
|
17775
|
+
if (options === false)
|
|
17776
|
+
return;
|
|
17777
|
+
this.ringStatePoller = new RingStatePoller(this, options);
|
|
17778
|
+
this.ringStatePoller.start();
|
|
17779
|
+
};
|
|
17780
|
+
/**
|
|
17781
|
+
* Cancels the ring state polling.
|
|
17782
|
+
*/
|
|
17783
|
+
cancelRingStatePolling = () => {
|
|
17784
|
+
this.ringStatePoller?.stop();
|
|
17785
|
+
this.ringStatePoller = undefined;
|
|
17410
17786
|
};
|
|
17411
17787
|
/**
|
|
17412
17788
|
* Retrieves the list of recordings for the current call or call session.
|
|
@@ -19077,11 +19453,11 @@ class StreamClient {
|
|
|
19077
19453
|
return await this.wsConnection.connect(this.defaultWSTimeout);
|
|
19078
19454
|
};
|
|
19079
19455
|
getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
|
|
19080
|
-
"1.
|
|
19456
|
+
"1.60.0";
|
|
19081
19457
|
getUserAgent = () => {
|
|
19082
19458
|
if (!this.cachedUserAgent) {
|
|
19083
19459
|
const { clientAppIdentifier = {} } = this.options;
|
|
19084
|
-
const { sdkName = 'js', sdkVersion = "1.
|
|
19460
|
+
const { sdkName = 'js', sdkVersion = "1.60.0", ...extras } = clientAppIdentifier;
|
|
19085
19461
|
this.cachedUserAgent = [
|
|
19086
19462
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
19087
19463
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|
|
@@ -19214,6 +19590,7 @@ class ClientEventReporter {
|
|
|
19214
19590
|
callContexts = new Map();
|
|
19215
19591
|
joinAttemptIds = new Map();
|
|
19216
19592
|
joinReasons = new Map();
|
|
19593
|
+
joinSources = new Map();
|
|
19217
19594
|
coordinatorPairs = new Map();
|
|
19218
19595
|
wsPairs = new Map();
|
|
19219
19596
|
peerConnectionPairs = new Map();
|
|
@@ -19328,6 +19705,7 @@ class ClientEventReporter {
|
|
|
19328
19705
|
this.callContexts.delete(cid);
|
|
19329
19706
|
this.joinAttemptIds.delete(cid);
|
|
19330
19707
|
this.joinReasons.delete(cid);
|
|
19708
|
+
this.joinSources.delete(cid);
|
|
19331
19709
|
this.coordinatorPairs.delete(cid);
|
|
19332
19710
|
this.wsPairs.delete(cid);
|
|
19333
19711
|
this.firstFrameReported.delete(`${cid}:FirstVideoFrame`);
|
|
@@ -19352,7 +19730,12 @@ class ClientEventReporter {
|
|
|
19352
19730
|
this.logger.warn('Failed to start join correlation', err);
|
|
19353
19731
|
}
|
|
19354
19732
|
};
|
|
19355
|
-
withJoinLifecycle = async (cid,
|
|
19733
|
+
withJoinLifecycle = async (cid, options, op) => {
|
|
19734
|
+
const { joinReason, joinSource } = options;
|
|
19735
|
+
if (joinSource)
|
|
19736
|
+
this.joinSources.set(cid, joinSource);
|
|
19737
|
+
else
|
|
19738
|
+
this.joinSources.delete(cid);
|
|
19356
19739
|
this.startCorrelation(cid, joinReason);
|
|
19357
19740
|
try {
|
|
19358
19741
|
return await op();
|
|
@@ -19361,6 +19744,9 @@ class ClientEventReporter {
|
|
|
19361
19744
|
this.closeCallPairs(cid);
|
|
19362
19745
|
throw err;
|
|
19363
19746
|
}
|
|
19747
|
+
finally {
|
|
19748
|
+
this.joinSources.delete(cid);
|
|
19749
|
+
}
|
|
19364
19750
|
};
|
|
19365
19751
|
track = async (cid, stage, op) => {
|
|
19366
19752
|
this.beginAttempt(cid, stage);
|
|
@@ -19484,6 +19870,7 @@ class ClientEventReporter {
|
|
|
19484
19870
|
startedAt: Date.now(),
|
|
19485
19871
|
joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
|
|
19486
19872
|
joinReasonSnapshot: this.joinReasons.get(cid),
|
|
19873
|
+
joinSourceSnapshot: this.joinSources.get(cid),
|
|
19487
19874
|
};
|
|
19488
19875
|
this.coordinatorPairs.set(cid, pair);
|
|
19489
19876
|
this.sendForCall(cid, {
|
|
@@ -19491,6 +19878,7 @@ class ClientEventReporter {
|
|
|
19491
19878
|
...(pair.joinReasonSnapshot && {
|
|
19492
19879
|
join_reason: pair.joinReasonSnapshot,
|
|
19493
19880
|
}),
|
|
19881
|
+
...(pair.joinSourceSnapshot && { source: pair.joinSourceSnapshot }),
|
|
19494
19882
|
event_type: 'initiated',
|
|
19495
19883
|
});
|
|
19496
19884
|
}
|
|
@@ -19505,6 +19893,7 @@ class ClientEventReporter {
|
|
|
19505
19893
|
...this.buildCommon(cid, 'CoordinatorJoin', pair),
|
|
19506
19894
|
...this.sessionIdField(cid),
|
|
19507
19895
|
...(pair.joinReasonSnapshot && { join_reason: pair.joinReasonSnapshot }),
|
|
19896
|
+
...(pair.joinSourceSnapshot && { source: pair.joinSourceSnapshot }),
|
|
19508
19897
|
event_type: 'completed',
|
|
19509
19898
|
outcome: 'success',
|
|
19510
19899
|
retry_count_attempt: pair.attempts - 1,
|
|
@@ -19523,6 +19912,7 @@ class ClientEventReporter {
|
|
|
19523
19912
|
...this.buildCommon(cid, 'CoordinatorJoin', pair),
|
|
19524
19913
|
...this.sessionIdField(cid),
|
|
19525
19914
|
...(pair.joinReasonSnapshot && { join_reason: pair.joinReasonSnapshot }),
|
|
19915
|
+
...(pair.joinSourceSnapshot && { source: pair.joinSourceSnapshot }),
|
|
19526
19916
|
event_type: 'completed',
|
|
19527
19917
|
outcome: 'failure',
|
|
19528
19918
|
retry_count_attempt: pair.attempts - 1,
|