/** * Regression test for the connect() single-flight guarantee (TECH-1468 local * validation finding): React StrictMode mounts an effect twice in dev, so two * connectPhyClient() calls race. Before the fix, the second call returned the * instance while initializeConnection was still in flight — a client whose * branch params were not set yet, so branch-gated APIs like * onWebAppSessionTerminated() threw "only available for web app sessions" * and broke every web app's vite dev loop. */ import { describe, expect, it } from 'bun:test'; import type { Socket, ManagerOptions, SocketOptions } from 'socket.io-client'; import { connectPhyClient } from '../index'; import type { WebAppSessionFetch } from '../services/web-app-session.service'; class MockWebSocket { public listeners = new Map void>>(); constructor( public url: string, _opts: Partial, ) {} on(event: string, listener: (...args: unknown[]) => void): this { const list = this.listeners.get(event) ?? []; list.push(listener); this.listeners.set(event, list); return this; } off(event: string, listener: (...args: unknown[]) => void): this { const list = this.listeners.get(event) ?? []; this.listeners.set( event, list.filter((handler) => handler !== listener), ); return this; } emit(): this { return this; } connect(): this { return this; } disconnect(): this { return this; } emitFromServer(event: string, ...args: unknown[]): void { (this.listeners.get(event) ?? []).forEach((listener) => listener(...args)); } } describe('PhyHubClient.connect single-flight', () => { it('a concurrent second connect() waits for the first initialization to finish', async () => { let initializationCompleted = false; // The mint answers only after a delay — the window in which the second // connect() used to return a half-initialized client. const slowFetch: WebAppSessionFetch = async () => { await new Promise((resolve) => setTimeout(resolve, 50)); initializationCompleted = true; return new Response( JSON.stringify({ token: 'jwt-1', refreshToken: 'grant-1', expiresIn: 900, expiresAt: new Date(Date.now() + 900 * 1000).toISOString(), deadlineInSeconds: 3600, }), { status: 200, headers: { 'Content-Type': 'application/json' } }, ); }; const ioFactory = (url: string, opts: Partial): Socket => { const socket = new MockWebSocket(url, opts); setTimeout(() => socket.emitFromServer('webAppAuthenticated', { status: 'success' }), 10); return socket as unknown as Socket; }; const params = { webApp: { urlId: 'local/dev-app', sessionBaseUrl: 'http://localhost:14400', phyhubUrl: 'http://localhost:14401', code: 'dev', fetch: slowFetch, ioFactory, storage: null, }, }; const firstConnect = connectPhyClient(params); const secondConnect = connectPhyClient(params); const secondClient = await secondConnect; // The race: before the fix this resolved BEFORE the mint finished. expect(initializationCompleted).toBe(true); const firstClient = await firstConnect; expect(secondClient).toBe(firstClient); // Branch-gated API is usable on the client either call returned. const detachTerminated = secondClient.onWebAppSessionTerminated(() => {}); detachTerminated(); firstClient.disconnect(); }); });