ai fixes
This commit is contained in:
parent
7f62273e72
commit
bff3f5a5e9
8 changed files with 167 additions and 79 deletions
|
|
@ -246,6 +246,67 @@ describe("WebSocketManager", () => {
|
|||
await manager.stop();
|
||||
});
|
||||
|
||||
it("handles concurrent stop() calls without stranding either caller", async () => {
|
||||
// Real WebSocket.close() doesn't fire onclose synchronously, and the
|
||||
// socket stays reachable across the close handshake. Model that
|
||||
// here so the manager's `while (isWebSocketConnected)` loop is
|
||||
// actually awaiting when the second stop() races in. Static OPEN
|
||||
// is required because the manager compares readyState against
|
||||
// `factory.OPEN`.
|
||||
class AsyncCloseWebSocket extends MockWebSocket {
|
||||
public static readonly OPEN = WebSocket.OPEN;
|
||||
|
||||
public override close(code?: number, reason?: string): void {
|
||||
if (
|
||||
this.readyState === WebSocket.CLOSED ||
|
||||
(this as { _closing?: boolean })._closing === true
|
||||
) {
|
||||
return;
|
||||
}
|
||||
(this as { _closing?: boolean })._closing = true;
|
||||
setTimeout(() => {
|
||||
this.readyState = WebSocket.CLOSED;
|
||||
this.onclose?.(
|
||||
new MockCloseEvent("close", {
|
||||
code: code ?? 1000,
|
||||
reason: reason ?? ""
|
||||
})
|
||||
);
|
||||
}, 5);
|
||||
}
|
||||
}
|
||||
|
||||
const manager = new WebSocketManager(
|
||||
mockLogger,
|
||||
mockSettings,
|
||||
AsyncCloseWebSocket as unknown as typeof WebSocket
|
||||
);
|
||||
|
||||
manager.start();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
const start = Date.now();
|
||||
// Two concurrent stops mimic destroy() racing onSettingsChange.
|
||||
await Promise.all([manager.stop(), manager.stop()]);
|
||||
const elapsed = Date.now() - start;
|
||||
|
||||
// Both should resolve via the normal close path; if the second call
|
||||
// had clobbered the first's resolver, the first would have been
|
||||
// stranded until the 10s disconnect timeout.
|
||||
assert.ok(
|
||||
elapsed < 1000,
|
||||
`concurrent stop() took ${elapsed}ms — expected fast resolution`
|
||||
);
|
||||
const errorCalls = (
|
||||
mockLogger.error as unknown as { calls: unknown[] }
|
||||
).calls;
|
||||
assert.strictEqual(
|
||||
errorCalls.length,
|
||||
0,
|
||||
"no timeout-recovery error should be logged"
|
||||
);
|
||||
});
|
||||
|
||||
it("tracks message handling promises", async () => {
|
||||
const manager = new WebSocketManager(
|
||||
mockLogger,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ export class WebSocketManager {
|
|||
|
||||
private isStopped = true;
|
||||
private resolveDisconnectingPromise: null | (() => unknown) = null;
|
||||
private stopPromise: Promise<void> | null = null;
|
||||
private reconnectTimeoutId: ReturnType<typeof setTimeout> | undefined;
|
||||
private connectionTimeoutId: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
|
|
@ -58,6 +59,17 @@ export class WebSocketManager {
|
|||
}
|
||||
|
||||
public async stop(): Promise<void> {
|
||||
// Concurrent callers (e.g. destroy() and onSettingsChange) must share
|
||||
// the same disconnect; otherwise the second call would overwrite
|
||||
// resolveDisconnectingPromise and strand the first caller's await
|
||||
// until the timeout rejects.
|
||||
this.stopPromise ??= this.performStop().finally(() => {
|
||||
this.stopPromise = null;
|
||||
});
|
||||
await this.stopPromise;
|
||||
}
|
||||
|
||||
private async performStop(): Promise<void> {
|
||||
const { promise, resolve } = Promise.withResolvers<undefined>();
|
||||
this.resolveDisconnectingPromise = (): void => {
|
||||
resolve(undefined);
|
||||
|
|
@ -98,7 +110,7 @@ export class WebSocketManager {
|
|||
`Error while waiting for WebSocket to close: ${String(error)}`
|
||||
);
|
||||
// Force cleanup even if close didn't work
|
||||
this.resolveDisconnectingPromise();
|
||||
this.resolveDisconnectingPromise?.();
|
||||
this.resolveDisconnectingPromise = null;
|
||||
} finally {
|
||||
// Clear timeout to prevent unhandled rejection
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue