@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
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import '../rtc/__tests__/mocks/webrtc.mocks';
|
|
2
|
+
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { fromPartial } from '@total-typescript/shoehorn';
|
|
5
|
+
import { Call } from '../Call';
|
|
6
|
+
import { StreamClient } from '../coordinator/connection/client';
|
|
7
|
+
import { ClientEventReporter } from '../reporting';
|
|
8
|
+
import { generateUUIDv4 } from '../coordinator/connection/utils';
|
|
9
|
+
import { CallingState, StreamVideoWriteableStateStore } from '../store';
|
|
10
|
+
import { CallSessionResponse } from '../gen/coordinator';
|
|
11
|
+
|
|
12
|
+
const ME = 'jane';
|
|
13
|
+
|
|
14
|
+
// `resolveOwnRingOutcome` owns the decision and is tested on its own. This
|
|
15
|
+
// covers the effect around it: that it runs for a ringing call and that the
|
|
16
|
+
// `ringing` guard keeps it off every other call.
|
|
17
|
+
describe('Leaving a call settled by the current user', () => {
|
|
18
|
+
let call: Call;
|
|
19
|
+
|
|
20
|
+
const createCall = async (ringing: boolean) => {
|
|
21
|
+
const clientStore = new StreamVideoWriteableStateStore();
|
|
22
|
+
const streamClient = new StreamClient('abc');
|
|
23
|
+
call = new Call({
|
|
24
|
+
type: 'test',
|
|
25
|
+
id: generateUUIDv4(),
|
|
26
|
+
streamClient,
|
|
27
|
+
clientEventReporter: new ClientEventReporter({ streamClient }),
|
|
28
|
+
clientStore,
|
|
29
|
+
ringing,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
clientStore.setConnectedUser(fromPartial({ id: ME }));
|
|
33
|
+
vi.spyOn(call, 'leave').mockResolvedValue(undefined);
|
|
34
|
+
await call.setup();
|
|
35
|
+
return call;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const settleByMe = () =>
|
|
39
|
+
call.state['sessionSubject'].next(
|
|
40
|
+
fromPartial<CallSessionResponse>({
|
|
41
|
+
id: 'session-1',
|
|
42
|
+
accepted_by: {},
|
|
43
|
+
rejected_by: { [ME]: new Date().toISOString() },
|
|
44
|
+
missed_by: {},
|
|
45
|
+
participants: [],
|
|
46
|
+
participants_count_by_role: {},
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
vi.restoreAllMocks();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('leaves a ringing call the current user rejected', async () => {
|
|
55
|
+
await createCall(true);
|
|
56
|
+
expect(call.state.callingState).toBe(CallingState.RINGING);
|
|
57
|
+
|
|
58
|
+
settleByMe();
|
|
59
|
+
|
|
60
|
+
expect(call.leave).toHaveBeenCalled();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('ignores the same session on a call that is not ringing', async () => {
|
|
64
|
+
await createCall(false);
|
|
65
|
+
expect(call.state.callingState).toBe(CallingState.IDLE);
|
|
66
|
+
|
|
67
|
+
settleByMe();
|
|
68
|
+
|
|
69
|
+
expect(call.leave).not.toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -3,6 +3,8 @@ import { StreamSfuClient } from '../StreamSfuClient';
|
|
|
3
3
|
import { Dispatcher } from '../rtc';
|
|
4
4
|
import { StreamClient } from '../coordinator/connection/client';
|
|
5
5
|
import { getTimers } from '../timers';
|
|
6
|
+
import { getSdkInfo, setSdkInfo } from '../helpers/client-details';
|
|
7
|
+
import { SdkType } from '../gen/video/sfu/models/models';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Minimal `WebSocket` stub used to drive `StreamSfuClient.close()` while the
|
|
@@ -431,3 +433,49 @@ describe('StreamSfuClient.leaveAndClose()', () => {
|
|
|
431
433
|
expect(notifyLeaveSpy).not.toHaveBeenCalled();
|
|
432
434
|
});
|
|
433
435
|
});
|
|
436
|
+
|
|
437
|
+
describe('StreamSfuClient RPC headers', () => {
|
|
438
|
+
beforeEach(() => {
|
|
439
|
+
CapturingWebSocket.instances = [];
|
|
440
|
+
vi.stubGlobal('WebSocket', CapturingWebSocket);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
afterEach(() => {
|
|
444
|
+
vi.unstubAllGlobals();
|
|
445
|
+
vi.restoreAllMocks();
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('identifies the SDK and its version in every RPC request', async () => {
|
|
449
|
+
const sdkInfo = getSdkInfo();
|
|
450
|
+
setSdkInfo({
|
|
451
|
+
type: SdkType.REACT,
|
|
452
|
+
major: '1',
|
|
453
|
+
minor: '2',
|
|
454
|
+
patch: '3',
|
|
455
|
+
});
|
|
456
|
+
const fetchMock = vi
|
|
457
|
+
.fn()
|
|
458
|
+
.mockResolvedValue(
|
|
459
|
+
new Response('{}', { headers: { 'content-type': 'application/json' } }),
|
|
460
|
+
);
|
|
461
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
462
|
+
|
|
463
|
+
try {
|
|
464
|
+
const sfuClient = buildSfuClient();
|
|
465
|
+
(
|
|
466
|
+
sfuClient as unknown as {
|
|
467
|
+
joinResponseTask: { resolve: (v: unknown) => void };
|
|
468
|
+
}
|
|
469
|
+
).joinResponseTask.resolve({});
|
|
470
|
+
|
|
471
|
+
await sfuClient.startNoiseCancellation();
|
|
472
|
+
|
|
473
|
+
const [, init] = fetchMock.mock.calls[0];
|
|
474
|
+
expect(new Headers(init.headers).get('x-stream-client')).toBe(
|
|
475
|
+
'stream-video-react-v1.2.3',
|
|
476
|
+
);
|
|
477
|
+
} finally {
|
|
478
|
+
setSdkInfo(sdkInfo!);
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
});
|
|
@@ -17,7 +17,14 @@ const secret = process.env.STREAM_SECRET!;
|
|
|
17
17
|
|
|
18
18
|
describe('StreamVideoClient Ringing', () => {
|
|
19
19
|
const serverClient = new StreamClient(apiKey, secret);
|
|
20
|
-
|
|
20
|
+
|
|
21
|
+
// user ids are generated per test: CI runs share a single Stream app, so
|
|
22
|
+
// fixed ids let a concurrently running job ring these users and satisfy the
|
|
23
|
+
// assertions in this file with an unrelated call.
|
|
24
|
+
let oliverId: string;
|
|
25
|
+
let sachaId: string;
|
|
26
|
+
let marceloId: string;
|
|
27
|
+
let testUserIds: string[];
|
|
21
28
|
|
|
22
29
|
let oliverClient: StreamVideoClient;
|
|
23
30
|
let sachaClient: StreamVideoClient;
|
|
@@ -54,6 +61,11 @@ describe('StreamVideoClient Ringing', () => {
|
|
|
54
61
|
};
|
|
55
62
|
|
|
56
63
|
beforeEach(async () => {
|
|
64
|
+
const suffix = crypto.randomUUID();
|
|
65
|
+
oliverId = `oliver-${suffix}`;
|
|
66
|
+
sachaId = `sacha-${suffix}`;
|
|
67
|
+
marceloId = `marcelo-${suffix}`;
|
|
68
|
+
testUserIds = [oliverId, sachaId, marceloId];
|
|
57
69
|
[oliverClient, sachaClient, marceloClient] =
|
|
58
70
|
await createClients(testUserIds);
|
|
59
71
|
});
|
|
@@ -75,11 +87,11 @@ describe('StreamVideoClient Ringing', () => {
|
|
|
75
87
|
await call.create({
|
|
76
88
|
ring: true,
|
|
77
89
|
data: {
|
|
78
|
-
created_by_id:
|
|
90
|
+
created_by_id: oliverId,
|
|
79
91
|
members: [
|
|
80
|
-
{ user_id:
|
|
81
|
-
{ user_id:
|
|
82
|
-
{ user_id:
|
|
92
|
+
{ user_id: oliverId },
|
|
93
|
+
{ user_id: sachaId },
|
|
94
|
+
{ user_id: marceloId },
|
|
83
95
|
],
|
|
84
96
|
},
|
|
85
97
|
});
|
|
@@ -112,9 +124,9 @@ describe('StreamVideoClient Ringing', () => {
|
|
|
112
124
|
ring: true,
|
|
113
125
|
data: {
|
|
114
126
|
members: [
|
|
115
|
-
{ user_id:
|
|
116
|
-
{ user_id:
|
|
117
|
-
{ user_id:
|
|
127
|
+
{ user_id: oliverId },
|
|
128
|
+
{ user_id: sachaId },
|
|
129
|
+
{ user_id: marceloId },
|
|
118
130
|
],
|
|
119
131
|
},
|
|
120
132
|
});
|
|
@@ -144,9 +156,9 @@ describe('StreamVideoClient Ringing', () => {
|
|
|
144
156
|
ring: false, // don't ring all members by default
|
|
145
157
|
data: {
|
|
146
158
|
members: [
|
|
147
|
-
{ user_id:
|
|
148
|
-
{ user_id:
|
|
149
|
-
{ user_id:
|
|
159
|
+
{ user_id: oliverId },
|
|
160
|
+
{ user_id: sachaId },
|
|
161
|
+
{ user_id: marceloId },
|
|
150
162
|
],
|
|
151
163
|
},
|
|
152
164
|
});
|
|
@@ -162,7 +174,7 @@ describe('StreamVideoClient Ringing', () => {
|
|
|
162
174
|
// oliver is calling sacha. only sacha should get a ring event
|
|
163
175
|
const sachaIndividualRing = expectEvent(sachaClient, 'call.ring');
|
|
164
176
|
const marceloIndividualRing = expectEvent(marceloClient, 'call.ring');
|
|
165
|
-
await oliverCall.ring({ members_ids: [
|
|
177
|
+
await oliverCall.ring({ members_ids: [sachaId] });
|
|
166
178
|
await expect(sachaIndividualRing).resolves.toHaveProperty(
|
|
167
179
|
'call.cid',
|
|
168
180
|
oliverCall.cid,
|
|
@@ -175,7 +187,7 @@ describe('StreamVideoClient Ringing', () => {
|
|
|
175
187
|
// sacha is calling marcelo. only marcelo should get a ring event
|
|
176
188
|
const oliverIndividualRing = expectEvent(oliverClient, 'call.ring');
|
|
177
189
|
const marceloIndividualRing2 = expectEvent(marceloClient, 'call.ring');
|
|
178
|
-
await sachaCall.ring({ members_ids: [
|
|
190
|
+
await sachaCall.ring({ members_ids: [marceloId] });
|
|
179
191
|
await expect(marceloIndividualRing2).resolves.toHaveProperty(
|
|
180
192
|
'call.cid',
|
|
181
193
|
sachaCall.cid,
|
|
@@ -241,8 +253,8 @@ describe('StreamVideoClient Ringing', () => {
|
|
|
241
253
|
const serverCall = serverClient.video.call('default', callId);
|
|
242
254
|
await serverCall.create({
|
|
243
255
|
data: {
|
|
244
|
-
created_by_id:
|
|
245
|
-
members: [{ user_id:
|
|
256
|
+
created_by_id: oliverId,
|
|
257
|
+
members: [{ user_id: oliverId }, { user_id: sachaId }],
|
|
246
258
|
},
|
|
247
259
|
});
|
|
248
260
|
|
|
@@ -266,7 +278,7 @@ describe('StreamVideoClient Ringing', () => {
|
|
|
266
278
|
await oliverCall.create({
|
|
267
279
|
ring: true,
|
|
268
280
|
data: {
|
|
269
|
-
members: [{ user_id:
|
|
281
|
+
members: [{ user_id: oliverId }, { user_id: sachaId }],
|
|
270
282
|
},
|
|
271
283
|
});
|
|
272
284
|
|
|
@@ -302,7 +314,7 @@ describe('StreamVideoClient Ringing', () => {
|
|
|
302
314
|
await marceloCall.create({
|
|
303
315
|
ring: true,
|
|
304
316
|
data: {
|
|
305
|
-
members: [{ user_id:
|
|
317
|
+
members: [{ user_id: marceloId }, { user_id: oliverId }],
|
|
306
318
|
},
|
|
307
319
|
});
|
|
308
320
|
|
|
@@ -332,7 +344,7 @@ describe('StreamVideoClient Ringing', () => {
|
|
|
332
344
|
await marceloCall.create({
|
|
333
345
|
ring: true,
|
|
334
346
|
data: {
|
|
335
|
-
members: [{ user_id:
|
|
347
|
+
members: [{ user_id: marceloId }, { user_id: sachaId }],
|
|
336
348
|
},
|
|
337
349
|
});
|
|
338
350
|
|
|
@@ -354,7 +366,7 @@ describe('StreamVideoClient Ringing', () => {
|
|
|
354
366
|
await marceloCall.create({
|
|
355
367
|
ring: true,
|
|
356
368
|
data: {
|
|
357
|
-
members: [{ user_id:
|
|
369
|
+
members: [{ user_id: marceloId }, { user_id: oliverId }],
|
|
358
370
|
},
|
|
359
371
|
});
|
|
360
372
|
|
|
@@ -375,7 +387,7 @@ describe('StreamVideoClient Ringing', () => {
|
|
|
375
387
|
await marceloCall.create({
|
|
376
388
|
ring: true,
|
|
377
389
|
data: {
|
|
378
|
-
members: [{ user_id:
|
|
390
|
+
members: [{ user_id: marceloId }, { user_id: sachaId }],
|
|
379
391
|
},
|
|
380
392
|
});
|
|
381
393
|
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { StreamClient } from '../client';
|
|
3
|
+
import { StableWSConnection } from '../connection';
|
|
4
|
+
import { videoLoggerSystem } from '../../../logger';
|
|
5
|
+
import type { StreamVideoEvent } from '../types';
|
|
6
|
+
|
|
7
|
+
const event: StreamVideoEvent = { type: 'network.changed', online: true };
|
|
8
|
+
|
|
9
|
+
describe('event listener isolation', () => {
|
|
10
|
+
let client: StreamClient;
|
|
11
|
+
let sink: ReturnType<typeof vi.fn>;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
sink = vi.fn();
|
|
15
|
+
videoLoggerSystem.configureLoggers({
|
|
16
|
+
coordinator: { sink, level: 'error' },
|
|
17
|
+
});
|
|
18
|
+
client = new StreamClient('key');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
videoLoggerSystem.restoreDefaults();
|
|
23
|
+
vi.restoreAllMocks();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const errorLogs = () =>
|
|
27
|
+
sink.mock.calls.filter(([level]) => level === 'error');
|
|
28
|
+
|
|
29
|
+
it('keeps dispatching after a listener throws', () => {
|
|
30
|
+
const failure = new Error('listener blew up');
|
|
31
|
+
const first = vi.fn(() => {
|
|
32
|
+
throw failure;
|
|
33
|
+
});
|
|
34
|
+
const second = vi.fn();
|
|
35
|
+
|
|
36
|
+
client.on('network.changed', first);
|
|
37
|
+
client.on('network.changed', second);
|
|
38
|
+
|
|
39
|
+
expect(() => client.dispatchEvent(event)).not.toThrow();
|
|
40
|
+
expect(first).toHaveBeenCalledTimes(1);
|
|
41
|
+
expect(second).toHaveBeenCalledTimes(1);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('reports a synchronous throw through the logger', () => {
|
|
45
|
+
const failure = new Error('listener blew up');
|
|
46
|
+
client.on('network.changed', () => {
|
|
47
|
+
throw failure;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
client.dispatchEvent(event);
|
|
51
|
+
|
|
52
|
+
expect(errorLogs()).toEqual([
|
|
53
|
+
[
|
|
54
|
+
'error',
|
|
55
|
+
'[coordinator]: Unhandled error in event listener',
|
|
56
|
+
failure,
|
|
57
|
+
event,
|
|
58
|
+
],
|
|
59
|
+
]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('reports a rejection from an async listener', async () => {
|
|
63
|
+
const failure = new Error('async listener blew up');
|
|
64
|
+
client.on('network.changed', async () => {
|
|
65
|
+
throw failure;
|
|
66
|
+
});
|
|
67
|
+
const next = vi.fn();
|
|
68
|
+
client.on('network.changed', next);
|
|
69
|
+
|
|
70
|
+
client.dispatchEvent(event);
|
|
71
|
+
expect(next).toHaveBeenCalledTimes(1);
|
|
72
|
+
expect(errorLogs()).toHaveLength(0);
|
|
73
|
+
|
|
74
|
+
await vi.waitFor(() => expect(errorLogs()).toHaveLength(1));
|
|
75
|
+
expect(errorLogs()[0]).toEqual([
|
|
76
|
+
'error',
|
|
77
|
+
'[coordinator]: Unhandled error in event listener',
|
|
78
|
+
failure,
|
|
79
|
+
event,
|
|
80
|
+
]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('isolates a faulty `all` listener from type specific listeners', () => {
|
|
84
|
+
const all = vi.fn(() => {
|
|
85
|
+
throw new Error('all listener blew up');
|
|
86
|
+
});
|
|
87
|
+
const typed = vi.fn();
|
|
88
|
+
|
|
89
|
+
client.on('all', all);
|
|
90
|
+
client.on('network.changed', typed);
|
|
91
|
+
|
|
92
|
+
client.dispatchEvent(event);
|
|
93
|
+
|
|
94
|
+
expect(all).toHaveBeenCalledTimes(1);
|
|
95
|
+
expect(typed).toHaveBeenCalledTimes(1);
|
|
96
|
+
expect(errorLogs()).toHaveLength(1);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('schedules the connection check even when a listener throws', () => {
|
|
100
|
+
const connection = new StableWSConnection(client);
|
|
101
|
+
const scheduleConnectionCheck = vi
|
|
102
|
+
.spyOn(connection, 'scheduleConnectionCheck')
|
|
103
|
+
.mockImplementation(() => {});
|
|
104
|
+
|
|
105
|
+
client.on('all', () => {
|
|
106
|
+
throw new Error('listener blew up');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
connection.onmessage(connection.wsID, {
|
|
110
|
+
data: JSON.stringify({ type: 'custom' }),
|
|
111
|
+
} as MessageEvent);
|
|
112
|
+
|
|
113
|
+
expect(scheduleConnectionCheck).toHaveBeenCalledTimes(1);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -10,6 +10,7 @@ import { TokenManager } from './token_manager';
|
|
|
10
10
|
import {
|
|
11
11
|
addConnectionEventListeners,
|
|
12
12
|
generateUUIDv4,
|
|
13
|
+
invokeEventListener,
|
|
13
14
|
isErrorResponse,
|
|
14
15
|
KnownCodes,
|
|
15
16
|
removeConnectionEventListeners,
|
|
@@ -589,12 +590,12 @@ export class StreamClient {
|
|
|
589
590
|
|
|
590
591
|
// call generic listeners
|
|
591
592
|
for (const listener of this.listeners.all || []) {
|
|
592
|
-
listener
|
|
593
|
+
invokeEventListener(listener, event, this.logger);
|
|
593
594
|
}
|
|
594
595
|
|
|
595
596
|
// call type specific listeners
|
|
596
597
|
for (const listener of this.listeners[event.type] || []) {
|
|
597
|
-
listener
|
|
598
|
+
invokeEventListener(listener, event, this.logger);
|
|
598
599
|
}
|
|
599
600
|
};
|
|
600
601
|
|
|
@@ -200,6 +200,18 @@ export type Logger = (
|
|
|
200
200
|
...args: unknown[]
|
|
201
201
|
) => void;
|
|
202
202
|
|
|
203
|
+
export type RingStatePollingOptions = {
|
|
204
|
+
/**
|
|
205
|
+
* Quiet time after the ring starts, before the first poll. Defaults to 15_000.
|
|
206
|
+
*/
|
|
207
|
+
startAfterMs?: number;
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The interval between polls. Defaults to 5_000.
|
|
211
|
+
*/
|
|
212
|
+
intervalMs?: number;
|
|
213
|
+
};
|
|
214
|
+
|
|
203
215
|
export type StreamClientOptions = Partial<AxiosRequestConfig> & {
|
|
204
216
|
/**
|
|
205
217
|
* Used to disable warnings that are triggered by using connectUser or connectAnonymousUser server-side.
|
|
@@ -305,6 +317,13 @@ export type StreamClientOptions = Partial<AxiosRequestConfig> & {
|
|
|
305
317
|
*/
|
|
306
318
|
rejectCallWhenBusy?: boolean;
|
|
307
319
|
|
|
320
|
+
/**
|
|
321
|
+
* Caller-side polling for the ring outcome, used when the `call.accepted`,
|
|
322
|
+
* `call.rejected` or `call.missed` event never arrives. Enabled by default;
|
|
323
|
+
* set to `false` to disable, or pass an object to tune the timings.
|
|
324
|
+
*/
|
|
325
|
+
ringStatePolling?: false | RingStatePollingOptions;
|
|
326
|
+
|
|
308
327
|
/**
|
|
309
328
|
* Device persistence preference options (web only).
|
|
310
329
|
*/
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AxiosResponse } from 'axios';
|
|
2
2
|
import type { APIErrorResponse } from './types';
|
|
3
3
|
import type { ConnectionErrorEvent } from '../../gen/coordinator';
|
|
4
|
+
import type { ScopedLogger } from '../../logger';
|
|
4
5
|
|
|
5
6
|
export const sleep = (m: number) => new Promise((r) => setTimeout(r, m));
|
|
6
7
|
|
|
@@ -118,3 +119,30 @@ export function isCloseEvent(
|
|
|
118
119
|
): res is CloseEvent {
|
|
119
120
|
return (res as CloseEvent).code !== undefined;
|
|
120
121
|
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Invokes a single event listener in isolation, so that a faulty listener
|
|
125
|
+
* cannot abort the dispatch loop nor the bookkeeping that follows it.
|
|
126
|
+
*
|
|
127
|
+
* @param listener the listener to invoke.
|
|
128
|
+
* @param event the event to pass to the listener.
|
|
129
|
+
* @param logger the logger to report listener failures with.
|
|
130
|
+
*/
|
|
131
|
+
export const invokeEventListener = <T>(
|
|
132
|
+
listener: (event: T) => unknown,
|
|
133
|
+
event: T,
|
|
134
|
+
logger: ScopedLogger,
|
|
135
|
+
) => {
|
|
136
|
+
try {
|
|
137
|
+
const result = listener(event);
|
|
138
|
+
if (result && typeof (result as PromiseLike<unknown>).then === 'function') {
|
|
139
|
+
Promise.resolve(result).catch((error) => report(logger, error, event));
|
|
140
|
+
}
|
|
141
|
+
} catch (error) {
|
|
142
|
+
report(logger, error, event);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const report = (logger: ScopedLogger, error: unknown, event: unknown) => {
|
|
147
|
+
logger.error('Unhandled error in event listener', error, event);
|
|
148
|
+
};
|