From b8df24a0bef808da1f3d05dedbfb86b2bdbbda57 Mon Sep 17 00:00:00 2001 From: gikkman Date: Mon, 8 Apr 2024 17:00:18 +0200 Subject: [PATCH 1/3] WIP: Allow external WS or WSS --- src/TipcBrowserClient.ts | 79 ++++++++++++++++-------- src/TipcNodeClient.ts | 79 ++++++++++++++++-------- src/TipcNodeServer.ts | 118 ++++++++++++++++++++++++++++++------ src/TipcTypes.ts | 22 +++++-- src/TipcTypesNodeOnly.ts | 2 + test/Helper.test.ts | 54 +++++++++++++++++ test/TipcNodeClient.test.ts | 84 +++++++++++-------------- test/TipcNodeServer.test.ts | 79 ++++++++++++++++-------- 8 files changed, 366 insertions(+), 151 deletions(-) diff --git a/src/TipcBrowserClient.ts b/src/TipcBrowserClient.ts index 6d80d23..c1fff4b 100644 --- a/src/TipcBrowserClient.ts +++ b/src/TipcBrowserClient.ts @@ -8,36 +8,57 @@ import { Callback, TipcClient, TipcConnectionManager, TipcAddressInfo, + TipcConnectionDetails, TipcClientOptions } from "./TipcTypes"; export class TipcBrowserClient implements TipcClient { protected readonly logger: TipcLogger; - protected readonly host: string; - protected readonly port: number; - protected readonly path: string; - protected readonly protocol: string; protected readonly tipcListenerComponent: TipcListenerComponent; + protected readonly url: string; private readonly usedNamespaces = new Set(); protected ws?: WebSocket; private onDisconnectCallback?: Callback; - private constructor(options: TipcClientOptions) { - this.host = options.host; - this.port = options.port; - this.path = options.path ?? ""; - this.protocol = options.protocol ?? "ws"; + private constructor(options: TipcConnectionDetails&TipcClientOptions) { + if("url" in options) { + this.url = options.url; + } + else { + this.url = `${options.protocol??"ws"}://${options.host}:${options.port}${options.path??""}`; + } this.onDisconnectCallback = options.onDisconnect; this.logger = new TipcLogger({messagePrefix: "[Tipc Client]", ...options.loggerOptions}); this.tipcListenerComponent = new TipcListenerComponent(this.logger); } - public static create(options: TipcClientOptions): TipcConnectionManager { + public static create(options: TipcConnectionDetails&TipcClientOptions): TipcConnectionManager { const instance = new TipcBrowserClient(options); return instance; } - public getAddressInfo(): TipcAddressInfo { - return {address: this.host, port: this.port}; + public static from(websocket: WebSocket, options?: TipcClientOptions) { + const instance = new TipcBrowserClient({url: websocket.url, ...options}); + instance.ws = websocket; + instance.initWs(websocket); + return instance; + } + + public getAddressInfo(): TipcAddressInfo | undefined { + const url = this.getUrl(); + if(!url) { + return; + } + try { + const parsed = new URL(url); + return {address: parsed.hostname, port: parseInt(parsed.port)}; + } + catch { + return undefined; + } + } + + public getUrl(): string { + return this.url; } public isConnected(): boolean { @@ -59,8 +80,7 @@ export class TipcBrowserClient implements TipcClient { if(this.isConnected()) { return this; } - const url = `${this.protocol}://${this.host}:${this.port}${this.path}`; - this.ws = await this.initWs(url); + this.ws = await this.initWs(this.url); return this; } @@ -81,9 +101,11 @@ export class TipcBrowserClient implements TipcClient { }); } - private initWs(url: string) { - const ws = new WebSocket(url); - let hasBeenConnected = false; + private initWs(urlOrWebsocket: string|WebSocket) { + const ws = typeof urlOrWebsocket === "string" + ? new WebSocket(urlOrWebsocket) + : urlOrWebsocket; + let hasBeenConnected = ws.readyState === WebSocket.OPEN; ws.addEventListener('error', (ev) => { this.logger.error('Error: %s', ev); @@ -118,16 +140,21 @@ export class TipcBrowserClient implements TipcClient { }); return new Promise((resolve, reject) => { - const onError = (ev: Event) => { - reject(ev); - }; - ws.addEventListener('error', onError); - ws.addEventListener('open', () => { - ws.removeEventListener('error', onError); - hasBeenConnected = true; - this.logger.info("Websocket connection established: %s", url); + if(ws.readyState === WebSocket.OPEN) { resolve(ws); - }); + } + else { + const onError = (ev: Event) => { + reject(ev); + }; + ws.addEventListener('error', onError); + ws.addEventListener('open', () => { + ws.removeEventListener('error', onError); + hasBeenConnected = true; + this.logger.info("Websocket connection established: %s", urlOrWebsocket); + resolve(ws); + }); + } }); } diff --git a/src/TipcNodeClient.ts b/src/TipcNodeClient.ts index 668dabc..a1c6e79 100644 --- a/src/TipcNodeClient.ts +++ b/src/TipcNodeClient.ts @@ -10,36 +10,57 @@ import { Callback, TipcClient, TipcConnectionManager, TipcAddressInfo, + TipcConnectionDetails, TipcClientOptions } from "./TipcTypes"; export class TipcNodeClient implements TipcClient { protected readonly logger: TipcLogger; - protected readonly host: string; - protected readonly port: number; - protected readonly path: string; - protected readonly protocol: string; protected readonly tipcListenerComponent: TipcListenerComponent; + protected readonly url: string; private readonly usedNamespaces = new Set(); protected ws?: WebSocket; private onDisconnectCallback?: Callback; - private constructor(options: TipcClientOptions) { - this.host = options.host; - this.port = options.port; - this.path = options.path ?? ""; - this.protocol = options.protocol ?? "ws"; + private constructor(options: TipcConnectionDetails&TipcClientOptions) { + if("url" in options) { + this.url = options.url; + } + else { + this.url = `${options.protocol??"ws"}://${options.host}:${options.port}${options.path??""}`; + } this.onDisconnectCallback = options.onDisconnect; this.logger = new TipcLogger({messagePrefix: "[Tipc Client]", ...options.loggerOptions}); this.tipcListenerComponent = new TipcListenerComponent(this.logger); } - public static create(options: TipcClientOptions): TipcConnectionManager { + public static create(options: TipcConnectionDetails&TipcClientOptions): TipcConnectionManager { const instance = new TipcNodeClient(options); return instance; } - public getAddressInfo(): TipcAddressInfo { - return {address: this.host, port: this.port}; + public static from(websocket: WebSocket, options?: TipcClientOptions) { + const instance = new TipcNodeClient({url: websocket.url, ...options}); + instance.ws = websocket; + instance.initWs(websocket); + return instance; + } + + public getAddressInfo(): TipcAddressInfo | undefined { + const url = this.getUrl(); + if(!url) { + return; + } + try { + const parsed = new URL(url); + return {address: parsed.hostname, port: parseInt(parsed.port)}; + } + catch { + return undefined; + } + } + + public getUrl(): string { + return this.url; } public isConnected(): boolean { @@ -61,8 +82,7 @@ export class TipcNodeClient implements TipcClient { if(this.isConnected()) { return this; } - const url = `${this.protocol}://${this.host}:${this.port}${this.path}`; - this.ws = await this.initWs(url); + this.ws = await this.initWs(this.url); return this; } @@ -83,9 +103,11 @@ export class TipcNodeClient implements TipcClient { }); } - private initWs(url: string) { - const ws = new WebSocket(url); - let hasBeenConnected = false; + private initWs(urlOrWebsocket: string|WebSocket) { + const ws = typeof urlOrWebsocket === "string" + ? new WebSocket(urlOrWebsocket, {perMessageDeflate: false}) + : urlOrWebsocket; + let hasBeenConnected = ws.readyState === WebSocket.OPEN; ws.on('error', (err) => { this.logger.error('Error: %s', err.message); @@ -120,16 +142,21 @@ export class TipcNodeClient implements TipcClient { }); return new Promise((resolve, reject) => { - const onError = (err: Error) => { - reject(err); - }; - ws.on('error', onError); - ws.on('open', () => { - ws.off('error', onError); - hasBeenConnected = true; - this.logger.info("Websocket connection established: %s", url); + if(ws.readyState === WebSocket.OPEN) { resolve(ws); - }); + } + else { + const onError = (err: Error) => { + reject(err); + }; + ws.on('error', onError); + ws.on('open', () => { + ws.off('error', onError); + hasBeenConnected = true; + this.logger.info("Websocket connection established: %s", urlOrWebsocket); + resolve(ws); + }); + } }); } diff --git a/src/TipcNodeServer.ts b/src/TipcNodeServer.ts index 89b0126..304087d 100644 --- a/src/TipcNodeServer.ts +++ b/src/TipcNodeServer.ts @@ -1,3 +1,5 @@ +import { nextTick } from "node:process"; +import { createServer, Server } from "node:http"; import { WebSocketServer, WebSocket, AddressInfo } from "ws"; import { makeKey, makeTipcErrorObject, makeTipcSendObject, validateMessageObject } from "./TipcCommon"; import { TipcListenerComponent } from "./TipcListenerComponent"; @@ -13,12 +15,13 @@ import { Callback, TipcServer, TipcConnectionManager } from "./TipcTypes"; import { TipcServerOptions } from "./TipcTypesNodeOnly"; -import { nextTick } from "process"; +import { TipcNodeClient } from "./TipcNodeClient"; export type { TipcServerOptions } from "./TipcTypesNodeOnly"; export class TipcNodeServer implements TipcServer { private wss?: WebSocketServer; + private server?: Server; private options: TipcServerOptions; private logger: TipcLogger; private usedNamespaces = new Set(); @@ -38,7 +41,7 @@ export class TipcNodeServer implements TipcServer { } public getAddressInfo() { - const address = this.wss?.address(); + const address = this.server?.address(); if(address) { return address as AddressInfo; } @@ -56,17 +59,34 @@ export class TipcNodeServer implements TipcServer { return new TipcNamespaceServerImpl(this, namespace); } + public isOpen(): boolean { + return !!this.getAddressInfo(); + } + public async connect(): Promise { await this.initWss(); return this; } - public async shutdown(): Promise { + /** + * Closes the TipcServer, including both the HTTP server and Websocket server it relies on. + * + * If you have supplied an external HTTP server to the TipcServer, this method will attempt to close that server too. + * In those cases, it is best to call this method from the `close` event handler of the server: + * ``` + * server.on('close', () => tipcServer.shutdown()) + * ``` + * @param gracefulShutdownTimeMs Time to allow client connections to close gracefully, before they are force closed + * @returns + */ + public async shutdown(gracefulShutdownTimeMs = 5000): Promise { + this.logger.info("Shutting down TipcNodeServer"); return new Promise((resolve) => { - if(!this.wss) { + if(!this.server || !this.wss) { resolve(undefined); } else { + this.server.close(); this.wss.close(); this.wss.clients.forEach(ws => { ws.close(); @@ -79,9 +99,11 @@ export class TipcNodeServer implements TipcServer { done = done && (ws.readyState !== WebSocket.CLOSED); }); if(done) { + this.logger.debug("All client connections appear closed"); resolve(undefined); } - else if(timer > 5_000) { + else if(timer > gracefulShutdownTimeMs) { + this.logger.warn("Graceful shutdown delay exceeded. Terminating remaining client connects"); this.wss?.clients.forEach(ws => { if(ws.readyState !== WebSocket.CLOSED) { ws.terminate(); @@ -90,13 +112,14 @@ export class TipcNodeServer implements TipcServer { resolve(undefined); } else { + this.logger.debug("Some client connections still open. Waiting 100ms"); setTimeout(() => { - timer += 50; + timer += 100; checkClosed(); - }, 50); + }, 100); } }; - checkClosed(); + nextTick(() => checkClosed()); } }); } @@ -104,13 +127,22 @@ export class TipcNodeServer implements TipcServer { ///////////////////////////////////////////////////////////// // Websocket stuff //////////////////////////////////////////////////////////// - private initWss() { + private async initWss() { if("noWsServer" in this.options) { return new Promise(r => r()); } - const clientAlive: Map = new Map(); - const wss = new WebSocketServer(this.options); + + const wss = new WebSocketServer({...this.options, port: undefined, host: undefined, server: undefined, noServer: true}); + const server = this.setupServer(); + + server.on('upgrade', (request, socket, head) => { + this.logger.debug("New client upgrade request detected"); + wss.handleUpgrade(request, socket, head, (ws) => { + wss.emit('connection', ws, request); + }); + }); + wss.on("connection", (ws, req) => { clientAlive.set(ws, true); ws.on("pong", () => clientAlive.set(ws, true)); @@ -133,20 +165,41 @@ export class TipcNodeServer implements TipcServer { this.logger.debug("Client disconnected"); clientAlive.delete(ws); }); - ws.onerror = (err) => { + ws.on('error', (err) => { this.logger.debug("Client error [%s]", err.message); - }; + }); - const callback = this.options.onNewConnection; + const callback = this.options.onClientConnect; if(callback) { - nextTick(() => callback(ws, req)); + // req.url is always set here, since the request originates from the Server instance + const url = req.url?.startsWith("/") ? new URL(req.url, `http://${req.headers.host}`) : new URL(req.url ?? ""); + const client = TipcNodeClient.from(ws); + nextTick(() => { + try { + callback(client, url, req); + } + catch (ex) { + this.logger.error("Error in onClientConnect callback: %s", ex); + } + }); + } + const oldCb = this.options.onNewConnection; + if(oldCb) { + nextTick(() => { + try { + oldCb(ws, req); + } + catch (ex) { + this.logger.error("Error in onNewConnection callback: %s", ex); + } + }); } }); /** * This timeout function will periodically mark all clients to "not alive", then send a ping * request to the client. A response to the ping (a pong), or any message from the client, will - * mark the clientas alive again. + * mark the client as alive again. * IF the client is still marked as "not alive" the next time the function is called, the * websocket will be terminated and cleaned up */ @@ -176,11 +229,19 @@ export class TipcNodeServer implements TipcServer { }); return new Promise((resolve) => { - wss.on("listening", () => { - this.logger.info("Websocket Server opened"); + if(server.listening) { this.wss = wss; + this.server = server; resolve(); - }); + } + else { + server.on("listening", () => { + this.logger.info("Websocket Server opened"); + this.wss = wss; + this.server = server; + resolve(); + }); + } }); } @@ -193,6 +254,25 @@ export class TipcNodeServer implements TipcServer { } } + private setupServer(): Server { + if('server' in this.options) { + return this.options.server; + } + if('port' in this.options && 'host' in this.options) { + const server = createServer(); + server.listen(this.options.port, this.options.host); + return server; + } + throw new Error("Invalid server options. The options must have either 'server' or 'port'+'host' defined"); + } + + private setupCloseFunction(options: TipcServerOptions, server: Server, wss: WebSocketServer): Function { + if('server' in options) { + return () => wss.close(); + } + return () => server.close(); + } + ///////////////////////////////////////////////////////////// // Event listeners //////////////////////////////////////////////////////////// diff --git a/src/TipcTypes.ts b/src/TipcTypes.ts index 658a21d..8fcd622 100644 --- a/src/TipcTypes.ts +++ b/src/TipcTypes.ts @@ -19,7 +19,6 @@ type Funcify = { export type Args = T[K] extends (...args: infer A) => any ? A : never; export type Ret = T[K] extends (...args: any) => infer U ? U : never; export type Typings = Funcify> = Args; - ///////////////////////////////////////////////////////////////////////////// // Interface types ///////////////////////////////////////////////////////////////////////////// @@ -76,20 +75,26 @@ export interface TipcConnectionManager { export interface TipcServer { getAddressInfo(): TipcAddressInfo|undefined shutdown(): Promise, + isOpen(): boolean, forContractAndNamespace(namespace: string & (T extends object ? string : never)): TipcNamespaceServer } /** - * Represents an established TipcClient connection. To be able to add/call listeners, you need to create a namespaced instance, using the + * A TipcClient connection. To be able to add/call listeners, you need to create a namespaced instance, using the * `forContractAndNamespace` method. */ export interface TipcClient { - getAddressInfo(): TipcAddressInfo|undefined + /** Get the URL that the underlying Websocket references */ + getUrl(): string, + /** Closes the underlying Websocket (without calling onDisconnect callbacks) */ shutdown(): Promise, isConnected(): boolean, + /** Creates a namespaced TipcClient, on which you can attach topic listeners */ forContractAndNamespace(namespace: string & (T extends object ? string : never)): TipcNamespaceClient, + /** If the underlying Websocket is disconnected, create a new Websocket and go through the connect process */ reconnect(): Promise, + /** @deprecate Use getUrl instead */ + getAddressInfo(): TipcAddressInfo|undefined } - ///////////////////////////////////////////////////////////////////////////// // Support types ///////////////////////////////////////////////////////////////////////////// @@ -125,11 +130,16 @@ export type TipcErrorObject = { } & TipcMessageBase; export type TipcMessageObject = TipcSendObject | TipcInvokeObject | TipcErrorObject; -export type TipcClientOptions = { +export type TipcConnectionDetails = { + url: string +} | { host: string, port: number, path?: string, - protocol?: "ws"|"wss", + protocol?: string, +} + +export type TipcClientOptions = { onDisconnect?: () => void, loggerOptions?: TipcLoggerOptions } diff --git a/src/TipcTypesNodeOnly.ts b/src/TipcTypesNodeOnly.ts index d094c13..31ccf77 100644 --- a/src/TipcTypesNodeOnly.ts +++ b/src/TipcTypesNodeOnly.ts @@ -2,6 +2,7 @@ import {Server as HTTPServer, IncomingMessage} from 'node:http'; import {Server as HTTPSServer} from 'node:http'; import { TipcLoggerOptions } from './TipcLogger'; import WebSocket from 'ws'; +import { TipcNodeClient } from './TipcNodeClient'; /** * --Tipc Server Options-- @@ -23,6 +24,7 @@ import WebSocket from 'ws'; export type TipcServerOptions = { clientTimeoutMs?: number, onNewConnection?: (ws: WebSocket, request: IncomingMessage) => any, + onClientConnect?: (client: TipcNodeClient, connectUrl: URL, connectRequest: IncomingMessage, ) => unknown, loggerOptions?: TipcLoggerOptions } & diff --git a/test/Helper.test.ts b/test/Helper.test.ts index 3544d24..dbc9242 100644 --- a/test/Helper.test.ts +++ b/test/Helper.test.ts @@ -1,3 +1,8 @@ +import { TipcNodeClient } from "../src/TipcNodeClient"; +import { TipcNodeServer } from "../src/TipcNodeServer"; +import { Callback, TipcClient, TipcClientOptions, TipcConnectionDetails, TipcNamespaceClient, TipcNamespaceServer, TipcServer } from "../src/TipcTypes"; +import { TipcServerOptions } from "../src/TipcTypesNodeOnly"; + export async function sleep(ms: number) { return new Promise(r => { setTimeout(() => { @@ -5,3 +10,52 @@ export async function sleep(ms: number) { }, ms); }); } + + + +export type AnyInterface = Record +export type CallbackInterface = Record + +let m_server_core: TipcServer|undefined; +let m_client_core: TipcClient|undefined; +let m_server: TipcNamespaceServer|undefined; +let m_clientA: TipcNamespaceClient|undefined; +let m_clientB: TipcNamespaceClient|undefined; + +export function getTestServerCore() { + return m_server_core; +} + +export const setupServerClient = async (namespaceServer= "default", + namespaceClientA = "default", + namespaceClientB = "default", + extraServerOptions: Partial = {}, + extraClientOptions: Partial = {}, +): Promise<[TipcNamespaceServer, TipcNamespaceClient, TipcNamespaceClient]> => { + m_server_core = await TipcNodeServer.create({ + host:"localhost", port: 0, loggerOptions: {logLevel: "OFF"}, ...extraServerOptions, + }).connect(); + m_server = m_server_core.forContractAndNamespace(namespaceServer); + + const address = m_server_core.getAddressInfo(); + if(!address) { + throw "Address undefined"; + } + + m_client_core = await TipcNodeClient.create({ + host: "localhost", port: address.port, loggerOptions: {logLevel: "OFF"}, ...extraClientOptions, + }).connect(); + m_clientA = m_client_core.forContractAndNamespace(namespaceClientA); + m_clientB = m_client_core.forContractAndNamespace(namespaceClientB); + return [m_server, m_clientA, m_clientB]; +}; + +afterEach(async () => { + await m_server_core?.shutdown(); + await m_client_core?.shutdown(); + m_server_core = undefined; + m_client_core = undefined; + m_server = undefined; + m_clientA = undefined; + m_clientB = undefined; +}, 10); diff --git a/test/TipcNodeClient.test.ts b/test/TipcNodeClient.test.ts index c4da56f..5c184c1 100644 --- a/test/TipcNodeClient.test.ts +++ b/test/TipcNodeClient.test.ts @@ -1,51 +1,9 @@ -import { TipcNodeServer, TipcServerOptions } from "../src/TipcNodeServer"; +import { TipcNodeServer } from "../src/TipcNodeServer"; import { TipcNodeClient } from "../src/TipcNodeClient"; -import { sleep } from "./Helper.test"; -import { Callback, TipcClient, TipcNamespaceClient, TipcServer, TipcNamespaceServer, TipcClientOptions } from "../src/TipcTypes"; +import { AnyInterface, CallbackInterface, getTestServerCore, setupServerClient, sleep } from "./Helper.test"; +import { TipcClient } from "../src/TipcTypes"; import { TipcLoggerOptions } from "../src/TipcLogger"; - -type AnyInterface = Record -type CallbackInterface = Record - -let m_server_core: TipcServer|undefined; -let m_client_core: TipcClient|undefined; -let m_server: TipcNamespaceServer|undefined; -let m_clientA: TipcNamespaceClient|undefined; -let m_clientB: TipcNamespaceClient|undefined; - -const setupServerClient = async (namespaceServer= "default", - namespaceClientA = "default", - namespaceClientB = "default", - extraServerOptions: Partial = {}, - extraClientOptions: Partial = {}, -): Promise<[TipcNamespaceServer, TipcNamespaceClient, TipcNamespaceClient]> => { - m_server_core = await TipcNodeServer.create({ - host:"localhost", port: 0, loggerOptions: {logLevel: "OFF"}, ...extraServerOptions, - }).connect(); - m_server = m_server_core.forContractAndNamespace(namespaceServer); - - const address = m_server_core.getAddressInfo(); - if(!address) { - throw "Address undefined"; - } - - m_client_core = await TipcNodeClient.create({ - host: "localhost", port: address.port, loggerOptions: {logLevel: "OFF"}, ...extraClientOptions, - }).connect(); - m_clientA = m_client_core.forContractAndNamespace(namespaceClientA); - m_clientB = m_client_core.forContractAndNamespace(namespaceClientB); - return [m_server, m_clientA, m_clientB]; -}; - -afterEach(async () => { - await m_server_core?.shutdown(); - await m_client_core?.shutdown(); - m_server_core = undefined; - m_client_core = undefined; - m_server = undefined; - m_clientA = undefined; - m_clientB = undefined; -}, 10); +import { WebSocket } from "ws"; describe("Test TipcNodeClient.addListener()", () => { it("will call client-side event listener in same namespace", async () => { @@ -319,7 +277,7 @@ describe("Test TipcNodeClient.isConnected", () => { it("returns false if not connected", async () => { const [_, client] = await setupServerClient(); - await m_server_core?.shutdown(); + await getTestServerCore()?.shutdown(); await sleep(100); expect(client.isConnected()).toBeFalse(); }); @@ -332,7 +290,7 @@ describe("Test TipcNodeClient.onDisconnect", () => { called = true; }; await setupServerClient("ns", "ns", "", {}, {onDisconnect}); - await m_server_core?.shutdown(); + await getTestServerCore()?.shutdown(); await sleep(5); expect(called).toBeTrue(); }); @@ -465,3 +423,33 @@ describe("Test TipcNodeClient.reconnect", () => { await server2.shutdown(); }); }); + +describe("Test TipcNodeClient.from", () => { + it("will see that the external websocket is connected/disconnected", async () => { + const [server] = await setupServerClient("ns"); + const ws = new WebSocket("ws://localhost:" + server.getAddressInfo()?.port); + await sleep(5); + expect(ws.readyState).toBe(WebSocket.OPEN); + const client = TipcNodeClient.from(ws).forContractAndNamespace("ns"); + expect(client.isConnected()).toBeTrue(); + ws.close(); + await sleep(5); + expect(client.isConnected()).toBeFalse(); + }); + + it("will attach listeners to external websocket", async () => { + const [server] = await setupServerClient("ns"); + const ws = new WebSocket("ws://localhost:" + server.getAddressInfo()?.port); + await sleep(5); + expect(ws.readyState).toBe(WebSocket.OPEN); + + const client = TipcNodeClient.from(ws).forContractAndNamespace("ns"); + let counter = 0; + client.addListener("topic", () => counter++); + server.send("topic"); + await sleep(5); + expect(counter).toBe(1); + + ws.close(); + }); +}); diff --git a/test/TipcNodeServer.test.ts b/test/TipcNodeServer.test.ts index 0ad8e8b..9be9645 100644 --- a/test/TipcNodeServer.test.ts +++ b/test/TipcNodeServer.test.ts @@ -1,9 +1,9 @@ -import { WebSocket } from "ws"; +import { createServer } from "http"; import { TipcLoggerOptions } from "../src/TipcLogger"; import { TipcNodeClient } from "../src/TipcNodeClient"; import { TipcNodeServer } from "../src/TipcNodeServer"; import { TipcClient, TipcServer } from "../src/TipcTypes"; -import { sleep } from "./Helper.test"; +import { setupServerClient, sleep } from "./Helper.test"; type AnyInterface = Record @@ -11,6 +11,27 @@ function basicServer() { return TipcNodeServer.create({host:"localhost", port:0, loggerOptions: {logLevel: "OFF"}}); } +describe("Test TipcNodeServer.shutdown", () => { + it("closes when using internal http server", async () => { + const core = await basicServer().connect(); + expect(core.isOpen()).toBeTrue(); + await core.shutdown(); + expect(core.isOpen()).toBeFalse(); + }); + it("closes when using external server", async () => { + const server = createServer(); + server.listen(0); + await sleep(5); + const core = await TipcNodeServer.create({server, loggerOptions: {logLevel: "OFF"}}).connect(); + expect(core.isOpen()).toBeTrue(); + + server.close(); + await sleep(5); + expect(core.isOpen()).toBeFalse(); + await core.shutdown(); // Cleanup the WSS instance + }); +}); + describe("Test TipcNodeServer.addListener()", () => { it("will call server-side event listener in same namespace", async () => { const core = await basicServer().connect(); @@ -249,24 +270,12 @@ describe("Test TipcNodeServer clientTimeoutMs", () => { }); describe("Test TipcNodeServer constructor params", () => { - it("will accept client connecting on corrent path", async () => { - let server: TipcServer|undefined = undefined; - let client: TipcClient|undefined = undefined; - try { - server = await TipcNodeServer.create({host:"localhost", port:0, path:"/here", loggerOptions: {logLevel: "OFF"}}).connect(); - const port = server.getAddressInfo()?.port ?? -1; - client = await TipcNodeClient.create({host:"localhost", port, path:"/here", loggerOptions: {logLevel: "OFF"}}).connect(); - await sleep(10); - expect(client.isConnected()).toBeTrue(); - } - catch (ex) { - fail(ex); - } - finally { - await server?.shutdown(); - await client?.shutdown(); - } + it("will accept client connecting on correct path", async () => { + const [_, client] = await setupServerClient("ns", "ns", "ns", {path: "/here"}, {path: "/here"}); + await sleep(10); + expect(client.isConnected()).toBeTrue(); }); + it("will not accept client connecting on wrong path", async () => { let server: TipcServer|undefined = undefined; let client: TipcClient|undefined = undefined; @@ -284,6 +293,7 @@ describe("Test TipcNodeServer constructor params", () => { await client?.shutdown(); } }); + it("is calls onNewConnection callback", async () => { let server: TipcServer|undefined = undefined; let client1: TipcClient|undefined = undefined; @@ -311,24 +321,41 @@ describe("Test TipcNodeServer constructor params", () => { await client2?.shutdown(); } }); - it("is possible to terminate a websocket in onNewConnection callback", async () => { + + it("is possible to terminate a websocket in onClientConnect callback", async () => { + const connectCallback = (ws: TipcNodeClient) => ws.shutdown(); + const [_, client] = await setupServerClient("ns", "ns", "ns", {onClientConnect: connectCallback}, {}); + await sleep(10); + expect(client.isConnected()).toBeFalse(); + }); + + it("is possible to check query params in onClientConnect callback", async () => { let server: TipcServer|undefined = undefined; - let client: TipcClient|undefined = undefined; - const connectCallback = (ws: WebSocket) => ws.close(); + let clientA: TipcClient|undefined = undefined; + let clientB: TipcClient|undefined = undefined; + const connectCallback = (ws: TipcNodeClient, url: URL) => { + if(url.searchParams.get("foo") !== "bar") { + ws.shutdown(); + } + }; try { - server = await TipcNodeServer.create({host:"localhost", port:0, onNewConnection: connectCallback, loggerOptions: {logLevel: "OFF"}}).connect(); + server = await TipcNodeServer.create({host:"localhost", port:0, path: "/hello", + onClientConnect: connectCallback, loggerOptions: {logLevel: "OFF"}}).connect(); const port = server.getAddressInfo()?.port ?? -1; - client = await TipcNodeClient.create({host:"localhost", port, loggerOptions: {logLevel: "OFF"}}).connect(); + clientA = await TipcNodeClient.create({url:`ws://localhost:${port}/hello?foo=baz`, loggerOptions: {logLevel: "OFF"}}).connect(); + clientB = await TipcNodeClient.create({url:`ws://localhost:${port}/hello?foo=bar&baz=foz`, loggerOptions: {logLevel: "OFF"}}).connect(); await sleep(5); - expect(client.isConnected()).toBeFalse(); + expect(clientA.isConnected()).toBeFalse(); + expect(clientB.isConnected()).toBeTrue(); } catch (ex) { fail(ex); } finally { await server?.shutdown(); - await client?.shutdown(); + await clientA?.shutdown(); + await clientB?.shutdown(); } }); }); From e66617aec402e56c5db51c57255f6fd337bc88e7 Mon Sep 17 00:00:00 2001 From: gikkman Date: Tue, 7 May 2024 17:11:52 +0200 Subject: [PATCH 2/3] Update package.json with multi-export --- package.json | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 3601f0f..eeab13b 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ }, "homepage": "https://github.com/gikkman/tipc", "bugs": "https://github.com/gikkman/tipc/issues", - "description": "A wrapper around websockets which allows using a mapped type as a contract. As long as the server and client shares this contract types, neither can send nor receive incorrect typed data.", + "description": "A wrapper around a websocket which allows using a mapped type as a contract. As long as the server and client shares this contract types, neither can send nor receive incorrect typed data.", "author": { "name": "Gikkman", "url": "http://www.github.com/gikkman" @@ -16,9 +16,31 @@ "module": "./dist/mjs/index.node.js", "browser": "./dist/umd/index.browser.js", "exports": { - "import": "./dist/mjs/index.node.js", - "require": "./dist/cjs/index.node.js", - "browser": "./dist/umd/index.browser.js" + ".": { + "node": { + "types": "./dist/mjs/index.node.d.ts", + "import": "./dist/mjs/index.node.js", + "require": "./dist/cjs/index.node.js" + }, + "default": { + "types": "./dist/umd/index.node.d.ts", + "import": "./dist/umd/index.browser.js" + } + }, + "./cjs": { + "types": "./dist/cjs/index.node.d.ts", + "import": "./dist/cjs/index.node.js", + "require": "./dist/cjs/index.node.js" + }, + "./mjs": { + "types": "./dist/mjs/index.node.d.ts", + "import": "./dist/mjs/index.node.js", + "require": "./dist/mjs/index.node.js" + }, + "./browser": { + "types": "./dist/umd/index.browser.d.ts", + "import": "./dist/umd/index.browser.js" + } }, "scripts": { "lint": "eslint .", From 17ed6f3f979fda94491df71426d6c0393c11b251 Mon Sep 17 00:00:00 2001 From: gikkman Date: Thu, 16 May 2024 22:58:35 +0200 Subject: [PATCH 3/3] Up version to 2.0.4 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 058d206..5b2bd1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "tipc", - "version": "2.0.3", + "version": "2.0.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "tipc", - "version": "2.0.3", + "version": "2.0.4", "license": "Apache-2.0", "devDependencies": { "@types/jasmine": "^4.0.3", diff --git a/package.json b/package.json index eeab13b..d81f00b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tipc", - "version": "2.0.3", + "version": "2.0.4", "repository": { "type": "git", "url": "git+https://github.com/gikkman/tipc.git"