From 46e7e4d20543b1123dab3d44e3647d1c9a85247b Mon Sep 17 00:00:00 2001 From: silentgeckoaudit3801 Date: Wed, 29 Jul 2026 02:05:15 -0600 Subject: [PATCH] Stop reconnecting after relayer disconnect --- src/relayer/WebSocketRelayer.ts | 8 +++++++- src/tests/relayer-stress.test.ts | 25 ++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/relayer/WebSocketRelayer.ts b/src/relayer/WebSocketRelayer.ts index deaf76e6..3b19e176 100644 --- a/src/relayer/WebSocketRelayer.ts +++ b/src/relayer/WebSocketRelayer.ts @@ -24,6 +24,7 @@ export class WebSocketRelayer { private lockQueue: Array<() => void> = []; private connectPromise: Promise | null = null; private reconnectAttempts = 0; + private reconnectEnabled = true; private maxReconnectAttempts: number; private reconnectDelayMs: number; private pendingMessages: WebSocketMessage[] = []; @@ -66,6 +67,7 @@ export class WebSocketRelayer { if (this.isDestroyed) { throw new Error('WebSocketRelayer has been destroyed'); } + this.reconnectEnabled = true; if (this.connectPromise) { return this.connectPromise; } @@ -133,7 +135,7 @@ export class WebSocketRelayer { settled = true; reject(new Error(`WebSocket connection closed before opening: ${this.url}`)); } - if (!this.isDestroyed) { + if (!this.isDestroyed && this.reconnectEnabled) { this.attemptReconnect(); } }; @@ -190,6 +192,7 @@ export class WebSocketRelayer { private shouldAttemptReconnect(): boolean { return ( !this.isDestroyed && + this.reconnectEnabled && this.reconnectAttempts < this.maxReconnectAttempts && (!this.ws || this.ws.readyState !== 1) ); @@ -275,7 +278,9 @@ export class WebSocketRelayer { } disconnect(): void { + this.reconnectEnabled = false; this.connectPromise = null; + this.reconnectAttempts = 0; if (this.ws) { try { @@ -289,6 +294,7 @@ export class WebSocketRelayer { destroy(): void { this.isDestroyed = true; + this.reconnectEnabled = false; this.connectPromise = null; this.pendingMessages = []; this.reconnectAttempts = this.maxReconnectAttempts; diff --git a/src/tests/relayer-stress.test.ts b/src/tests/relayer-stress.test.ts index e3a272bf..012efb14 100644 --- a/src/tests/relayer-stress.test.ts +++ b/src/tests/relayer-stress.test.ts @@ -4,14 +4,22 @@ import { WebSocketRelayer } from '../relayer/WebSocketRelayer.js'; function createMockWs(): { mock: any; onmessage: () => Function | null } { let onmessage: Function | null = null; let onopen: Function | null = null; + let onclose: Function | null = null; const mock = { readyState: 1, send: vi.fn(), - close: vi.fn(), + close: vi.fn(() => { + mock.readyState = 3; + setTimeout(() => { + if (onclose) onclose(); + }, 0); + }), set onmessage(fn: any) { onmessage = fn; }, get onmessage() { return onmessage; }, set onopen(fn: any) { onopen = fn; if (fn) setTimeout(fn, 0); }, get onopen() { return onopen; }, + set onclose(fn: any) { onclose = fn; }, + get onclose() { return onclose; }, }; return { mock, onmessage: () => onmessage }; } @@ -173,6 +181,21 @@ describe('WebSocketRelayer — High-Concurrency Stress Tests', () => { } }); + it('does not reconnect after an intentional disconnect close event', async () => { + await relayer.connect(); + expect((global as any).WebSocket).toHaveBeenCalledTimes(1); + + relayer.disconnect(); + await new Promise((resolve) => setTimeout(resolve, 20)); + + expect((global as any).WebSocket).toHaveBeenCalledTimes(1); + expect(relayer.state.connected).toBe(false); + expect(relayer.state.reconnecting).toBe(false); + + await relayer.connect(); + expect((global as any).WebSocket).toHaveBeenCalledTimes(2); + }); + it('delivers messages to the correct handler based on type', async () => { const priceHandler = vi.fn(); const tradeHandler = vi.fn();