mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 13:31:38 +00:00
fix-meeting-audio-pull-timeout-cleanup (#114526)
Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com>
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
import { setTimeout as sleep } from "node:timers/promises";
|
||||
|
||||
/** Internal pull-wait ownership used by the node-host long poll. */
|
||||
export class MeetingNodeAudioPullWaiters {
|
||||
readonly #waiters = new Set<() => void>();
|
||||
@@ -14,12 +12,19 @@ export class MeetingNodeAudioPullWaiters {
|
||||
wake = resolve;
|
||||
this.#waiters.add(wake);
|
||||
});
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
const timeout = new Promise<void>((resolve) => {
|
||||
timer = setTimeout(resolve, timeoutMs);
|
||||
});
|
||||
try {
|
||||
await Promise.race([sleep(timeoutMs), ready]);
|
||||
await Promise.race([timeout, ready]);
|
||||
} finally {
|
||||
// A stalled bridge can be polled indefinitely. Timeout must release its
|
||||
// resolver instead of retaining one waiter per empty pull.
|
||||
this.#waiters.delete(wake);
|
||||
if (timer !== undefined) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,65 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { MeetingNodeAudioPullWaiters } from "./node-audio-pull-waiters.js";
|
||||
|
||||
describe("MeetingNodeAudioPullWaiters", () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("removes a pull waiter when its timeout wins", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const waiters = new MeetingNodeAudioPullWaiters();
|
||||
const waiting = waiters.wait(250);
|
||||
const waiters = new MeetingNodeAudioPullWaiters();
|
||||
const waiting = waiters.wait(250);
|
||||
|
||||
expect(waiters.size).toBe(1);
|
||||
await vi.advanceTimersByTimeAsync(250);
|
||||
await waiting;
|
||||
expect(waiters.size).toBe(1);
|
||||
await vi.advanceTimersByTimeAsync(250);
|
||||
await waiting;
|
||||
|
||||
expect(waiters.size).toBe(0);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
expect(waiters.size).toBe(0);
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
});
|
||||
|
||||
it("cancels a pull timeout when audio wakes the waiter early", async () => {
|
||||
vi.useFakeTimers();
|
||||
const waiters = new MeetingNodeAudioPullWaiters();
|
||||
const waiting = waiters.wait(2_000);
|
||||
|
||||
expect(waiters.size).toBe(1);
|
||||
expect(vi.getTimerCount()).toBe(1);
|
||||
waiters.wake();
|
||||
await waiting;
|
||||
|
||||
expect(waiters.size).toBe(0);
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
});
|
||||
|
||||
it("releases its real timeout resource when audio wakes the waiter early", async () => {
|
||||
const activeTimeouts = () =>
|
||||
process.getActiveResourcesInfo().filter((resource) => resource === "Timeout").length;
|
||||
const baselineTimeouts = activeTimeouts();
|
||||
const waiters = new MeetingNodeAudioPullWaiters();
|
||||
const waiting = waiters.wait(2_000);
|
||||
|
||||
expect(waiters.size).toBe(1);
|
||||
expect(activeTimeouts()).toBe(baselineTimeouts + 1);
|
||||
waiters.wake();
|
||||
await waiting;
|
||||
|
||||
expect(waiters.size).toBe(0);
|
||||
expect(activeTimeouts()).toBe(baselineTimeouts);
|
||||
});
|
||||
|
||||
it("cancels every pull timeout when a wake releases concurrent waiters", async () => {
|
||||
vi.useFakeTimers();
|
||||
const waiters = new MeetingNodeAudioPullWaiters();
|
||||
const waiting = [waiters.wait(2_000), waiters.wait(2_000)];
|
||||
|
||||
expect(waiters.size).toBe(2);
|
||||
expect(vi.getTimerCount()).toBe(2);
|
||||
waiters.wake();
|
||||
await Promise.all(waiting);
|
||||
|
||||
expect(waiters.size).toBe(0);
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user