@stream-io/video-client 2.0.0-beta.0 → 2.0.0-beta.1
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 +18 -0
- package/dist/index.browser.es.js +1126 -791
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +1126 -791
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +1126 -791
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +37 -11
- package/dist/src/StreamVideoClient.d.ts +15 -21
- 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/rtc/Publisher.d.ts +18 -0
- package/dist/src/rtc/TransceiverCache.d.ts +10 -0
- package/dist/src/rtc/e2ee/E2EEManager.d.ts +10 -1
- package/dist/src/stats/utils.d.ts +6 -0
- package/dist/src/store/CallState.d.ts +118 -267
- package/dist/src/store/ClientState.d.ts +62 -0
- package/dist/src/store/index.d.ts +1 -1
- package/dist/src/store/subjects.d.ts +15 -0
- package/dist/src/store/types.d.ts +22 -0
- package/dist/src/types.d.ts +33 -4
- package/index.ts +1 -0
- package/package.json +2 -2
- package/src/Call.ts +285 -125
- package/src/StreamSfuClient.ts +6 -2
- package/src/StreamVideoClient.ts +40 -47
- package/src/__tests__/Call.autodrop.test.ts +118 -66
- package/src/__tests__/Call.getRingState.test.ts +87 -0
- package/src/__tests__/Call.lifecycle.test.ts +499 -6
- package/src/__tests__/Call.publishing.test.ts +2 -2
- package/src/__tests__/Call.ringSettled.test.ts +71 -0
- package/src/__tests__/Call.ringingLifecycle.test.ts +242 -0
- package/src/__tests__/StreamSfuClient.test.ts +48 -0
- package/src/__tests__/StreamVideoClient.disconnect.test.ts +62 -0
- package/src/coordinator/connection/types.ts +19 -0
- package/src/devices/__tests__/CameraManager.test.ts +2 -2
- package/src/devices/__tests__/DeviceManager.test.ts +2 -2
- package/src/devices/__tests__/DeviceManagerFilters.test.ts +2 -2
- package/src/devices/__tests__/MicrophoneManager.test.ts +2 -2
- package/src/devices/__tests__/MicrophoneManagerRN.test.ts +2 -2
- package/src/devices/__tests__/ScreenShareManager.test.ts +3 -3
- package/src/devices/__tests__/SpeakerManager.test.ts +4 -4
- package/src/events/__tests__/call.test.ts +5 -220
- package/src/events/call.ts +1 -73
- package/src/events/callEventHandlers.ts +12 -23
- package/src/gen/coordinator/index.ts +67 -0
- package/src/helpers/__tests__/AudioBindingsWatchdog.test.ts +2 -2
- package/src/helpers/__tests__/DynascaleManager.test.ts +2 -2
- package/src/helpers/__tests__/ViewportTracker.test.ts +2 -2
- 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/rtc/Publisher.ts +66 -12
- package/src/rtc/TransceiverCache.ts +17 -0
- package/src/rtc/__tests__/Call.reconnect.test.ts +5 -5
- package/src/rtc/__tests__/Publisher.test.ts +209 -0
- package/src/rtc/__tests__/mocks/webrtc.mocks.ts +1 -0
- package/src/rtc/e2ee/E2EEManager.ts +10 -1
- package/src/rtc/e2ee/EncryptionManager.ts +1 -1
- package/src/rtc/e2ee/SPEC.md +1 -1
- package/src/stats/__tests__/utils.test.ts +33 -0
- package/src/stats/utils.ts +15 -0
- package/src/store/CallState.ts +305 -550
- package/src/store/ClientState.ts +101 -0
- package/src/store/__tests__/CallState.test.ts +60 -1
- package/src/store/__tests__/ClientState.test.ts +35 -0
- package/src/store/__tests__/rxUtils.test.ts +67 -1
- package/src/store/index.ts +1 -1
- package/src/store/rxUtils.ts +8 -4
- package/src/store/subjects.ts +30 -0
- package/src/store/types.ts +45 -0
- package/src/types.ts +40 -4
- package/dist/src/store/stateStore.d.ts +0 -84
- package/src/store/stateStore.ts +0 -156
package/src/StreamSfuClient.ts
CHANGED
|
@@ -39,7 +39,8 @@ import {
|
|
|
39
39
|
} from './helpers/promise';
|
|
40
40
|
import { withoutConcurrency } from './helpers/concurrency';
|
|
41
41
|
import { getTimers } from './timers';
|
|
42
|
-
import {
|
|
42
|
+
import { getSdkInfo } from './helpers/client-details';
|
|
43
|
+
import { getStreamClientId, Tracer, TraceSlice } from './stats';
|
|
43
44
|
import { SfuJoinError, SfuTimeoutError } from './errors';
|
|
44
45
|
|
|
45
46
|
export type StreamSfuClientConstructor = {
|
|
@@ -271,7 +272,10 @@ export class StreamSfuClient {
|
|
|
271
272
|
this.rpc = createSignalClient({
|
|
272
273
|
baseUrl: server.url,
|
|
273
274
|
interceptors: [
|
|
274
|
-
withHeaders({
|
|
275
|
+
withHeaders({
|
|
276
|
+
Authorization: `Bearer ${token}`,
|
|
277
|
+
'X-Stream-Client': getStreamClientId(getSdkInfo()),
|
|
278
|
+
}),
|
|
275
279
|
this.tracer && withRequestTracer(this.tracer.trace),
|
|
276
280
|
this.logger.getLogLevel() === 'trace' && withRequestLogger(this.logger),
|
|
277
281
|
withTimeout(rpcRequestTimeout, this.tracer?.trace),
|
package/src/StreamVideoClient.ts
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { Call } from './Call';
|
|
2
2
|
import { StreamClient } from './coordinator/connection/client';
|
|
3
|
-
import {
|
|
4
|
-
CallingState,
|
|
5
|
-
StreamVideoReadOnlyStateStore,
|
|
6
|
-
StreamVideoWriteableStateStore,
|
|
7
|
-
} from './store';
|
|
3
|
+
import { CallingState, ClientState } from './store';
|
|
8
4
|
import type {
|
|
9
5
|
CallCreatedEvent,
|
|
10
6
|
CallRingEvent,
|
|
@@ -48,19 +44,14 @@ import { ClientEventReporter } from './reporting';
|
|
|
48
44
|
*/
|
|
49
45
|
export class StreamVideoClient {
|
|
50
46
|
/**
|
|
51
|
-
*
|
|
47
|
+
* The reactive state of this client.
|
|
52
48
|
* You can subscribe to changes of the different state variables.
|
|
53
|
-
* Our library is built in a way that all state changes are exposed in this store,
|
|
54
|
-
* o all UI changes in your application should be handled by subscribing to these variables.
|
|
55
|
-
*
|
|
56
|
-
* @deprecated use the `client.state` getter.
|
|
57
49
|
*/
|
|
58
|
-
readonly
|
|
50
|
+
readonly state = new ClientState();
|
|
59
51
|
readonly logger: ScopedLogger;
|
|
60
52
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
readonly clientEventReporter: ClientEventReporter;
|
|
53
|
+
readonly streamClient: StreamClient;
|
|
54
|
+
private readonly clientEventReporter: ClientEventReporter;
|
|
64
55
|
|
|
65
56
|
private effectsRegistered = false;
|
|
66
57
|
private eventHandlersToUnregister: Array<() => void> = [];
|
|
@@ -103,11 +94,6 @@ export class StreamVideoClient {
|
|
|
103
94
|
enabled: clientOptions?.clientEventsReportingEnabled ?? true,
|
|
104
95
|
});
|
|
105
96
|
|
|
106
|
-
this.writeableStateStore = new StreamVideoWriteableStateStore();
|
|
107
|
-
this.readOnlyStateStore = new StreamVideoReadOnlyStateStore(
|
|
108
|
-
this.writeableStateStore,
|
|
109
|
-
);
|
|
110
|
-
|
|
111
97
|
if (typeof apiKeyOrArgs !== 'string' && apiKeyOrArgs.user) {
|
|
112
98
|
const user = apiKeyOrArgs.user;
|
|
113
99
|
if (user.type === 'anonymous') user.id = '!anon';
|
|
@@ -158,13 +144,6 @@ export class StreamVideoClient {
|
|
|
158
144
|
StreamVideoClient._instances.set(instanceKey, this);
|
|
159
145
|
};
|
|
160
146
|
|
|
161
|
-
/**
|
|
162
|
-
* Return the reactive state store, use this if you want to be notified about changes to the client state
|
|
163
|
-
*/
|
|
164
|
-
get state() {
|
|
165
|
-
return this.readOnlyStateStore;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
147
|
private registerEffects = () => {
|
|
169
148
|
if (this.effectsRegistered) return;
|
|
170
149
|
|
|
@@ -174,7 +153,7 @@ export class StreamVideoClient {
|
|
|
174
153
|
this.on('connection.changed', (event) => {
|
|
175
154
|
if (!event.online) return;
|
|
176
155
|
|
|
177
|
-
const callsToReWatch = this.
|
|
156
|
+
const callsToReWatch = this.state.calls
|
|
178
157
|
.filter((call) => call.watching)
|
|
179
158
|
.map((call) => call.cid);
|
|
180
159
|
if (callsToReWatch.length <= 0) return;
|
|
@@ -197,7 +176,7 @@ export class StreamVideoClient {
|
|
|
197
176
|
const concurrencyTag = getCallInitConcurrencyTag(e.call_cid);
|
|
198
177
|
await withoutConcurrency(concurrencyTag, async () => {
|
|
199
178
|
const ringing = e.type === 'call.ring';
|
|
200
|
-
let call = this.
|
|
179
|
+
let call = this.state.findCall(e.call.type, e.call.id);
|
|
201
180
|
if (call) {
|
|
202
181
|
if (ringing) {
|
|
203
182
|
if (this.shouldRejectCall(call.cid)) {
|
|
@@ -224,7 +203,7 @@ export class StreamVideoClient {
|
|
|
224
203
|
type: e.call.type,
|
|
225
204
|
id: e.call.id,
|
|
226
205
|
members: e.members,
|
|
227
|
-
|
|
206
|
+
clientState: this.state,
|
|
228
207
|
ringing,
|
|
229
208
|
});
|
|
230
209
|
|
|
@@ -239,7 +218,7 @@ export class StreamVideoClient {
|
|
|
239
218
|
}
|
|
240
219
|
} else {
|
|
241
220
|
call.state.updateFromCallResponse(e.call);
|
|
242
|
-
this.
|
|
221
|
+
this.state.registerCall(call);
|
|
243
222
|
this.logger.info(`New call created and registered: ${call.cid}`);
|
|
244
223
|
}
|
|
245
224
|
});
|
|
@@ -278,10 +257,7 @@ export class StreamVideoClient {
|
|
|
278
257
|
});
|
|
279
258
|
|
|
280
259
|
for (const c of response.calls) {
|
|
281
|
-
const call = this.
|
|
282
|
-
c.call.type,
|
|
283
|
-
c.call.id,
|
|
284
|
-
);
|
|
260
|
+
const call = this.state.findCall(c.call.type, c.call.id);
|
|
285
261
|
|
|
286
262
|
if (call) {
|
|
287
263
|
call.updateFromCallStateResponse(c);
|
|
@@ -310,8 +286,6 @@ export class StreamVideoClient {
|
|
|
310
286
|
|
|
311
287
|
/**
|
|
312
288
|
* Connects the given user to the client.
|
|
313
|
-
* Only one user can connect at a time, if you want to change users, call `disconnectUser` before connecting a new user.
|
|
314
|
-
* If the connection is successful, the connected user [state variable](#readonlystatestore) will be updated accordingly.
|
|
315
289
|
*
|
|
316
290
|
* @param user the user to connect.
|
|
317
291
|
* @param tokenOrProvider a token or a function that returns a token.
|
|
@@ -369,7 +343,7 @@ export class StreamVideoClient {
|
|
|
369
343
|
|
|
370
344
|
// connectUserResponse will be void if connectUser called twice for the same user
|
|
371
345
|
if (connectUserResponse?.me) {
|
|
372
|
-
this.
|
|
346
|
+
this.state.setConnectedUser(connectUserResponse.me);
|
|
373
347
|
}
|
|
374
348
|
|
|
375
349
|
this.registerEffects();
|
|
@@ -377,19 +351,36 @@ export class StreamVideoClient {
|
|
|
377
351
|
return connectUserResponse;
|
|
378
352
|
};
|
|
379
353
|
|
|
354
|
+
/**
|
|
355
|
+
* Leaves every call this client still tracks. Runs while the coordinator
|
|
356
|
+
* connection is alive, so that the leave requests each call sends reach the
|
|
357
|
+
* backend.
|
|
358
|
+
*/
|
|
359
|
+
private leaveAllCalls = async () => {
|
|
360
|
+
for (const call of this.state.calls) {
|
|
361
|
+
if (call.state.callingState === CallingState.LEFT) continue;
|
|
362
|
+
|
|
363
|
+
this.logger.info(`User disconnected, leaving call: ${call.cid}`);
|
|
364
|
+
await call
|
|
365
|
+
.leave({ message: 'client.disconnectUser() called' })
|
|
366
|
+
.catch((err) => {
|
|
367
|
+
this.logger.error(`Error leaving call: ${call.cid}`, err);
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
|
|
380
372
|
/**
|
|
381
373
|
* Disconnects the currently connected user from the client.
|
|
382
|
-
*
|
|
383
|
-
* If the connection is successfully disconnected, the connected user [state variable](#readonlystatestore) will be updated accordingly
|
|
374
|
+
* Leaves every call the user is still in, and updates `client.state`accordingly.
|
|
384
375
|
*
|
|
385
376
|
* @param timeout Max number of ms, to wait for close event of websocket, before forcefully assuming successful disconnection.
|
|
386
|
-
* https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
|
|
387
377
|
*/
|
|
388
378
|
disconnectUser = async (timeout?: number) => {
|
|
389
379
|
await withoutConcurrency(this.connectionConcurrencyTag, async () => {
|
|
390
380
|
const { user, key } = this.streamClient;
|
|
391
381
|
if (!user) return;
|
|
392
382
|
|
|
383
|
+
await this.leaveAllCalls();
|
|
393
384
|
await this.streamClient.disconnectUser(timeout);
|
|
394
385
|
|
|
395
386
|
if (user.id) {
|
|
@@ -398,14 +389,16 @@ export class StreamVideoClient {
|
|
|
398
389
|
this.eventHandlersToUnregister.forEach((unregister) => unregister());
|
|
399
390
|
this.eventHandlersToUnregister = [];
|
|
400
391
|
this.effectsRegistered = false;
|
|
401
|
-
this.
|
|
392
|
+
this.state.setConnectedUser(undefined);
|
|
402
393
|
});
|
|
403
394
|
};
|
|
404
395
|
|
|
405
396
|
/**
|
|
406
397
|
* You can subscribe to WebSocket events provided by the API.
|
|
407
398
|
* To remove a subscription, call the `off` method or, execute the returned unsubscribe function.
|
|
408
|
-
* Please note that subscribing to WebSocket events is an advanced use-case,
|
|
399
|
+
* Please note that subscribing to WebSocket events is an advanced use-case,
|
|
400
|
+
* for most use-cases it should be enough to watch for changes in the reactive
|
|
401
|
+
* `client.state`.
|
|
409
402
|
*
|
|
410
403
|
* @param eventName the event name or 'all'.
|
|
411
404
|
* @param callback the callback which will be called when the event is emitted.
|
|
@@ -444,7 +437,7 @@ export class StreamVideoClient {
|
|
|
444
437
|
options: { reuseInstance?: boolean } = {},
|
|
445
438
|
) => {
|
|
446
439
|
const call = options.reuseInstance
|
|
447
|
-
? this.
|
|
440
|
+
? this.state.findCall(type, id)
|
|
448
441
|
: undefined;
|
|
449
442
|
return (
|
|
450
443
|
call ??
|
|
@@ -453,7 +446,7 @@ export class StreamVideoClient {
|
|
|
453
446
|
clientEventReporter: this.clientEventReporter,
|
|
454
447
|
id: id,
|
|
455
448
|
type: type,
|
|
456
|
-
|
|
449
|
+
clientState: this.state,
|
|
457
450
|
})
|
|
458
451
|
);
|
|
459
452
|
};
|
|
@@ -492,7 +485,7 @@ export class StreamVideoClient {
|
|
|
492
485
|
members: c.members,
|
|
493
486
|
ownCapabilities: c.own_capabilities,
|
|
494
487
|
watching: data.watch,
|
|
495
|
-
|
|
488
|
+
clientState: this.state,
|
|
496
489
|
});
|
|
497
490
|
call.state.updateFromCallResponse(c.call);
|
|
498
491
|
await call.applyDeviceConfig(c.call.settings, {
|
|
@@ -501,7 +494,7 @@ export class StreamVideoClient {
|
|
|
501
494
|
});
|
|
502
495
|
if (data.watch) {
|
|
503
496
|
await call.setup();
|
|
504
|
-
this.
|
|
497
|
+
this.state.registerCall(call);
|
|
505
498
|
}
|
|
506
499
|
calls.push(call);
|
|
507
500
|
}
|
|
@@ -636,7 +629,7 @@ export class StreamVideoClient {
|
|
|
636
629
|
clientEventReporter: this.clientEventReporter,
|
|
637
630
|
type: callType,
|
|
638
631
|
id: callId,
|
|
639
|
-
|
|
632
|
+
clientState: this.state,
|
|
640
633
|
ringing: true,
|
|
641
634
|
});
|
|
642
635
|
await call.get();
|
|
@@ -1,104 +1,156 @@
|
|
|
1
1
|
import '../rtc/__tests__/mocks/webrtc.mocks';
|
|
2
2
|
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { fromPartial } from '@total-typescript/shoehorn';
|
|
4
5
|
import { Call } from '../Call';
|
|
5
6
|
import { StreamClient } from '../coordinator/connection/client';
|
|
6
7
|
import { ClientEventReporter } from '../reporting';
|
|
7
8
|
import { generateUUIDv4 } from '../coordinator/connection/utils';
|
|
8
|
-
import { CallingState,
|
|
9
|
+
import { CallingState, ClientState } from '../store';
|
|
10
|
+
import { CallSettingsResponse } from '../gen/coordinator';
|
|
9
11
|
|
|
12
|
+
const TIMEOUT_MS = 30_000;
|
|
13
|
+
|
|
14
|
+
// The timeout's own behaviour is covered by `ringing/__tests__/RingTimeout`.
|
|
15
|
+
// This file covers the wiring: that a ringing call arms one and that leaving
|
|
16
|
+
// cancels it.
|
|
10
17
|
describe('Auto drop ringing calls', () => {
|
|
11
|
-
let call: Call;
|
|
12
18
|
const userId = 'jane';
|
|
19
|
+
let call: Call;
|
|
13
20
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const clientStore = new StreamVideoWriteableStateStore();
|
|
21
|
+
const ringingCall = () => {
|
|
22
|
+
const clientState = new ClientState();
|
|
18
23
|
const streamClient = new StreamClient('abc');
|
|
19
|
-
|
|
24
|
+
const newCall = new Call({
|
|
20
25
|
type: 'test',
|
|
21
26
|
id: generateUUIDv4(),
|
|
22
27
|
streamClient,
|
|
23
28
|
clientEventReporter: new ClientEventReporter({ streamClient }),
|
|
24
|
-
|
|
29
|
+
clientState,
|
|
30
|
+
ringing: true,
|
|
25
31
|
});
|
|
26
32
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
console.log(`TEST: leave() called`);
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('caller should drop ringing calls after a timeout if no one accepted', async () => {
|
|
40
|
-
call.state['settingsSubject'].next({
|
|
41
|
-
// @ts-expect-error mocking only what we need for the test, we use fake timers, so undefined for timeout works
|
|
42
|
-
ring: {},
|
|
43
|
-
// @ts-expect-error mocking only what we need for the test
|
|
44
|
-
screensharing: {
|
|
45
|
-
enabled: false,
|
|
46
|
-
target_resolution: {
|
|
47
|
-
width: 100,
|
|
48
|
-
height: 100,
|
|
33
|
+
clientState.setConnectedUser(fromPartial({ id: userId }));
|
|
34
|
+
newCall.state['createdBySubject'].next(fromPartial({ id: userId }));
|
|
35
|
+
newCall.state['settingsSubject'].next(
|
|
36
|
+
fromPartial<CallSettingsResponse>({
|
|
37
|
+
ring: {
|
|
38
|
+
auto_cancel_timeout_ms: TIMEOUT_MS,
|
|
39
|
+
incoming_call_timeout_ms: TIMEOUT_MS,
|
|
40
|
+
missed_call_timeout_ms: TIMEOUT_MS,
|
|
49
41
|
},
|
|
50
|
-
|
|
51
|
-
|
|
42
|
+
screensharing: {
|
|
43
|
+
enabled: false,
|
|
44
|
+
target_resolution: { width: 100, height: 100 },
|
|
45
|
+
},
|
|
46
|
+
}),
|
|
47
|
+
);
|
|
48
|
+
newCall.state['sessionSubject'].next(
|
|
49
|
+
fromPartial({
|
|
50
|
+
id: 'session-1',
|
|
51
|
+
accepted_by: {},
|
|
52
|
+
rejected_by: {},
|
|
53
|
+
missed_by: {},
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
newCall.state['callingStateSubject'].next(CallingState.RINGING);
|
|
57
|
+
|
|
58
|
+
vi.spyOn(newCall, 'leave').mockResolvedValue(undefined);
|
|
59
|
+
return newCall;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
vi.useFakeTimers();
|
|
64
|
+
call = ringingCall();
|
|
65
|
+
});
|
|
52
66
|
|
|
53
|
-
|
|
54
|
-
call
|
|
55
|
-
|
|
56
|
-
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
call['cancelAutoDrop']();
|
|
69
|
+
vi.useRealTimers();
|
|
70
|
+
vi.restoreAllMocks();
|
|
71
|
+
});
|
|
57
72
|
|
|
58
|
-
|
|
59
|
-
call['
|
|
73
|
+
it('is armed when a call starts ringing', async () => {
|
|
74
|
+
call.state['callingStateSubject'].next(CallingState.IDLE);
|
|
60
75
|
|
|
61
|
-
|
|
76
|
+
call['handleRingingCall']();
|
|
62
77
|
|
|
78
|
+
expect(call['ringTimeout']).toBeDefined();
|
|
79
|
+
await vi.advanceTimersByTimeAsync(TIMEOUT_MS);
|
|
63
80
|
expect(call.leave).toHaveBeenCalledWith({
|
|
64
81
|
reject: true,
|
|
65
82
|
reason: 'timeout',
|
|
66
|
-
message:
|
|
83
|
+
message: 'ringing timeout - no one accepted',
|
|
67
84
|
});
|
|
68
85
|
});
|
|
69
86
|
|
|
70
|
-
it(
|
|
71
|
-
call
|
|
72
|
-
// @ts-expect-error mocking only what we need for the test, we use fake timers, so undefined for timeout works
|
|
73
|
-
ring: {},
|
|
74
|
-
// @ts-expect-error mocking only what we need for the test
|
|
75
|
-
screensharing: {
|
|
76
|
-
enabled: false,
|
|
77
|
-
target_resolution: {
|
|
78
|
-
width: 100,
|
|
79
|
-
height: 100,
|
|
80
|
-
},
|
|
81
|
-
},
|
|
82
|
-
});
|
|
87
|
+
it('is cancelled by cancelAutoDrop', async () => {
|
|
88
|
+
call['scheduleAutoDrop']();
|
|
83
89
|
|
|
84
|
-
|
|
85
|
-
call.state['createdBySubject'].next({
|
|
86
|
-
id: 'not-' + userId,
|
|
87
|
-
});
|
|
90
|
+
call['cancelAutoDrop']();
|
|
88
91
|
|
|
89
|
-
|
|
92
|
+
expect(call['ringTimeout']).toBeUndefined();
|
|
93
|
+
await vi.advanceTimersByTimeAsync(TIMEOUT_MS);
|
|
94
|
+
expect(call.leave).not.toHaveBeenCalled();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// the calling state stays RINGING well into teardown, so the watchdogs have
|
|
98
|
+
// to be paused before `leave` awaits anything
|
|
99
|
+
it('pauses the watchdogs synchronously when leave starts', async () => {
|
|
90
100
|
call['scheduleAutoDrop']();
|
|
101
|
+
call['scheduleRingStatePolling']();
|
|
102
|
+
const timeout = call['ringTimeout'];
|
|
103
|
+
vi.spyOn(call, 'leave').mockRestore();
|
|
104
|
+
vi.spyOn(call, 'reject').mockResolvedValue(fromPartial({}));
|
|
91
105
|
|
|
92
|
-
|
|
106
|
+
const poller = call['ringStatePoller'];
|
|
93
107
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
108
|
+
const leaving = call.leave({ reject: false });
|
|
109
|
+
|
|
110
|
+
// paused, not cancelled: both keep the window this leave is ending
|
|
111
|
+
expect(call['ringTimeout']).toBe(timeout);
|
|
112
|
+
expect(call['ringStatePoller']).toBe(poller);
|
|
113
|
+
expect(timeout!['timeoutId']).toBeUndefined();
|
|
114
|
+
expect(timeout!['stopped']).toBe(false);
|
|
115
|
+
expect(poller!['idleTimeoutId']).toBeUndefined();
|
|
116
|
+
expect(poller!['stopped']).toBe(false);
|
|
117
|
+
|
|
118
|
+
await leaving.catch(() => {});
|
|
119
|
+
|
|
120
|
+
expect(timeout!['stopped']).toBe(true);
|
|
121
|
+
expect(poller!['stopped']).toBe(true);
|
|
99
122
|
});
|
|
100
123
|
|
|
101
|
-
|
|
102
|
-
|
|
124
|
+
it('restores both watchdogs when rejecting the ring fails', async () => {
|
|
125
|
+
call['scheduleAutoDrop']();
|
|
126
|
+
call['scheduleRingStatePolling']();
|
|
127
|
+
const timeout = call['ringTimeout'];
|
|
128
|
+
const deadlineAt = timeout!['deadlineAt'];
|
|
129
|
+
const poller = call['ringStatePoller'];
|
|
130
|
+
const pollDeadlineAt = poller!['deadlineAt'];
|
|
131
|
+
vi.spyOn(call, 'leave').mockRestore();
|
|
132
|
+
vi.spyOn(call, 'reject').mockRejectedValueOnce(new Error('transient'));
|
|
133
|
+
|
|
134
|
+
await expect(call.leave({ reject: true })).rejects.toThrow('transient');
|
|
135
|
+
|
|
136
|
+
expect(call.state.callingState).toBe(CallingState.RINGING);
|
|
137
|
+
expect(call['ringTimeout']).toBe(timeout);
|
|
138
|
+
expect(timeout!['timeoutId']).toBeDefined();
|
|
139
|
+
expect(timeout!['deadlineAt']).toBe(deadlineAt);
|
|
140
|
+
expect(call['ringStatePoller']).toBe(poller);
|
|
141
|
+
expect(poller!['deadlineAt']).toBe(pollDeadlineAt);
|
|
142
|
+
expect(poller!['stopped']).toBe(false);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('replaces a previously armed timeout', async () => {
|
|
146
|
+
call['scheduleAutoDrop']();
|
|
147
|
+
const first = call['ringTimeout'];
|
|
148
|
+
|
|
149
|
+
call['scheduleAutoDrop']();
|
|
150
|
+
|
|
151
|
+
expect(call['ringTimeout']).not.toBe(first);
|
|
152
|
+
expect(first!['stopped']).toBe(true);
|
|
153
|
+
await vi.advanceTimersByTimeAsync(TIMEOUT_MS);
|
|
154
|
+
expect(call.leave).toHaveBeenCalledTimes(1);
|
|
103
155
|
});
|
|
104
156
|
});
|
|
@@ -0,0 +1,87 @@
|
|
|
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 { ClientState } from '../store';
|
|
10
|
+
import { CallSessionResponse } from '../gen/coordinator';
|
|
11
|
+
|
|
12
|
+
describe('Call.getRingState', () => {
|
|
13
|
+
const callId = generateUUIDv4();
|
|
14
|
+
|
|
15
|
+
const fakeCall = (sessionId?: string) => {
|
|
16
|
+
const streamClient = new StreamClient('abc');
|
|
17
|
+
const call = new Call({
|
|
18
|
+
type: 'test',
|
|
19
|
+
id: callId,
|
|
20
|
+
streamClient,
|
|
21
|
+
clientEventReporter: new ClientEventReporter({ streamClient }),
|
|
22
|
+
clientState: new ClientState(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (sessionId) {
|
|
26
|
+
call.state['sessionSubject'].next(
|
|
27
|
+
fromPartial<CallSessionResponse>({ id: sessionId }),
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const get = vi
|
|
32
|
+
.spyOn(streamClient, 'get')
|
|
33
|
+
.mockResolvedValue(fromPartial({ session_id: sessionId }));
|
|
34
|
+
|
|
35
|
+
return { call, get };
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
vi.restoreAllMocks();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('reads the ring state of the current session', async () => {
|
|
43
|
+
const { call, get } = fakeCall('session-1');
|
|
44
|
+
|
|
45
|
+
await call.getRingState();
|
|
46
|
+
|
|
47
|
+
expect(get).toHaveBeenCalledWith(`/call/test/${callId}/ring_state`, {
|
|
48
|
+
call_session_id: 'session-1',
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('reads the ring state of an explicitly named session', async () => {
|
|
53
|
+
// ending a call clears its current session, so a caller reconciling after
|
|
54
|
+
// `call.ended` has to name the session it rang on
|
|
55
|
+
const { call, get } = fakeCall('current-session');
|
|
56
|
+
|
|
57
|
+
await call.getRingState('ended-session');
|
|
58
|
+
|
|
59
|
+
expect(get).toHaveBeenCalledWith(`/call/test/${callId}/ring_state`, {
|
|
60
|
+
call_session_id: 'ended-session',
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('returns the coordinator response', async () => {
|
|
65
|
+
const { call, get } = fakeCall('session-1');
|
|
66
|
+
get.mockResolvedValue(
|
|
67
|
+
fromPartial({
|
|
68
|
+
session_id: 'session-1',
|
|
69
|
+
accepted_by: { bob: '2026-08-24T10:00:04Z' },
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
await expect(call.getRingState()).resolves.toMatchObject({
|
|
74
|
+
session_id: 'session-1',
|
|
75
|
+
accepted_by: { bob: '2026-08-24T10:00:04Z' },
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('rejects when the call has no session to read', async () => {
|
|
80
|
+
const { call, get } = fakeCall();
|
|
81
|
+
|
|
82
|
+
await expect(call.getRingState()).rejects.toThrow(
|
|
83
|
+
'Cannot read the ring state: the call has no session',
|
|
84
|
+
);
|
|
85
|
+
expect(get).not.toHaveBeenCalled();
|
|
86
|
+
});
|
|
87
|
+
});
|