Skip to content

Commit 2b09b7c

Browse files
authored
Replace async-await-queue with mutex based queue (#694)
* selectively use supported ts libs for supported browser targets * reenable babel * update readme * fix grammar * add compat action to check es compatibility of build artificats * replace async-await-queue with mutex based queue * rename * Create rude-snails-yell.md * revert clearing queued requests * add snapshot * rename
1 parent 41d9002 commit 2b09b7c

File tree

6 files changed

+164
-9
lines changed

6 files changed

+164
-9
lines changed

.changeset/rude-snails-yell.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"livekit-client": patch
3+
---
4+
5+
Replace async-await-queue with mutex based queue

package.json

-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@
4141
"compat": "eslint --no-eslintrc --config ./.eslintrc.dist.cjs ./dist/livekit-client.umd.js"
4242
},
4343
"dependencies": {
44-
"async-await-queue": "^1.2.1",
4544
"events": "^3.3.0",
4645
"loglevel": "^1.8.0",
4746
"protobufjs": "^7.0.0",

src/AsyncQueue.test.ts

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { AsyncQueue } from './AsyncQueue';
2+
import { sleep } from './room/utils';
3+
4+
describe('asyncQueue', () => {
5+
it('runs multiple tasks in order', async () => {
6+
const queue = new AsyncQueue();
7+
const tasksExecuted: number[] = [];
8+
9+
for (let i = 0; i < 5; i++) {
10+
queue.run(async () => {
11+
await sleep(50);
12+
tasksExecuted.push(i);
13+
});
14+
}
15+
await queue.flush();
16+
expect(tasksExecuted).toMatchObject([0, 1, 2, 3, 4]);
17+
});
18+
it('runs tasks sequentially and not in parallel', async () => {
19+
const queue = new AsyncQueue();
20+
const results: number[] = [];
21+
for (let i = 0; i < 5; i++) {
22+
queue.run(async () => {
23+
results.push(i);
24+
await sleep(10);
25+
results.push(i);
26+
});
27+
}
28+
await queue.flush();
29+
expect(results).toMatchObject([0, 0, 1, 1, 2, 2, 3, 3, 4, 4]);
30+
});
31+
it('continues executing tasks if one task throws an error', async () => {
32+
const queue = new AsyncQueue();
33+
34+
let task1threw = false;
35+
let task2Executed = false;
36+
37+
queue
38+
.run(async () => {
39+
await sleep(100);
40+
throw Error('task 1 throws');
41+
})
42+
.catch(() => {
43+
task1threw = true;
44+
});
45+
46+
await queue
47+
.run(async () => {
48+
task2Executed = true;
49+
})
50+
.catch(() => {
51+
fail('task 2 should not have thrown');
52+
});
53+
54+
expect(task1threw).toBeTruthy();
55+
expect(task2Executed).toBeTruthy();
56+
});
57+
it('returns the result of the task', async () => {
58+
const queue = new AsyncQueue();
59+
60+
const result = await queue.run(async () => {
61+
await sleep(10);
62+
return 'result';
63+
});
64+
65+
expect(result).toBe('result');
66+
});
67+
it('returns only when the enqueued task and all previous tasks have completed', async () => {
68+
const queue = new AsyncQueue();
69+
const tasksExecuted: number[] = [];
70+
for (let i = 0; i < 10; i += 1) {
71+
queue.run(async () => {
72+
await sleep(10);
73+
tasksExecuted.push(i);
74+
return i;
75+
});
76+
}
77+
78+
const result = await queue.run(async () => {
79+
await sleep(10);
80+
tasksExecuted.push(999);
81+
return 'result';
82+
});
83+
84+
expect(result).toBe('result');
85+
expect(tasksExecuted).toMatchObject([...new Array(10).fill(0).map((_, idx) => idx), 999]);
86+
});
87+
it('can handle queue sizes of up to 10_000 tasks', async () => {
88+
const queue = new AsyncQueue();
89+
const tasksExecuted: number[] = [];
90+
91+
for (let i = 0; i < 10_000; i++) {
92+
queue.run(async () => {
93+
tasksExecuted.push(i);
94+
});
95+
}
96+
await queue.flush();
97+
expect(tasksExecuted).toMatchObject(new Array(10_000).fill(0).map((_, idx) => idx));
98+
});
99+
});

src/AsyncQueue.ts

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { Mutex } from './room/utils';
2+
3+
type QueueTask<T> = () => PromiseLike<T>;
4+
5+
enum QueueTaskStatus {
6+
'WAITING',
7+
'RUNNING',
8+
'COMPLETED',
9+
}
10+
11+
type QueueTaskInfo = {
12+
id: number;
13+
enqueuedAt: number;
14+
executedAt?: number;
15+
status: QueueTaskStatus;
16+
};
17+
18+
export class AsyncQueue {
19+
private pendingTasks: Map<number, QueueTaskInfo>;
20+
21+
private taskMutex: Mutex;
22+
23+
private nextTaskIndex: number;
24+
25+
constructor() {
26+
this.pendingTasks = new Map();
27+
this.taskMutex = new Mutex();
28+
this.nextTaskIndex = 0;
29+
}
30+
31+
async run<T>(task: QueueTask<T>) {
32+
const taskInfo: QueueTaskInfo = {
33+
id: this.nextTaskIndex++,
34+
enqueuedAt: Date.now(),
35+
status: QueueTaskStatus.WAITING,
36+
};
37+
this.pendingTasks.set(taskInfo.id, taskInfo);
38+
const unlock = await this.taskMutex.lock();
39+
try {
40+
taskInfo.executedAt = Date.now();
41+
taskInfo.status = QueueTaskStatus.RUNNING;
42+
return await task();
43+
} finally {
44+
taskInfo.status = QueueTaskStatus.COMPLETED;
45+
this.pendingTasks.delete(taskInfo.id);
46+
unlock();
47+
}
48+
}
49+
50+
async flush() {
51+
return this.run(async () => {});
52+
}
53+
54+
snapshot() {
55+
return Array.from(this.pendingTasks.values());
56+
}
57+
}

src/api/SignalClient.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import Queue from 'async-await-queue';
21
import 'webrtc-adapter';
2+
import { AsyncQueue } from '../AsyncQueue';
33
import log from '../logger';
44
import {
55
ClientInfo,
@@ -87,7 +87,7 @@ export class SignalClient {
8787

8888
isReconnecting: boolean;
8989

90-
requestQueue: Queue;
90+
requestQueue: AsyncQueue;
9191

9292
queuedRequests: Array<() => Promise<void>>;
9393

@@ -154,7 +154,7 @@ export class SignalClient {
154154
this.isConnected = false;
155155
this.isReconnecting = false;
156156
this.useJSON = useJSON;
157-
this.requestQueue = new Queue();
157+
this.requestQueue = new AsyncQueue();
158158
this.queuedRequests = [];
159159
this.closingLock = new Mutex();
160160
}

yarn.lock

-5
Original file line numberDiff line numberDiff line change
@@ -2616,11 +2616,6 @@ arrify@^1.0.1:
26162616
resolved "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz"
26172617
integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==
26182618

2619-
async-await-queue@^1.2.1:
2620-
version "1.2.1"
2621-
resolved "https://registry.npmjs.org/async-await-queue/-/async-await-queue-1.2.1.tgz"
2622-
integrity sha512-v2j+/EMzAnuJZ8I4570KJMFhi6G9g3WZyFh6cPnmQSJh3nLao77XpRt01kyFegQazPSKvue1yaIYDp/NfV/b0g==
2623-
26242619
async@^3.2.4:
26252620
version "3.2.4"
26262621
resolved "https://registry.npmjs.org/async/-/async-3.2.4.tgz"

0 commit comments

Comments
 (0)