proto-socket/typescript/test/ws.test.ts
toki 4dbeecf2fb feat: high-performance-parallel-operations milestone - gateway real path and cleanup
- Add gateway real path implementation (06_gateway_real_path)
- Add gateway cleanup tasks (07+06_gateway_cleanup)
- Update Go WebSocket client and benchmark
- Update Kotlin Communicator, TcpClient, WsClient and stress tests
- Update TypeScript base_client, communicator, inbound_gateway, node variants, tcp/ws clients and tests
- Update Kotlin and TypeScript stress benchmarks
- Update roadmap milestone documentation
2026-06-03 18:05:34 +09:00

290 lines
9.3 KiB
TypeScript

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<TestData> {
return new Promise<TestData>((resolve) => {
addListenerTyped(client.communicator, TestDataSchema, (msg) => resolve(msg));
});
}
describe("WS", () => {
const cleanup: Array<() => Promise<void>> = [];
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<void>((resolve) => {
server.close(() => resolve());
}),
);
await new Promise<void>((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/);
});
});