import * as fs from "node:fs"; import * as net from "node:net"; import * as path from "node:path"; import * as tls from "node:tls"; import { fileURLToPath } from "node:url"; import { afterEach, describe, expect, test } from "vitest"; import { addListenerTyped, addRequestListenerTyped, parserFromSchema, sendRequestTyped, } from "../src/communicator.js"; import { createNodeWorkerGateway } from "../src/node_inbound_gateway.js"; import { create, TestDataSchema, type TestData } from "../src/packets/message_common_pb.js"; import { connectNodeWs, connectNodeWss, NodeWsClient } from "../src/node_ws_client.js"; import { NodeWsServer } from "../src/node_ws_server.js"; const certsDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "certs"); const cert = fs.readFileSync(path.join(certsDir, "server.crt")); const key = fs.readFileSync(path.join(certsDir, "server.key")); function parserMap() { return new Map([[TestDataSchema.typeName, parserFromSchema(TestDataSchema)]]); } async function waitForClientMessage(client: NodeWsClient): Promise { return new Promise((resolve) => { addListenerTyped(client.communicator, TestDataSchema, (msg) => resolve(msg)); }); } describe("WS", () => { const cleanup: Array<() => Promise> = []; afterEach(async () => { while (cleanup.length > 0) { const fn = cleanup.pop(); if (fn) { await fn(); } } }); test("connectWs sends and receives TestData", async () => { const server = new NodeWsServer("127.0.0.1", 0, "/", (ws) => new NodeWsClient(ws, 0, 0, parserMap())); cleanup.push(async () => server.stop()); await server.start(); server.onClientConnected = (client) => { addListenerTyped(client.communicator, TestDataSchema, async (msg) => { if (msg.index === 11 && msg.message === "hello over ws") { await client.communicator.send( create(TestDataSchema, { index: 200, message: "push from ws server" }), ); } }); }; const client = await connectNodeWs("127.0.0.1", server.port, "/", 0, 0, parserMap()); cleanup.push(async () => client.close()); const pushed = waitForClientMessage(client); await client.communicator.send(create(TestDataSchema, { index: 11, message: "hello over ws" })); await expect(pushed).resolves.toMatchObject({ index: 200, message: "push from ws server", }); }); test("sendRequest/response roundtrip over WS", async () => { const server = new NodeWsServer("127.0.0.1", 0, "/", (ws) => new NodeWsClient(ws, 0, 0, parserMap())); cleanup.push(async () => server.stop()); await server.start(); server.onClientConnected = (client) => { addRequestListenerTyped(client.communicator, TestDataSchema, (req) => create(TestDataSchema, { index: req.index * 2, message: `echo: ${req.message}`, }), ); }; const client = await connectNodeWs("127.0.0.1", server.port, "/", 0, 0, parserMap()); cleanup.push(async () => client.close()); await expect( sendRequestTyped( client.communicator, create(TestDataSchema, { index: 21, message: "request over ws" }), TestDataSchema, 500, ), ).resolves.toMatchObject({ index: 42, message: "echo: request over ws", }); }); test("connectWss sends and receives TestData", async () => { const server = new NodeWsServer("127.0.0.1", 0, "/", (ws) => new NodeWsClient(ws, 0, 0, parserMap()), { cert, key, }); cleanup.push(async () => server.stop()); await server.start(); server.onClientConnected = (client) => { addListenerTyped(client.communicator, TestDataSchema, async (msg) => { if (msg.index === 12 && msg.message === "hello over wss") { await client.communicator.send( create(TestDataSchema, { index: 201, message: "push from wss server" }), ); } }); }; const client = await connectNodeWss( "127.0.0.1", server.port, "/", { ca: cert }, 0, 0, parserMap(), ); cleanup.push(async () => client.close()); const pushed = waitForClientMessage(client); await client.communicator.send(create(TestDataSchema, { index: 12, message: "hello over wss" })); await expect(pushed).resolves.toMatchObject({ index: 201, message: "push from wss server", }); }); test("WSS sendRequest/response roundtrip", async () => { const server = new NodeWsServer("127.0.0.1", 0, "/", (ws) => new NodeWsClient(ws, 0, 0, parserMap()), { cert, key, }); cleanup.push(async () => server.stop()); await server.start(); server.onClientConnected = (client) => { addRequestListenerTyped(client.communicator, TestDataSchema, (req) => create(TestDataSchema, { index: req.index * 2, message: `wss echo: ${req.message}`, }), ); }; const client = await connectNodeWss( "127.0.0.1", server.port, "/", { ca: cert }, 0, 0, parserMap(), ); cleanup.push(async () => client.close()); await expect( sendRequestTyped( client.communicator, create(TestDataSchema, { index: 22, message: "request over wss" }), TestDataSchema, 500, ), ).resolves.toMatchObject({ index: 44, message: "wss echo: request over wss", }); }); test("concurrent sendRequest/response roundtrip over WS", async () => { const server = new NodeWsServer("127.0.0.1", 0, "/", (ws) => new NodeWsClient(ws, 0, 0, parserMap())); cleanup.push(async () => server.stop()); await server.start(); server.onClientConnected = (client) => { addRequestListenerTyped(client.communicator, TestDataSchema, (req) => create(TestDataSchema, { index: req.index * 2, message: `echo: ${req.message}`, }), ); }; const client = await connectNodeWs("127.0.0.1", server.port, "/", 0, 0, parserMap()); cleanup.push(async () => client.close()); const results = await Promise.all( Array.from({ length: 5 }, (_, i) => sendRequestTyped( client.communicator, create(TestDataSchema, { index: 30 + i, message: `request ${i}` }), TestDataSchema, 2000, ), ), ); results.forEach((res, i) => { expect(res.index).toBe((30 + i) * 2); expect(res.message).toBe(`echo: request ${i}`); }); }); test( "WS read loop drives server inbound gateway: concurrent request-response keeps FIFO/nonce", async () => { const server = new NodeWsServer("127.0.0.1", 0, "/", (ws) => new NodeWsClient(ws, 0, 0, parserMap())); cleanup.push(async () => server.stop()); await server.start(); let lastNonce = 0; let fifoViolation = false; server.onClientConnected = (client) => { const comm = client.communicator; // 실제 WS read loop가 onReceivedFrame으로 흘린 raw binary message를 worker_threads gateway가 // off-thread로 decode하고, reorder 후 coordinator가 FIFO로 dispatch하는지 검증한다. comm.attachInboundGateway( createNodeWorkerGateway({ sink: (frame) => comm.enqueueInbound(frame.typeName, frame.data, frame.incomingNonce, frame.responseNonce), workers: 2, }), ); // raw addRequestListener로 nonce를 받아 gateway reorder 이후에도 connection별 nonce가 // 단조 증가(FIFO)하는지 확인한다. comm.addRequestListener(TestDataSchema.typeName, (msg, nonce) => { if (nonce < lastNonce) { fifoViolation = true; } lastNonce = nonce; const req = msg as TestData; return create(TestDataSchema, { index: req.index * 2, message: `gw echo: ${req.message}` }); }); }; const client = await connectNodeWs("127.0.0.1", server.port, "/", 0, 0, parserMap()); cleanup.push(async () => client.close()); // worker_threads gateway는 첫 submit 시 worker pool을 lazily spawn하므로 cold-start를 흡수할 수 // 있게 요청 timeout을 넉넉히 둔다(기존 worker_threads 단위 테스트와 동일한 여유). const results = await Promise.all( Array.from({ length: 16 }, (_, i) => sendRequestTyped( client.communicator, create(TestDataSchema, { index: i, message: `req ${i}` }), TestDataSchema, 8000, ), ), ); results.forEach((res, i) => { expect(res.index).toBe(i * 2); expect(res.message).toBe(`gw echo: req ${i}`); }); expect(fifoViolation).toBe(false); }, 15000, ); test("connectNodeWss rejects when peer never completes WebSocket handshake", async () => { const server = tls.createServer({ cert, key }, (socket) => { socket.on("data", () => {}); }); cleanup.push( () => new Promise((resolve) => { server.close(() => resolve()); }), ); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); const port = (server.address() as net.AddressInfo).port; await expect( connectNodeWss("127.0.0.1", port, "/", { ca: cert }, 0, 0, parserMap()), ).rejects.toThrow(/websocket handshake timed out/); }); });