diff --git a/packages/void/src/runtime/ws.ts b/packages/void/src/runtime/ws.ts index d48064077..7b49086a6 100644 --- a/packages/void/src/runtime/ws.ts +++ b/packages/void/src/runtime/ws.ts @@ -186,6 +186,7 @@ export class VoidSocket

{ private readonly buffer: Array> = []; private reconnectAttempts = 0; private closedByUser = false; + private reconnectTimer: ReturnType | undefined; constructor( private readonly url: string, @@ -252,7 +253,7 @@ export class VoidSocket

{ this.reconnectAttempts += 1; const delay = this.options.reconnectDelayMs ?? 1_000; - setTimeout(() => this.connect(), delay); + this.reconnectTimer = setTimeout(() => this.connect(), delay); }, ); } @@ -267,6 +268,7 @@ export class VoidSocket

{ close(code?: number, reason?: string): void { this.closedByUser = true; + clearTimeout(this.reconnectTimer); this.socket?.close(code, reason); } @@ -306,6 +308,6 @@ export function connect( const base = resolveBaseUrl(options.baseURL); const resolvedPath = interpolateRouteParams(path, options.params); const url = new URL(resolvedPath, base); - url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'; + url.protocol = url.protocol === 'https:' || url.protocol === 'wss:' ? 'wss:' : 'ws:'; return new VoidSocket(url.toString(), options); } diff --git a/packages/void/test/unit/ws.test.ts b/packages/void/test/unit/ws.test.ts index d179c28b8..440cdcfd3 100644 --- a/packages/void/test/unit/ws.test.ts +++ b/packages/void/test/unit/ws.test.ts @@ -85,6 +85,16 @@ describe('typed websocket client', () => { expect(MockWebSocket.instances[0]!.url).toBe('wss://void.test/files/docs/start%20here'); }); + it('keeps a secure wss baseURL secure', () => { + connect('/notifications', { baseURL: 'wss://api.example.com' }); + expect(MockWebSocket.instances[0]!.url).toBe('wss://api.example.com/notifications'); + }); + + it('uses ws for an http baseURL', () => { + connect('/notifications', { baseURL: 'http://localhost:5173' }); + expect(MockWebSocket.instances[0]!.url).toBe('ws://localhost:5173/notifications'); + }); + it('buffers messages until the socket opens', () => { const socket = connect('/chat/:room', { params: { room: 'general' } }); const ws = MockWebSocket.instances[0]!; @@ -108,4 +118,32 @@ describe('typed websocket client', () => { expect(handler).toHaveBeenCalledWith({ type: 'notifications.item', title: 'done' }); }); + describe('reconnect', () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('reconnects after the server drops the socket', () => { + connect('/notifications', { reconnectDelayMs: 50 }); + MockWebSocket.instances[0]!.emit('close', { code: 1006, wasClean: false }); + + vi.advanceTimersByTime(50); + + expect(MockWebSocket.instances).toHaveLength(2); + }); + + it('does not reconnect when closed during the reconnect delay', () => { + const socket = connect('/notifications', { reconnectDelayMs: 50 }); + MockWebSocket.instances[0]!.emit('close', { code: 1006, wasClean: false }); + + socket.close(1000, 'unmount'); + vi.advanceTimersByTime(1_000); + + expect(MockWebSocket.instances).toHaveLength(1); + }); + }); });