@stream-io/video-client 1.59.0 → 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 +14 -0
- package/dist/index.browser.es.js +544 -111
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +544 -110
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +544 -111
- 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/coordinator/connection/utils.d.ts +10 -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/sorting/participants.d.ts +13 -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/__tests__/StreamVideoClient.ringing.test.ts +32 -20
- package/src/coordinator/connection/__tests__/eventListenerIsolation.test.ts +115 -0
- package/src/coordinator/connection/client.ts +3 -2
- package/src/coordinator/connection/types.ts +19 -0
- package/src/coordinator/connection/utils.ts +28 -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/sorting/__tests__/presets.test.ts +188 -3
- package/src/sorting/__tests__/sorting.test.ts +22 -1
- package/src/sorting/participants.ts +20 -0
- package/src/sorting/presets.ts +4 -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 +61 -2
|
@@ -1,16 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { fromPartial } from '@total-typescript/shoehorn';
|
|
3
2
|
import { CallingState, StreamVideoWriteableStateStore } from '../../store';
|
|
3
|
+
import { watchCallEnded, watchSfuCallEnded } from '../call';
|
|
4
4
|
import {
|
|
5
|
-
watchCallAccepted,
|
|
6
|
-
watchCallEnded,
|
|
7
|
-
watchCallRejected,
|
|
8
|
-
watchSfuCallEnded,
|
|
9
|
-
} from '../call';
|
|
10
|
-
import {
|
|
11
|
-
CallAcceptedEvent,
|
|
12
5
|
CallEndedEvent,
|
|
13
|
-
CallResponse,
|
|
14
6
|
OwnCapability,
|
|
15
7
|
RejectCallResponse,
|
|
16
8
|
} from '../../gen/coordinator';
|
|
@@ -20,189 +12,7 @@ import { ClientEventReporter } from '../../reporting';
|
|
|
20
12
|
import { SfuEvent } from '../../gen/video/sfu/event/events';
|
|
21
13
|
import { CallEndedReason } from '../../gen/video/sfu/models/models';
|
|
22
14
|
|
|
23
|
-
describe('Call
|
|
24
|
-
describe(`call.accepted`, () => {
|
|
25
|
-
it(`will ignore events from the current user`, async () => {
|
|
26
|
-
const call = fakeCall();
|
|
27
|
-
vi.spyOn(call, 'join');
|
|
28
|
-
const handler = watchCallAccepted(call);
|
|
29
|
-
const event: CallAcceptedEvent = {
|
|
30
|
-
type: 'call.accepted',
|
|
31
|
-
// @ts-expect-error incomplete data
|
|
32
|
-
user: { id: 'test-user-id' },
|
|
33
|
-
};
|
|
34
|
-
await handler(event);
|
|
35
|
-
|
|
36
|
-
expect(call.join).not.toHaveBeenCalled();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it(`will join the call for the caller if atleast one callee has accepted`, async () => {
|
|
40
|
-
const call = fakeCall({ currentUserId: 'test-user' });
|
|
41
|
-
vi.spyOn(call, 'join').mockImplementation(async () => {
|
|
42
|
-
console.log(`TEST: join() called`);
|
|
43
|
-
});
|
|
44
|
-
const handler = watchCallAccepted(call);
|
|
45
|
-
const event: CallAcceptedEvent = {
|
|
46
|
-
type: 'call.accepted',
|
|
47
|
-
// @ts-expect-error incomplete data
|
|
48
|
-
user: { id: 'test-user-id-callee' },
|
|
49
|
-
// @ts-expect-error incomplete data
|
|
50
|
-
call: { created_by: { id: 'test-user' } },
|
|
51
|
-
};
|
|
52
|
-
await handler(event);
|
|
53
|
-
|
|
54
|
-
expect(call.join).toHaveBeenCalled();
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('will not join the call for the other callee automatically when someone accepts', async () => {
|
|
59
|
-
const call = fakeCall({ currentUserId: 'test-user-id-callee-2' });
|
|
60
|
-
vi.spyOn(call, 'join').mockImplementation(async () => {
|
|
61
|
-
console.log(`TEST: join() called`);
|
|
62
|
-
});
|
|
63
|
-
const handler = watchCallAccepted(call);
|
|
64
|
-
const event: CallAcceptedEvent = {
|
|
65
|
-
type: 'call.accepted',
|
|
66
|
-
// @ts-expect-error incomplete data
|
|
67
|
-
user: { id: 'test-user-id-callee-1' },
|
|
68
|
-
// @ts-expect-error incomplete data
|
|
69
|
-
call: { created_by: { id: 'test-user-id-caller' } },
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
await handler(event);
|
|
73
|
-
|
|
74
|
-
expect(call.join).not.toHaveBeenCalled();
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
describe(`call.rejected`, () => {
|
|
78
|
-
it(`caller will leave the call if all callees have rejected`, async () => {
|
|
79
|
-
const call = fakeCall({ currentUserId: 'm1' });
|
|
80
|
-
call.state.updateFromCallResponse({
|
|
81
|
-
...fakeMetadata(),
|
|
82
|
-
// @ts-expect-error type issue
|
|
83
|
-
created_by: { id: 'm1' },
|
|
84
|
-
});
|
|
85
|
-
call.state.setMembers([
|
|
86
|
-
// @ts-expect-error incomplete data
|
|
87
|
-
{ user_id: 'm1' },
|
|
88
|
-
// @ts-expect-error incomplete data
|
|
89
|
-
{ user_id: 'm2' },
|
|
90
|
-
// @ts-expect-error incomplete data
|
|
91
|
-
{ user_id: 'm3' },
|
|
92
|
-
]);
|
|
93
|
-
call.state.setCallingState(CallingState.RINGING);
|
|
94
|
-
vi.spyOn(call, 'leave').mockImplementation(async () => {
|
|
95
|
-
console.log(`TEST: leave() called`);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
const handler = watchCallRejected(call);
|
|
99
|
-
// all members reject the call
|
|
100
|
-
await handler({
|
|
101
|
-
type: 'call.rejected',
|
|
102
|
-
// @ts-expect-error type issue
|
|
103
|
-
user: {
|
|
104
|
-
id: 'm2',
|
|
105
|
-
},
|
|
106
|
-
call: {
|
|
107
|
-
// @ts-expect-error type issue
|
|
108
|
-
created_by: {
|
|
109
|
-
id: 'm1',
|
|
110
|
-
},
|
|
111
|
-
// @ts-expect-error type issue
|
|
112
|
-
session: {
|
|
113
|
-
rejected_by: {
|
|
114
|
-
m2: new Date().toISOString(),
|
|
115
|
-
m3: new Date().toISOString(),
|
|
116
|
-
},
|
|
117
|
-
},
|
|
118
|
-
},
|
|
119
|
-
});
|
|
120
|
-
expect(call.leave).toHaveBeenCalledWith({
|
|
121
|
-
reject: true,
|
|
122
|
-
reason: 'cancel',
|
|
123
|
-
message: 'ring: everyone rejected',
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it(`caller will not leave the call if only one callee rejects`, async () => {
|
|
128
|
-
const call = fakeCall();
|
|
129
|
-
call.state.updateFromCallResponse({
|
|
130
|
-
...fakeMetadata(),
|
|
131
|
-
// @ts-expect-error type issue
|
|
132
|
-
created_by: { id: 'm0' },
|
|
133
|
-
});
|
|
134
|
-
// @ts-expect-error incomplete data
|
|
135
|
-
call.state.setMembers([{ user_id: 'm1' }, { user_id: 'm2' }]);
|
|
136
|
-
vi.spyOn(call, 'leave').mockImplementation(async () => {
|
|
137
|
-
console.log(`TEST: leave() called`);
|
|
138
|
-
});
|
|
139
|
-
const handler = watchCallRejected(call);
|
|
140
|
-
|
|
141
|
-
// only one member rejects the call
|
|
142
|
-
const event: CallAcceptedEvent = {
|
|
143
|
-
type: 'call.rejected',
|
|
144
|
-
// @ts-expect-error type issue
|
|
145
|
-
user: {
|
|
146
|
-
id: 'm2',
|
|
147
|
-
},
|
|
148
|
-
call: {
|
|
149
|
-
// @ts-expect-error type issue
|
|
150
|
-
created_by: {
|
|
151
|
-
id: 'm0',
|
|
152
|
-
},
|
|
153
|
-
// @ts-expect-error type issue
|
|
154
|
-
session: {
|
|
155
|
-
rejected_by: {
|
|
156
|
-
m2: new Date().toISOString(),
|
|
157
|
-
},
|
|
158
|
-
},
|
|
159
|
-
},
|
|
160
|
-
};
|
|
161
|
-
await handler(event);
|
|
162
|
-
|
|
163
|
-
expect(call.leave).not.toHaveBeenCalled();
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it('callee will leave the call if caller rejects', async () => {
|
|
167
|
-
const call = fakeCall({ currentUserId: 'm1' });
|
|
168
|
-
call.state.updateFromCallResponse({
|
|
169
|
-
...fakeMetadata(),
|
|
170
|
-
// @ts-expect-error type issue
|
|
171
|
-
created_by: { id: 'm0' },
|
|
172
|
-
});
|
|
173
|
-
// @ts-expect-error incomplete data
|
|
174
|
-
call.state.setMembers([{ user_id: 'm1' }, { user_id: 'm2' }]);
|
|
175
|
-
vi.spyOn(call, 'leave').mockImplementation(async () => {
|
|
176
|
-
console.log(`TEST: leave() called`);
|
|
177
|
-
});
|
|
178
|
-
const handler = watchCallRejected(call);
|
|
179
|
-
|
|
180
|
-
// only one member rejects the call
|
|
181
|
-
const event: CallAcceptedEvent = {
|
|
182
|
-
type: 'call.rejected',
|
|
183
|
-
// @ts-expect-error type issue
|
|
184
|
-
user: {
|
|
185
|
-
id: 'm0',
|
|
186
|
-
},
|
|
187
|
-
call: {
|
|
188
|
-
// @ts-expect-error type issue
|
|
189
|
-
created_by: {
|
|
190
|
-
id: 'm0',
|
|
191
|
-
},
|
|
192
|
-
// @ts-expect-error type issue
|
|
193
|
-
session: {
|
|
194
|
-
rejected_by: {
|
|
195
|
-
m0: new Date().toISOString(),
|
|
196
|
-
},
|
|
197
|
-
},
|
|
198
|
-
},
|
|
199
|
-
};
|
|
200
|
-
await handler(event);
|
|
201
|
-
|
|
202
|
-
expect(call.leave).toHaveBeenCalled();
|
|
203
|
-
});
|
|
204
|
-
});
|
|
205
|
-
|
|
15
|
+
describe('Call lifecycle events', () => {
|
|
206
16
|
describe(`call.ended`, () => {
|
|
207
17
|
it(`will leave the call unless joined`, async () => {
|
|
208
18
|
const call = fakeCall();
|
|
@@ -377,28 +187,3 @@ const fakeCall = ({ ring = true, currentUserId = 'test-user-id' } = {}) => {
|
|
|
377
187
|
ringing: ring,
|
|
378
188
|
});
|
|
379
189
|
};
|
|
380
|
-
|
|
381
|
-
const fakeMetadata = (): CallResponse => {
|
|
382
|
-
return fromPartial({
|
|
383
|
-
id: '12345',
|
|
384
|
-
type: 'development',
|
|
385
|
-
cid: 'development:12345',
|
|
386
|
-
|
|
387
|
-
created_by: {
|
|
388
|
-
id: 'test-user-id',
|
|
389
|
-
},
|
|
390
|
-
blocked_user_ids: [],
|
|
391
|
-
egress: {},
|
|
392
|
-
|
|
393
|
-
settings: {
|
|
394
|
-
ring: {
|
|
395
|
-
auto_cancel_timeout_ms: 30000,
|
|
396
|
-
incoming_call_timeout_ms: 30000,
|
|
397
|
-
missed_call_timeout_ms: 30000,
|
|
398
|
-
},
|
|
399
|
-
screensharing: {
|
|
400
|
-
target_resolution: undefined,
|
|
401
|
-
},
|
|
402
|
-
},
|
|
403
|
-
});
|
|
404
|
-
};
|
package/src/events/call.ts
CHANGED
|
@@ -1,81 +1,9 @@
|
|
|
1
1
|
import { CallingState } from '../store';
|
|
2
2
|
import { Call } from '../Call';
|
|
3
|
-
import {
|
|
4
|
-
CallAcceptedEvent,
|
|
5
|
-
CallRejectedEvent,
|
|
6
|
-
OwnCapability,
|
|
7
|
-
} from '../gen/coordinator';
|
|
3
|
+
import { OwnCapability } from '../gen/coordinator';
|
|
8
4
|
import { CallEnded } from '../gen/video/sfu/event/events';
|
|
9
5
|
import { CallEndedReason } from '../gen/video/sfu/models/models';
|
|
10
6
|
|
|
11
|
-
/**
|
|
12
|
-
* Event handler that watched the delivery of `call.accepted`.
|
|
13
|
-
* Once the event is received, the call is joined.
|
|
14
|
-
*/
|
|
15
|
-
export const watchCallAccepted = (call: Call) => {
|
|
16
|
-
return async function onCallAccepted(event: CallAcceptedEvent) {
|
|
17
|
-
// We want to discard the event if it's from the current user
|
|
18
|
-
if (event.user.id === call.currentUserId) return;
|
|
19
|
-
const { state } = call;
|
|
20
|
-
if (
|
|
21
|
-
event.call.created_by.id === call.currentUserId &&
|
|
22
|
-
state.callingState === CallingState.RINGING
|
|
23
|
-
) {
|
|
24
|
-
await call.join();
|
|
25
|
-
}
|
|
26
|
-
};
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Event handler that watches delivery of `call.rejected` Websocket event.
|
|
31
|
-
* Once the event is received, the call is left.
|
|
32
|
-
*/
|
|
33
|
-
export const watchCallRejected = (call: Call) => {
|
|
34
|
-
return async function onCallRejected(event: CallRejectedEvent) {
|
|
35
|
-
// We want to discard the event if it's from the current user
|
|
36
|
-
if (event.user.id === call.currentUserId) return;
|
|
37
|
-
const { call: eventCall } = event;
|
|
38
|
-
const { session: callSession } = eventCall;
|
|
39
|
-
|
|
40
|
-
if (!callSession) {
|
|
41
|
-
call.logger.warn(
|
|
42
|
-
'No call session provided. Ignoring call.rejected event.',
|
|
43
|
-
event,
|
|
44
|
-
);
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const rejectedBy = callSession.rejected_by;
|
|
49
|
-
const { members, callingState } = call.state;
|
|
50
|
-
if (callingState !== CallingState.RINGING) {
|
|
51
|
-
call.logger.info(
|
|
52
|
-
'Call is not in ringing mode (it is either accepted or rejected already). Ignoring call.rejected event.',
|
|
53
|
-
event,
|
|
54
|
-
);
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
if (call.isCreatedByMe) {
|
|
58
|
-
const everyoneElseRejected = members
|
|
59
|
-
.filter((m) => m.user_id !== call.currentUserId)
|
|
60
|
-
.every((m) => rejectedBy[m.user_id]);
|
|
61
|
-
if (everyoneElseRejected) {
|
|
62
|
-
call.logger.info('everyone rejected, leaving the call');
|
|
63
|
-
await call.leave({
|
|
64
|
-
reject: true,
|
|
65
|
-
reason: 'cancel',
|
|
66
|
-
message: 'ring: everyone rejected',
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
} else {
|
|
70
|
-
if (rejectedBy[eventCall.created_by.id]) {
|
|
71
|
-
call.logger.info('call creator rejected, leaving call');
|
|
72
|
-
globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
|
|
73
|
-
await call.leave({ message: 'ring: creator rejected' });
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
};
|
|
78
|
-
|
|
79
7
|
/**
|
|
80
8
|
* Event handler that watches the delivery of `call.ended` Websocket event.
|
|
81
9
|
*/
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { Call } from '../Call';
|
|
2
|
+
import { reconcileRingState } from '../ringing';
|
|
2
3
|
import { Dispatcher } from '../rtc';
|
|
3
4
|
import {
|
|
4
5
|
handleRemoteSoftMute,
|
|
5
6
|
watchAudioLevelChanged,
|
|
6
|
-
watchCallAccepted,
|
|
7
7
|
watchCallEnded,
|
|
8
8
|
watchCallGrantsUpdated,
|
|
9
|
-
watchCallRejected,
|
|
10
9
|
watchConnectionQualityChanged,
|
|
11
10
|
watchDominantSpeakerChanged,
|
|
12
11
|
watchInboundStateNotification,
|
|
@@ -21,16 +20,6 @@ import {
|
|
|
21
20
|
watchTrackPublished,
|
|
22
21
|
watchTrackUnpublished,
|
|
23
22
|
} from '../events';
|
|
24
|
-
import {
|
|
25
|
-
AllCallEvents,
|
|
26
|
-
AllClientCallEvents,
|
|
27
|
-
CallEventListener,
|
|
28
|
-
} from '../coordinator/connection/types';
|
|
29
|
-
|
|
30
|
-
type RingCallEvents = Extract<
|
|
31
|
-
AllClientCallEvents,
|
|
32
|
-
'call.accepted' | 'call.rejected' | 'call.missed'
|
|
33
|
-
>;
|
|
34
23
|
|
|
35
24
|
/**
|
|
36
25
|
* Registers the default event handlers for a call during its lifecycle.
|
|
@@ -83,19 +72,19 @@ export const registerEventHandlers = (call: Call, dispatcher: Dispatcher) => {
|
|
|
83
72
|
* @param call the call to register event handlers for.
|
|
84
73
|
*/
|
|
85
74
|
export const registerRingingCallEventHandlers = (call: Call) => {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
call
|
|
89
|
-
)
|
|
90
|
-
} = {
|
|
91
|
-
'call.accepted': watchCallAccepted(call),
|
|
92
|
-
'call.rejected': watchCallRejected(call),
|
|
75
|
+
const reconcile = () => {
|
|
76
|
+
reconcileRingState(call, 'ring-ws').catch((err) => {
|
|
77
|
+
call.logger.error('Failed to reconcile the ring state', err);
|
|
78
|
+
});
|
|
93
79
|
};
|
|
94
80
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
81
|
+
// each event needs its own closure. `call.missed` is deliberately absent:
|
|
82
|
+
// nothing in the reconciler acts on `missed_by`, so the auto-drop owns the
|
|
83
|
+
// "nobody answered" case until the server-owned ring timeout lands.
|
|
84
|
+
const eventHandlers = [
|
|
85
|
+
call.on('call.accepted', () => reconcile()),
|
|
86
|
+
call.on('call.rejected', () => reconcile()),
|
|
87
|
+
];
|
|
99
88
|
|
|
100
89
|
return () => {
|
|
101
90
|
eventHandlers.forEach((unsubscribe) => unsubscribe());
|
|
@@ -5130,6 +5130,73 @@ export interface GetCallResponse {
|
|
|
5130
5130
|
*/
|
|
5131
5131
|
own_capabilities: Array<OwnCapability>;
|
|
5132
5132
|
}
|
|
5133
|
+
/**
|
|
5134
|
+
* The ring state of a call session: who accepted, rejected, or missed the ring.
|
|
5135
|
+
* @export
|
|
5136
|
+
* @interface GetCallRingStateResponse
|
|
5137
|
+
*/
|
|
5138
|
+
export interface GetCallRingStateResponse {
|
|
5139
|
+
/**
|
|
5140
|
+
* Users that accepted the call, mapped to when they accepted
|
|
5141
|
+
* @type {{ [key: string]: string; }}
|
|
5142
|
+
* @memberof GetCallRingStateResponse
|
|
5143
|
+
*/
|
|
5144
|
+
accepted_by: { [key: string]: string };
|
|
5145
|
+
/**
|
|
5146
|
+
* The CID of the call
|
|
5147
|
+
* @type {string}
|
|
5148
|
+
* @memberof GetCallRingStateResponse
|
|
5149
|
+
*/
|
|
5150
|
+
call_cid: string;
|
|
5151
|
+
/**
|
|
5152
|
+
* When the call ended
|
|
5153
|
+
* @type {string}
|
|
5154
|
+
* @memberof GetCallRingStateResponse
|
|
5155
|
+
*/
|
|
5156
|
+
call_ended_at?: string;
|
|
5157
|
+
/**
|
|
5158
|
+
* The user that created the call, i.e. the caller
|
|
5159
|
+
* @type {string}
|
|
5160
|
+
* @memberof GetCallRingStateResponse
|
|
5161
|
+
*/
|
|
5162
|
+
created_by_user_id: string;
|
|
5163
|
+
/**
|
|
5164
|
+
* Duration of the request in milliseconds
|
|
5165
|
+
* @type {string}
|
|
5166
|
+
* @memberof GetCallRingStateResponse
|
|
5167
|
+
*/
|
|
5168
|
+
duration: string;
|
|
5169
|
+
/**
|
|
5170
|
+
* Users that missed the call, mapped to when they were marked as missed
|
|
5171
|
+
* @type {{ [key: string]: string; }}
|
|
5172
|
+
* @memberof GetCallRingStateResponse
|
|
5173
|
+
*/
|
|
5174
|
+
missed_by: { [key: string]: string };
|
|
5175
|
+
/**
|
|
5176
|
+
* Users that rejected the call, mapped to when they rejected
|
|
5177
|
+
* @type {{ [key: string]: string; }}
|
|
5178
|
+
* @memberof GetCallRingStateResponse
|
|
5179
|
+
*/
|
|
5180
|
+
rejected_by: { [key: string]: string };
|
|
5181
|
+
/**
|
|
5182
|
+
* When the call session ended
|
|
5183
|
+
* @type {string}
|
|
5184
|
+
* @memberof GetCallRingStateResponse
|
|
5185
|
+
*/
|
|
5186
|
+
session_ended_at?: string;
|
|
5187
|
+
/**
|
|
5188
|
+
* The call session this state belongs to, empty when the call has never rung
|
|
5189
|
+
* @type {string}
|
|
5190
|
+
* @memberof GetCallRingStateResponse
|
|
5191
|
+
*/
|
|
5192
|
+
session_id: string;
|
|
5193
|
+
/**
|
|
5194
|
+
* When the call session started
|
|
5195
|
+
* @type {string}
|
|
5196
|
+
* @memberof GetCallRingStateResponse
|
|
5197
|
+
*/
|
|
5198
|
+
session_started_at?: string;
|
|
5199
|
+
}
|
|
5133
5200
|
/**
|
|
5134
5201
|
* Basic response information
|
|
5135
5202
|
* @export
|
|
@@ -45,6 +45,13 @@ export type ReportedIceState = 'CONNECTED' | 'FAILED' | 'NOT_CONNECTED';
|
|
|
45
45
|
export type JoinReason =
|
|
46
46
|
'first-attempt' | 'network-available' | 'migration' | 'full-rejoin';
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* What triggered an automatic join, reported as `source` on the call's
|
|
50
|
+
* CoordinatorJoin events. `ring-ws` is a ring WebSocket event, `ring-poll-api`
|
|
51
|
+
* is the ring state poller.
|
|
52
|
+
*/
|
|
53
|
+
export type JoinSource = 'ring-ws' | 'ring-poll-api';
|
|
54
|
+
|
|
48
55
|
export type ClientEventStandardCode =
|
|
49
56
|
| 'CLIENT_ABORTED'
|
|
50
57
|
| 'BACKEND_LEAVE'
|
|
@@ -67,6 +74,9 @@ export type ClientEventReporterOptions = {
|
|
|
67
74
|
enabled?: boolean;
|
|
68
75
|
};
|
|
69
76
|
|
|
77
|
+
// TODO OL: update OpenAPI
|
|
78
|
+
type ReportedClientEvent = ClientEvent & { source?: JoinSource };
|
|
79
|
+
|
|
70
80
|
type StageError = {
|
|
71
81
|
reason: string;
|
|
72
82
|
code: string;
|
|
@@ -78,6 +88,7 @@ type StagePairState = {
|
|
|
78
88
|
startedAt: number;
|
|
79
89
|
joinAttemptIdSnapshot?: string;
|
|
80
90
|
joinReasonSnapshot?: JoinReason;
|
|
91
|
+
joinSourceSnapshot?: JoinSource;
|
|
81
92
|
userIdSnapshot?: string;
|
|
82
93
|
lastError?: StageError;
|
|
83
94
|
};
|
|
@@ -104,6 +115,7 @@ export class ClientEventReporter {
|
|
|
104
115
|
private callContexts = new Map<string, CallReportContext>();
|
|
105
116
|
private joinAttemptIds = new Map<string, string>();
|
|
106
117
|
private joinReasons = new Map<string, JoinReason>();
|
|
118
|
+
private joinSources = new Map<string, JoinSource>();
|
|
107
119
|
private coordinatorPairs = new Map<string, StagePairState>();
|
|
108
120
|
private wsPairs = new Map<string, StagePairState>();
|
|
109
121
|
|
|
@@ -238,6 +250,7 @@ export class ClientEventReporter {
|
|
|
238
250
|
this.callContexts.delete(cid);
|
|
239
251
|
this.joinAttemptIds.delete(cid);
|
|
240
252
|
this.joinReasons.delete(cid);
|
|
253
|
+
this.joinSources.delete(cid);
|
|
241
254
|
this.coordinatorPairs.delete(cid);
|
|
242
255
|
this.wsPairs.delete(cid);
|
|
243
256
|
|
|
@@ -269,15 +282,22 @@ export class ClientEventReporter {
|
|
|
269
282
|
|
|
270
283
|
withJoinLifecycle = async <T>(
|
|
271
284
|
cid: string,
|
|
272
|
-
joinReason: JoinReason,
|
|
285
|
+
options: { joinReason: JoinReason; joinSource?: JoinSource },
|
|
273
286
|
op: () => Promise<T>,
|
|
274
287
|
): Promise<T> => {
|
|
288
|
+
const { joinReason, joinSource } = options;
|
|
289
|
+
|
|
290
|
+
if (joinSource) this.joinSources.set(cid, joinSource);
|
|
291
|
+
else this.joinSources.delete(cid);
|
|
292
|
+
|
|
275
293
|
this.startCorrelation(cid, joinReason);
|
|
276
294
|
try {
|
|
277
295
|
return await op();
|
|
278
296
|
} catch (err) {
|
|
279
297
|
this.closeCallPairs(cid);
|
|
280
298
|
throw err;
|
|
299
|
+
} finally {
|
|
300
|
+
this.joinSources.delete(cid);
|
|
281
301
|
}
|
|
282
302
|
};
|
|
283
303
|
|
|
@@ -436,6 +456,7 @@ export class ClientEventReporter {
|
|
|
436
456
|
startedAt: Date.now(),
|
|
437
457
|
joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
|
|
438
458
|
joinReasonSnapshot: this.joinReasons.get(cid),
|
|
459
|
+
joinSourceSnapshot: this.joinSources.get(cid),
|
|
439
460
|
};
|
|
440
461
|
this.coordinatorPairs.set(cid, pair);
|
|
441
462
|
this.sendForCall(cid, {
|
|
@@ -443,6 +464,7 @@ export class ClientEventReporter {
|
|
|
443
464
|
...(pair.joinReasonSnapshot && {
|
|
444
465
|
join_reason: pair.joinReasonSnapshot,
|
|
445
466
|
}),
|
|
467
|
+
...(pair.joinSourceSnapshot && { source: pair.joinSourceSnapshot }),
|
|
446
468
|
event_type: 'initiated',
|
|
447
469
|
});
|
|
448
470
|
}
|
|
@@ -457,6 +479,7 @@ export class ClientEventReporter {
|
|
|
457
479
|
...this.buildCommon(cid, 'CoordinatorJoin', pair),
|
|
458
480
|
...this.sessionIdField(cid),
|
|
459
481
|
...(pair.joinReasonSnapshot && { join_reason: pair.joinReasonSnapshot }),
|
|
482
|
+
...(pair.joinSourceSnapshot && { source: pair.joinSourceSnapshot }),
|
|
460
483
|
event_type: 'completed',
|
|
461
484
|
outcome: 'success',
|
|
462
485
|
retry_count_attempt: pair.attempts - 1,
|
|
@@ -476,6 +499,7 @@ export class ClientEventReporter {
|
|
|
476
499
|
...this.buildCommon(cid, 'CoordinatorJoin', pair),
|
|
477
500
|
...this.sessionIdField(cid),
|
|
478
501
|
...(pair.joinReasonSnapshot && { join_reason: pair.joinReasonSnapshot }),
|
|
502
|
+
...(pair.joinSourceSnapshot && { source: pair.joinSourceSnapshot }),
|
|
479
503
|
event_type: 'completed',
|
|
480
504
|
outcome: 'failure',
|
|
481
505
|
retry_count_attempt: pair.attempts - 1,
|
|
@@ -705,17 +729,19 @@ export class ClientEventReporter {
|
|
|
705
729
|
};
|
|
706
730
|
};
|
|
707
731
|
|
|
708
|
-
private send = (body:
|
|
732
|
+
private send = (body: ReportedClientEvent) => {
|
|
709
733
|
if (!this.enabled) return;
|
|
710
734
|
void this.sendWithRetry(body);
|
|
711
735
|
};
|
|
712
736
|
|
|
713
|
-
private sendForCall = (cid: string, body:
|
|
737
|
+
private sendForCall = (cid: string, body: ReportedClientEvent) => {
|
|
714
738
|
if (!this.callContexts.has(cid)) return;
|
|
715
739
|
this.send(body);
|
|
716
740
|
};
|
|
717
741
|
|
|
718
|
-
private sendWithRetry = async (
|
|
742
|
+
private sendWithRetry = async (
|
|
743
|
+
body: ReportedClientEvent,
|
|
744
|
+
): Promise<boolean> => {
|
|
719
745
|
for (let attempt = 0; attempt < 5; attempt++) {
|
|
720
746
|
try {
|
|
721
747
|
await this.streamClient.doAxiosRequest<
|