fix(web): harden typed SPA chat lifecycle
Co-Authored-By: Claude Haiku 4.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Haiku 4.5
parent
b2e005f2b4
commit
caebf9ef70
@@ -27,6 +27,11 @@ export interface FakeChatSocket {
|
||||
* missing a required field fails to compile instead of silently no-op'ing at
|
||||
* runtime.
|
||||
*/
|
||||
/** Socket.IO's built-in connection-state events. Not part of the app-level
|
||||
* ServerToClientEvents contract, but real sockets always support them and
|
||||
* `useChatConnection` registers a `disconnect` handler on the real socket. */
|
||||
type LifecycleEvent = 'connect' | 'disconnect';
|
||||
|
||||
export function createFakeChatSocket(): {
|
||||
socket: FakeChatSocket;
|
||||
listeners: Map<ServerEvent, Set<(payload: never) => void>>;
|
||||
@@ -35,6 +40,19 @@ export function createFakeChatSocket(): {
|
||||
event: K,
|
||||
payload: Parameters<ServerToClientEvents[K]>[0],
|
||||
): void;
|
||||
/** Escape hatch for malformed-payload tests: bypasses the compile-time
|
||||
* payload contract to simulate a genuinely untrusted runtime value from the
|
||||
* server, e.g. a `session:info` with a non-array `availableThinkingLevels`. */
|
||||
serverEmitRaw(event: ServerEvent, payload: unknown): void;
|
||||
/** Simulates a transient Socket.IO `disconnect` — fires any handler(s)
|
||||
* registered via `socket.on('disconnect', ...)` without clearing any
|
||||
* listeners, mirroring how a real reconnecting socket behaves. */
|
||||
simulateDisconnect(): void;
|
||||
/** Simulates socket.io-client's automatic reconnect of the *same*
|
||||
* instance after a transient disconnect: marks the socket connected again
|
||||
* and fires any handler(s) registered via `socket.on('connect', ...)`,
|
||||
* without clearing or replacing any listeners. */
|
||||
simulateReconnect(): void;
|
||||
} {
|
||||
const listeners = new Map<ServerEvent, Set<(payload: never) => void>>();
|
||||
const emitted: EmittedEvent[] = [];
|
||||
@@ -73,5 +91,35 @@ export function createFakeChatSocket(): {
|
||||
}
|
||||
}
|
||||
|
||||
return { socket, listeners, emitted, serverEmit };
|
||||
function serverEmitRaw(event: ServerEvent, payload: unknown): void {
|
||||
for (const handler of listeners.get(event) ?? []) {
|
||||
(handler as (payload: unknown) => void)(payload);
|
||||
}
|
||||
}
|
||||
|
||||
function simulateDisconnect(): void {
|
||||
socket.connected = false;
|
||||
const lifecycleKey = 'disconnect' satisfies LifecycleEvent as unknown as ServerEvent;
|
||||
for (const handler of listeners.get(lifecycleKey) ?? []) {
|
||||
(handler as () => void)();
|
||||
}
|
||||
}
|
||||
|
||||
function simulateReconnect(): void {
|
||||
socket.connected = true;
|
||||
const lifecycleKey = 'connect' satisfies LifecycleEvent as unknown as ServerEvent;
|
||||
for (const handler of listeners.get(lifecycleKey) ?? []) {
|
||||
(handler as () => void)();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
socket,
|
||||
listeners,
|
||||
emitted,
|
||||
serverEmit,
|
||||
serverEmitRaw,
|
||||
simulateDisconnect,
|
||||
simulateReconnect,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user