129 lines
3.8 KiB
TypeScript
129 lines
3.8 KiB
TypeScript
import type {
|
|
Handshake,
|
|
WsServerCallbacks,
|
|
WsServerFactory,
|
|
WsServerLike,
|
|
WsServerStartOptions,
|
|
WsServerStartResult,
|
|
WsSocketLike,
|
|
} from "../../src/types/types.js";
|
|
|
|
/**
|
|
* In-memory deterministic `WsServerLike` implementation. The
|
|
* production wiring binds the real `ws.WebSocketServer`, but unit
|
|
* tests + the `tests/integration.test.ts` 3-client scenario drive
|
|
* lifecycle events through this real-implementation fake. Per the
|
|
* CLAUDE.md "no mocks" rule, the daemon never sees `vi.mock` — only
|
|
* an alternative concrete implementation.
|
|
*/
|
|
export class FakeWsServer implements WsServerLike {
|
|
public started = false;
|
|
public stopped = false;
|
|
public lastStartOptions: WsServerStartOptions | undefined;
|
|
public callbacks: WsServerCallbacks | undefined;
|
|
public boundAddress = "127.0.0.1";
|
|
public boundPort = 0;
|
|
public startError: Error | undefined;
|
|
private readonly sockets = new Map<string, FakeWsSocket>();
|
|
|
|
public async start(
|
|
options: WsServerStartOptions
|
|
): Promise<WsServerStartResult> {
|
|
if (this.startError) throw this.startError;
|
|
this.lastStartOptions = options;
|
|
this.callbacks = options.callbacks;
|
|
this.started = true;
|
|
return { address: this.boundAddress, port: this.boundPort };
|
|
}
|
|
|
|
public async stop(): Promise<void> {
|
|
this.stopped = true;
|
|
for (const id of [...this.sockets.keys()])
|
|
this.disconnect(id, 1001, "stop");
|
|
}
|
|
|
|
public async closeAll(code = 1001, reason = "shutdown"): Promise<void> {
|
|
for (const id of [...this.sockets.keys()])
|
|
this.disconnect(id, code, reason);
|
|
}
|
|
|
|
/** Test affordance: simulate a new client connecting. */
|
|
public connect(id: string, handshake?: Partial<Handshake>): FakeWsSocket {
|
|
const socket = new FakeWsSocket(id, () =>
|
|
this.disconnect(id, 1000, "client-close")
|
|
);
|
|
this.sockets.set(id, socket);
|
|
const h: Handshake = {
|
|
headers: handshake?.headers ?? {},
|
|
remoteAddress: handshake?.remoteAddress,
|
|
url: handshake?.url ?? "/",
|
|
query: handshake?.query ?? {},
|
|
};
|
|
this.callbacks?.onConnection(socket, h);
|
|
return socket;
|
|
}
|
|
|
|
public disconnect(id: string, code = 1000, reason = "close"): void {
|
|
const socket = this.sockets.get(id);
|
|
if (!socket) return;
|
|
this.sockets.delete(id);
|
|
socket.closed = true;
|
|
this.callbacks?.onClose(id, code, reason);
|
|
}
|
|
|
|
public message(id: string, payload: string): void {
|
|
this.callbacks?.onMessage(id, payload);
|
|
}
|
|
|
|
public errorOn(id: string, err: Error): void {
|
|
this.callbacks?.onError(id, err);
|
|
}
|
|
|
|
public getSocket(id: string): FakeWsSocket | undefined {
|
|
return this.sockets.get(id);
|
|
}
|
|
|
|
public socketCount(): number {
|
|
return this.sockets.size;
|
|
}
|
|
}
|
|
|
|
export class FakeWsSocket implements WsSocketLike {
|
|
public readonly id: string;
|
|
public readonly sent: string[] = [];
|
|
public closed = false;
|
|
public readonly remoteAddress = "127.0.0.1";
|
|
private readonly onClose: () => void;
|
|
|
|
public constructor(id: string, onClose: () => void) {
|
|
this.id = id;
|
|
this.onClose = onClose;
|
|
}
|
|
|
|
public send(payload: string): void {
|
|
if (!this.closed) this.sent.push(payload);
|
|
}
|
|
|
|
public close(): void {
|
|
if (this.closed) return;
|
|
this.closed = true;
|
|
this.onClose();
|
|
}
|
|
}
|
|
|
|
/** Factory producing a fresh `FakeWsServer` per construction. */
|
|
export function makeFakeWsServerFactory(): {
|
|
readonly instances: FakeWsServer[];
|
|
readonly factory: WsServerFactory;
|
|
} {
|
|
const instances: FakeWsServer[] = [];
|
|
return {
|
|
instances,
|
|
factory: () => {
|
|
const inst = new FakeWsServer();
|
|
instances.push(inst);
|
|
return inst;
|
|
},
|
|
};
|
|
}
|