Skip to content

Commit a5a0333

Browse files
authored
Fix abort race (#1695)
1 parent 9466637 commit a5a0333

File tree

3 files changed

+37
-8
lines changed

3 files changed

+37
-8
lines changed

.changeset/happy-camels-turn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"livekit-client": patch
3+
---
4+
5+
Fix abort race resulting in multiple code paths trying to close the ws connection

src/api/SignalClient.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,12 @@ import type { LoggerOptions } from '../room/types';
5151
import { getClientInfo, isReactNative, sleep } from '../room/utils';
5252
import { AsyncQueue } from '../utils/AsyncQueue';
5353
import { type WebSocketConnection, WebSocketStream } from './WebSocketStream';
54-
import { createRtcUrl, createValidateUrl, parseSignalResponse } from './utils';
54+
import {
55+
createRtcUrl,
56+
createValidateUrl,
57+
getAbortReasonAsString,
58+
parseSignalResponse,
59+
} from './utils';
5560

5661
// internal options
5762
interface ConnectOpts extends SignalOptions {
@@ -295,10 +300,12 @@ export class SignalClient {
295300
const combinedAbort = AbortSignal.any(signals);
296301

297302
const abortHandler = async (event: Event) => {
303+
const target = event.currentTarget;
304+
const reason = getAbortReasonAsString(target, 'Abort handler called');
298305
// send leave if we have an active stream writer (connection is open)
299-
if (this.streamWriter) {
306+
if (this.streamWriter && !this.isDisconnected) {
300307
this.sendLeave()
301-
.then(() => this.close())
308+
.then(() => this.close(reason))
302309
.catch((e) => {
303310
this.log.error(e);
304311
this.close();
@@ -307,7 +314,6 @@ export class SignalClient {
307314
this.close();
308315
}
309316
clearTimeout(wsTimeout);
310-
const target = event.currentTarget;
311317
reject(target instanceof AbortSignal ? target.reason : target);
312318
};
313319

@@ -341,7 +347,7 @@ export class SignalClient {
341347
if (this.ws) {
342348
await this.close(false);
343349
}
344-
this.ws = new WebSocketStream<ArrayBuffer>(rtcUrl, { signal: combinedAbort });
350+
this.ws = new WebSocketStream<ArrayBuffer>(rtcUrl);
345351

346352
try {
347353
this.ws.closed
@@ -482,15 +488,15 @@ export class SignalClient {
482488
this.onMediaSectionsRequirement = undefined;
483489
};
484490

485-
async close(updateState: boolean = true) {
491+
async close(updateState: boolean = true, reason = 'Close method called on signal client') {
486492
const unlock = await this.closingLock.lock();
487493
try {
488494
this.clearPingInterval();
489495
if (updateState) {
490496
this.state = SignalConnectionState.DISCONNECTING;
491497
}
492498
if (this.ws) {
493-
this.ws.close({ closeCode: 1000, reason: 'Close method called on signal client' });
499+
this.ws.close({ closeCode: 1000, reason });
494500

495501
// calling `ws.close()` only starts the closing handshake (CLOSING state), prefer to wait until state is actually CLOSED
496502
const closePromise = this.ws.closed;
@@ -818,7 +824,7 @@ export class SignalClient {
818824
private async handleOnClose(reason: string) {
819825
if (this.state === SignalConnectionState.DISCONNECTED) return;
820826
const onCloseCallback = this.onClose;
821-
await this.close();
827+
await this.close(undefined, reason);
822828
this.log.debug(`websocket connection closed: ${reason}`, { ...this.logContext, reason });
823829
if (onCloseCallback) {
824830
onCloseCallback(reason);

src/api/utils.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,21 @@ export function parseSignalResponse(value: ArrayBuffer | string) {
3131
}
3232
throw new Error(`could not decode websocket message: ${typeof value}`);
3333
}
34+
35+
export function getAbortReasonAsString(
36+
signal: AbortSignal | unknown,
37+
defaultMessage = 'Unknown reason',
38+
) {
39+
if (!(signal instanceof AbortSignal)) {
40+
return defaultMessage;
41+
}
42+
const reason = signal.reason;
43+
switch (typeof reason) {
44+
case 'string':
45+
return reason;
46+
case 'object':
47+
return reason instanceof Error ? reason.message : defaultMessage;
48+
default:
49+
return 'toString' in reason ? reason.toString() : defaultMessage;
50+
}
51+
}

0 commit comments

Comments
 (0)