Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/relayer/WebSocketRelayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export class WebSocketRelayer {
private lockQueue: Array<() => void> = [];
private connectPromise: Promise<void> | null = null;
private reconnectAttempts = 0;
private reconnectEnabled = true;
private maxReconnectAttempts: number;
private reconnectDelayMs: number;
private pendingMessages: WebSocketMessage[] = [];
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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();
}
};
Expand Down Expand Up @@ -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)
);
Expand Down Expand Up @@ -275,7 +278,9 @@ export class WebSocketRelayer {
}

disconnect(): void {
this.reconnectEnabled = false;
this.connectPromise = null;
this.reconnectAttempts = 0;

if (this.ws) {
try {
Expand All @@ -289,6 +294,7 @@ export class WebSocketRelayer {

destroy(): void {
this.isDestroyed = true;
this.reconnectEnabled = false;
this.connectPromise = null;
this.pendingMessages = [];
this.reconnectAttempts = this.maxReconnectAttempts;
Expand Down
25 changes: 24 additions & 1 deletion src/tests/relayer-stress.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}
Expand Down Expand Up @@ -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();
Expand Down
Loading