From a9f5aaec24f88c015173b51151e86492f2743e22 Mon Sep 17 00:00:00 2001 From: Vladyslav Babak Date: Thu, 12 Feb 2026 10:04:58 +0100 Subject: [PATCH 01/19] feat(connection): added handlers on open and close connections --- .../runtime-node/src/launch-http-server.ts | 3 +++ packages/runtime-node/src/node-env-manager.ts | 13 +++++++++- packages/runtime-node/src/ws-node-host.ts | 26 +++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/packages/runtime-node/src/launch-http-server.ts b/packages/runtime-node/src/launch-http-server.ts index 0fec58d46..d14fd9f58 100644 --- a/packages/runtime-node/src/launch-http-server.ts +++ b/packages/runtime-node/src/launch-http-server.ts @@ -2,6 +2,7 @@ import express from 'express'; import cors from 'cors'; import { safeListeningHttpServer } from 'create-listening-server'; import * as io from 'socket.io'; +import { Message } from '@dazl/engine-core'; export const DEFAULT_PORT = 3000; @@ -21,6 +22,8 @@ export interface ILaunchHttpServerOptions { socketServerOptions?: Partial; routeMiddlewares?: Array; hostname?: string; + onConnectionOpen?: (clientId: string, socket: io.Socket, postMessage: (message: Message) => void) => void; + onConnectionClose?: (clientId: string) => void; } export async function launchEngineHttpServer({ diff --git a/packages/runtime-node/src/node-env-manager.ts b/packages/runtime-node/src/node-env-manager.ts index c17ec6a63..460d228a9 100644 --- a/packages/runtime-node/src/node-env-manager.ts +++ b/packages/runtime-node/src/node-env-manager.ts @@ -60,6 +60,16 @@ export class NodeEnvManager implements IDisposable { runtimeOptions.set('enginePort', port.toString()); const clientsHost = new WsServerHost(socketServer); + const disposeOnConnectionOpen = serverOptions.onConnectionOpen + ? clientsHost.registerConnectionHandler(serverOptions.onConnectionOpen) + : undefined; + const disposeOnConnectionClose = serverOptions.onConnectionClose + ? clientsHost.registerDisconnectionHandler(serverOptions.onConnectionClose) + : undefined; + const disposeConnectionHandlers = () => { + disposeOnConnectionOpen?.(); + disposeOnConnectionClose?.(); + }; clientsHost.addEventListener('message', handleRegistrationOnMessage); const forwardingCom = new Communication(clientsHost, 'clients-host-com'); function handleRegistrationOnMessage({ data }: { data: Message }) { @@ -84,6 +94,7 @@ export class NodeEnvManager implements IDisposable { disposeMetricsListener(); await this.closeAll(); clientsHost.removeEventListener('message', handleRegistrationOnMessage); + disposeConnectionHandlers(); await clientsHost.dispose(); await close(); }; @@ -97,7 +108,7 @@ export class NodeEnvManager implements IDisposable { if (process.send) { process.send({ port }); } - return { port }; + return { port, socketServer }; } async closeAll() { diff --git a/packages/runtime-node/src/ws-node-host.ts b/packages/runtime-node/src/ws-node-host.ts index b7921c2f2..6902dfced 100644 --- a/packages/runtime-node/src/ws-node-host.ts +++ b/packages/runtime-node/src/ws-node-host.ts @@ -1,6 +1,7 @@ import type io from 'socket.io'; import { BaseHost, type Message } from '@dazl/engine-core'; import { SafeDisposable, type IDisposable } from '@dazl/patterns'; +import { ILaunchHttpServerOptions } from './launch-http-server.js'; export class WsHost extends BaseHost { constructor(private socket: io.Socket) { @@ -15,6 +16,8 @@ export class WsHost extends BaseHost { } export class WsServerHost extends BaseHost implements IDisposable { + private connectionHandlers = new Set['onConnectionOpen']>(); + private disconnectionHandlers = new Set['onConnectionClose']>(); private socketToEnvId = new Map(); private clientIdToSocket = new Map(); private disposables = new SafeDisposable(WsServerHost.name); @@ -28,6 +31,20 @@ export class WsServerHost extends BaseHost implements IDisposable { this.disposables.add('clear handlers', () => this.handlers.clear()); } + public registerConnectionHandler(handler: Required['onConnectionOpen']) { + this.connectionHandlers.add(handler); + return () => { + this.connectionHandlers.delete(handler); + }; + } + + public registerDisconnectionHandler(handler: Required['onConnectionClose']) { + this.disconnectionHandlers.add(handler); + return () => { + this.disconnectionHandlers.delete(handler); + }; + } + public postMessage(data: Message) { if (data.to !== '*') { if (this.socketToEnvId.has(data.to)) { @@ -51,6 +68,12 @@ export class WsServerHost extends BaseHost implements IDisposable { existingSocket.disconnect(true); } + for (const handler of this.connectionHandlers) { + handler(clientId, socket, (message: Message) => { + this.postMessage(message); + }); + } + this.clientIdToSocket.set(clientId, socket); const nameSpace = (original: string) => `${clientId}/${original}`; @@ -88,6 +111,9 @@ export class WsServerHost extends BaseHost implements IDisposable { if (this.clientIdToSocket.get(clientId) === socket) { this.clientIdToSocket.delete(clientId); } + for (const handler of this.disconnectionHandlers) { + handler(clientId); + } }); }; } From 4fc69703eeaf717466ce0176c08e6076dde185ee Mon Sep 17 00:00:00 2001 From: Vladyslav Babak Date: Thu, 12 Feb 2026 10:40:52 +0100 Subject: [PATCH 02/19] fixes --- packages/runtime-node/src/launch-http-server.ts | 2 +- packages/runtime-node/src/node-env-manager.ts | 2 +- packages/runtime-node/src/ws-node-host.ts | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/runtime-node/src/launch-http-server.ts b/packages/runtime-node/src/launch-http-server.ts index d14fd9f58..83c470b9d 100644 --- a/packages/runtime-node/src/launch-http-server.ts +++ b/packages/runtime-node/src/launch-http-server.ts @@ -23,7 +23,7 @@ export interface ILaunchHttpServerOptions { routeMiddlewares?: Array; hostname?: string; onConnectionOpen?: (clientId: string, socket: io.Socket, postMessage: (message: Message) => void) => void; - onConnectionClose?: (clientId: string) => void; + onConnectionClose?: (clientId: string, postMessage: (message: Message) => void) => void; } export async function launchEngineHttpServer({ diff --git a/packages/runtime-node/src/node-env-manager.ts b/packages/runtime-node/src/node-env-manager.ts index 460d228a9..fd815ecc9 100644 --- a/packages/runtime-node/src/node-env-manager.ts +++ b/packages/runtime-node/src/node-env-manager.ts @@ -108,7 +108,7 @@ export class NodeEnvManager implements IDisposable { if (process.send) { process.send({ port }); } - return { port, socketServer }; + return { port }; } async closeAll() { diff --git a/packages/runtime-node/src/ws-node-host.ts b/packages/runtime-node/src/ws-node-host.ts index 6902dfced..8934c8c18 100644 --- a/packages/runtime-node/src/ws-node-host.ts +++ b/packages/runtime-node/src/ws-node-host.ts @@ -112,7 +112,9 @@ export class WsServerHost extends BaseHost implements IDisposable { this.clientIdToSocket.delete(clientId); } for (const handler of this.disconnectionHandlers) { - handler(clientId); + handler(clientId, (message: Message) => { + this.postMessage(message); + }); } }); }; From 403fabae5848b014d7b1cfcb8f12f4f8abb28e09 Mon Sep 17 00:00:00 2001 From: Vladyslav Babak Date: Thu, 12 Feb 2026 12:06:22 +0100 Subject: [PATCH 03/19] tests --- .../test/node-env.manager.unit.ts | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/packages/runtime-node/test/node-env.manager.unit.ts b/packages/runtime-node/test/node-env.manager.unit.ts index 2cacb339b..00cea7fdc 100644 --- a/packages/runtime-node/test/node-env.manager.unit.ts +++ b/packages/runtime-node/test/node-env.manager.unit.ts @@ -1,4 +1,6 @@ import { expect } from 'chai'; +import sinon from 'sinon'; +import type io from 'socket.io'; import { BaseHost, COM, Communication, WsClientHost } from '@dazl/engine-core'; import { launchEngineHttpServer, @@ -153,6 +155,67 @@ describe('NodeEnvManager', () => { }); }); + describe('NodeEnvManager with connection handlers', () => { + it('should call connection and disconnection handlers', async () => { + const connectionHandler = sinon.spy(); + const disconnectionHandler = sinon.spy(); + + const featureEnvironmentsMapping: NodeEnvsFeatureMapping = { + featureToEnvironments: { + 'test-feature': [aEnv.env], + }, + availableEnvironments: { + a: { + env: aEnv.env, + endpointType: 'single', + envType: 'node', + }, + }, + }; + + const manager = disposeAfterTest(new NodeEnvManager(meta, featureEnvironmentsMapping)); + const { port } = await manager.autoLaunch(new Map([['feature', 'test-feature']]), { + onConnectionOpen: connectionHandler, + onConnectionClose: disconnectionHandler, + }); + + // Create a client connection + const host = new WsClientHost('http://localhost:' + port, {}); + const communication = new Communication(new BaseHost(), testCommunicationId); + communication.registerEnv(aEnv.env, host); + communication.registerMessageHandler(host); + + await host.connected; + + // Verify connection handler was called + expect(connectionHandler.callCount).to.equal(1); + const [clientId, socket, postMessage] = connectionHandler.firstCall.args; + expect(clientId).to.be.a('string'); + expect(socket).to.have.property('id'); + expect(postMessage).to.be.a('function'); + + // Disconnect the client + host.disconnectSocket(); + + let disconnectResolve: () => void; + const disconnectPromise = new Promise((resolve) => { + disconnectResolve = resolve; + }); + (socket as io.Socket).on('disconnect', () => { + disconnectResolve(); + }); + await disconnectPromise; + + // Verify disconnection handler was called + expect(disconnectionHandler.callCount).to.equal(1); + const [disconnectedClientId, disconnectPostMessage] = disconnectionHandler.firstCall.args; + expect(disconnectedClientId).to.equal(clientId); + expect(disconnectPostMessage).to.be.a('function'); + + await communication.dispose(); + }); + }); + function getClientCom(port: number) { const host = new WsClientHost('http://localhost:' + port, {}); const com = new Communication(new BaseHost(), testCommunicationId); From ad2860975ffdf29963984f44e1285cb61a63c986 Mon Sep 17 00:00:00 2001 From: Vladyslav Babak Date: Thu, 12 Feb 2026 12:10:22 +0100 Subject: [PATCH 04/19] fix order --- packages/runtime-node/test/node-env.manager.unit.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/runtime-node/test/node-env.manager.unit.ts b/packages/runtime-node/test/node-env.manager.unit.ts index 00cea7fdc..8347e3489 100644 --- a/packages/runtime-node/test/node-env.manager.unit.ts +++ b/packages/runtime-node/test/node-env.manager.unit.ts @@ -195,8 +195,6 @@ describe('NodeEnvManager', () => { expect(postMessage).to.be.a('function'); // Disconnect the client - host.disconnectSocket(); - let disconnectResolve: () => void; const disconnectPromise = new Promise((resolve) => { disconnectResolve = resolve; @@ -204,6 +202,7 @@ describe('NodeEnvManager', () => { (socket as io.Socket).on('disconnect', () => { disconnectResolve(); }); + host.disconnectSocket(); await disconnectPromise; // Verify disconnection handler was called From 26646ee35675fb159e88bbde909fdcce606fab73 Mon Sep 17 00:00:00 2001 From: Vladyslav Babak Date: Thu, 12 Feb 2026 12:11:08 +0100 Subject: [PATCH 05/19] fix types --- packages/runtime-node/src/launch-http-server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime-node/src/launch-http-server.ts b/packages/runtime-node/src/launch-http-server.ts index 83c470b9d..90ac36f52 100644 --- a/packages/runtime-node/src/launch-http-server.ts +++ b/packages/runtime-node/src/launch-http-server.ts @@ -2,7 +2,7 @@ import express from 'express'; import cors from 'cors'; import { safeListeningHttpServer } from 'create-listening-server'; import * as io from 'socket.io'; -import { Message } from '@dazl/engine-core'; +import type { Message } from '@dazl/engine-core'; export const DEFAULT_PORT = 3000; From 850dd61679a4bd212744889f16f0e7e11dd24905 Mon Sep 17 00:00:00 2001 From: Vladyslav Babak Date: Thu, 12 Feb 2026 12:13:09 +0100 Subject: [PATCH 06/19] simple await --- packages/runtime-node/test/node-env.manager.unit.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/runtime-node/test/node-env.manager.unit.ts b/packages/runtime-node/test/node-env.manager.unit.ts index 8347e3489..f0328ca7a 100644 --- a/packages/runtime-node/test/node-env.manager.unit.ts +++ b/packages/runtime-node/test/node-env.manager.unit.ts @@ -195,15 +195,12 @@ describe('NodeEnvManager', () => { expect(postMessage).to.be.a('function'); // Disconnect the client - let disconnectResolve: () => void; - const disconnectPromise = new Promise((resolve) => { - disconnectResolve = resolve; + await new Promise((resolve) => { + (socket as io.Socket).on('disconnect', () => { + resolve(); + }); + host.disconnectSocket(); }); - (socket as io.Socket).on('disconnect', () => { - disconnectResolve(); - }); - host.disconnectSocket(); - await disconnectPromise; // Verify disconnection handler was called expect(disconnectionHandler.callCount).to.equal(1); From 0eb7d2772d7dcb66e49cc5c66a6b769e32ebfcac Mon Sep 17 00:00:00 2001 From: Vladyslav Babak Date: Thu, 12 Feb 2026 12:14:25 +0100 Subject: [PATCH 07/19] fix import --- packages/runtime-node/src/ws-node-host.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime-node/src/ws-node-host.ts b/packages/runtime-node/src/ws-node-host.ts index 8934c8c18..4a757fda9 100644 --- a/packages/runtime-node/src/ws-node-host.ts +++ b/packages/runtime-node/src/ws-node-host.ts @@ -1,7 +1,7 @@ import type io from 'socket.io'; import { BaseHost, type Message } from '@dazl/engine-core'; import { SafeDisposable, type IDisposable } from '@dazl/patterns'; -import { ILaunchHttpServerOptions } from './launch-http-server.js'; +import type { ILaunchHttpServerOptions } from './launch-http-server.js'; export class WsHost extends BaseHost { constructor(private socket: io.Socket) { From 34e061c0574291eed0f8ff74eb5f2014f07a668a Mon Sep 17 00:00:00 2001 From: Vladyslav Babak Date: Thu, 12 Feb 2026 12:31:33 +0100 Subject: [PATCH 08/19] bump version --- packages/core/package.json | 2 +- packages/engine-cli/package.json | 2 +- packages/runtime-node/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 6a05acb24..5507a0127 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@dazl/engine-core", - "version": "51.10.0", + "version": "51.11.0", "type": "module", "main": "dist/index.js", "exports": { diff --git a/packages/engine-cli/package.json b/packages/engine-cli/package.json index 5c9af9024..528c2cbc9 100644 --- a/packages/engine-cli/package.json +++ b/packages/engine-cli/package.json @@ -1,6 +1,6 @@ { "name": "@dazl/engine-cli", - "version": "51.10.0", + "version": "51.11.0", "type": "module", "bin": { "engine": "./bin/engine.js" diff --git a/packages/runtime-node/package.json b/packages/runtime-node/package.json index 3f63a5af0..65bc6d3ad 100644 --- a/packages/runtime-node/package.json +++ b/packages/runtime-node/package.json @@ -1,6 +1,6 @@ { "name": "@dazl/engine-runtime-node", - "version": "51.10.0", + "version": "51.11.0", "type": "module", "main": "dist/index.js", "exports": { From 83b8bd291fb6853016e0f4770d66efdf6ff669b2 Mon Sep 17 00:00:00 2001 From: Vladyslav Babak Date: Thu, 12 Feb 2026 12:55:44 +0100 Subject: [PATCH 09/19] added initialClientId into test --- packages/runtime-node/test/node-env.manager.unit.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/runtime-node/test/node-env.manager.unit.ts b/packages/runtime-node/test/node-env.manager.unit.ts index f0328ca7a..f1034261c 100644 --- a/packages/runtime-node/test/node-env.manager.unit.ts +++ b/packages/runtime-node/test/node-env.manager.unit.ts @@ -180,7 +180,12 @@ describe('NodeEnvManager', () => { }); // Create a client connection - const host = new WsClientHost('http://localhost:' + port, {}); + const initialClientId = 'test-client-id'; + const host = new WsClientHost('http://localhost:' + port, { + auth: { + clientId: initialClientId, + }, + }); const communication = new Communication(new BaseHost(), testCommunicationId); communication.registerEnv(aEnv.env, host); communication.registerMessageHandler(host); @@ -190,7 +195,7 @@ describe('NodeEnvManager', () => { // Verify connection handler was called expect(connectionHandler.callCount).to.equal(1); const [clientId, socket, postMessage] = connectionHandler.firstCall.args; - expect(clientId).to.be.a('string'); + expect(clientId).to.equal(initialClientId); expect(socket).to.have.property('id'); expect(postMessage).to.be.a('function'); From 30226216991e9ed1de48beeeb3894025b8ad680d Mon Sep 17 00:00:00 2001 From: Vladyslav Babak Date: Thu, 12 Feb 2026 13:17:20 +0100 Subject: [PATCH 10/19] call disconnect callback only for active socket to avoid race condition --- packages/runtime-node/src/ws-node-host.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/runtime-node/src/ws-node-host.ts b/packages/runtime-node/src/ws-node-host.ts index 4a757fda9..ba3cf7502 100644 --- a/packages/runtime-node/src/ws-node-host.ts +++ b/packages/runtime-node/src/ws-node-host.ts @@ -61,9 +61,11 @@ export class WsServerHost extends BaseHost implements IDisposable { private onConnection = (socket: io.Socket): void => { const clientId = socket.handshake.auth?.clientId || socket.id; + const existingSocket = this.clientIdToSocket.get(clientId); + + this.clientIdToSocket.set(clientId, socket); // disconnect previous connection - const existingSocket = this.clientIdToSocket.get(clientId); if (existingSocket && existingSocket.connected) { existingSocket.disconnect(true); } @@ -74,8 +76,6 @@ export class WsServerHost extends BaseHost implements IDisposable { }); } - this.clientIdToSocket.set(clientId, socket); - const nameSpace = (original: string) => `${clientId}/${original}`; const onMessage = (message: Message): void => { // this mapping should not be here because of forwarding of messages @@ -110,11 +110,11 @@ export class WsServerHost extends BaseHost implements IDisposable { } if (this.clientIdToSocket.get(clientId) === socket) { this.clientIdToSocket.delete(clientId); - } - for (const handler of this.disconnectionHandlers) { - handler(clientId, (message: Message) => { - this.postMessage(message); - }); + for (const handler of this.disconnectionHandlers) { + handler(clientId, (message: Message) => { + this.postMessage(message); + }); + } } }); }; From 11720d0b34556f64c28cd50dd47f45a5de6106f1 Mon Sep 17 00:00:00 2001 From: Vladyslav Babak Date: Thu, 12 Feb 2026 13:17:29 +0100 Subject: [PATCH 11/19] rename --- packages/runtime-node/test/node-env.manager.unit.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/runtime-node/test/node-env.manager.unit.ts b/packages/runtime-node/test/node-env.manager.unit.ts index f1034261c..f5e035733 100644 --- a/packages/runtime-node/test/node-env.manager.unit.ts +++ b/packages/runtime-node/test/node-env.manager.unit.ts @@ -181,16 +181,16 @@ describe('NodeEnvManager', () => { // Create a client connection const initialClientId = 'test-client-id'; - const host = new WsClientHost('http://localhost:' + port, { + const client = new WsClientHost('http://localhost:' + port, { auth: { clientId: initialClientId, }, }); const communication = new Communication(new BaseHost(), testCommunicationId); - communication.registerEnv(aEnv.env, host); - communication.registerMessageHandler(host); + communication.registerEnv(aEnv.env, client); + communication.registerMessageHandler(client); - await host.connected; + await client.connected; // Verify connection handler was called expect(connectionHandler.callCount).to.equal(1); @@ -204,7 +204,7 @@ describe('NodeEnvManager', () => { (socket as io.Socket).on('disconnect', () => { resolve(); }); - host.disconnectSocket(); + client.disconnectSocket(); }); // Verify disconnection handler was called From d5f1344eb71f058065e81dacd0d96c1f29e94490 Mon Sep 17 00:00:00 2001 From: Vladyslav Babak Date: Thu, 12 Feb 2026 13:20:02 +0100 Subject: [PATCH 12/19] revert version --- packages/core/package.json | 2 +- packages/engine-cli/package.json | 2 +- packages/runtime-node/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 5507a0127..6a05acb24 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@dazl/engine-core", - "version": "51.11.0", + "version": "51.10.0", "type": "module", "main": "dist/index.js", "exports": { diff --git a/packages/engine-cli/package.json b/packages/engine-cli/package.json index 528c2cbc9..5c9af9024 100644 --- a/packages/engine-cli/package.json +++ b/packages/engine-cli/package.json @@ -1,6 +1,6 @@ { "name": "@dazl/engine-cli", - "version": "51.11.0", + "version": "51.10.0", "type": "module", "bin": { "engine": "./bin/engine.js" diff --git a/packages/runtime-node/package.json b/packages/runtime-node/package.json index 65bc6d3ad..3f63a5af0 100644 --- a/packages/runtime-node/package.json +++ b/packages/runtime-node/package.json @@ -1,6 +1,6 @@ { "name": "@dazl/engine-runtime-node", - "version": "51.11.0", + "version": "51.10.0", "type": "module", "main": "dist/index.js", "exports": { From 72cfca274e845d1de0e4b0093fd12ae1bebbdfc6 Mon Sep 17 00:00:00 2001 From: Vladyslav Babak Date: Thu, 12 Feb 2026 13:29:56 +0100 Subject: [PATCH 13/19] rm space --- packages/runtime-node/src/ws-node-host.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime-node/src/ws-node-host.ts b/packages/runtime-node/src/ws-node-host.ts index ba3cf7502..6c2517ab1 100644 --- a/packages/runtime-node/src/ws-node-host.ts +++ b/packages/runtime-node/src/ws-node-host.ts @@ -62,7 +62,7 @@ export class WsServerHost extends BaseHost implements IDisposable { private onConnection = (socket: io.Socket): void => { const clientId = socket.handshake.auth?.clientId || socket.id; const existingSocket = this.clientIdToSocket.get(clientId); - + this.clientIdToSocket.set(clientId, socket); // disconnect previous connection From d779593fe1ea8f1fc273368c9f29230dc4f9dd0c Mon Sep 17 00:00:00 2001 From: Vladyslav Babak Date: Thu, 12 Feb 2026 15:01:44 +0100 Subject: [PATCH 14/19] better handle connection replacement --- .../runtime-node/src/launch-http-server.ts | 9 +- packages/runtime-node/src/ws-node-host.ts | 22 +++-- .../test/node-env.manager.unit.ts | 87 ++++++++++++++----- 3 files changed, 87 insertions(+), 31 deletions(-) diff --git a/packages/runtime-node/src/launch-http-server.ts b/packages/runtime-node/src/launch-http-server.ts index 90ac36f52..ee5a24c21 100644 --- a/packages/runtime-node/src/launch-http-server.ts +++ b/packages/runtime-node/src/launch-http-server.ts @@ -22,8 +22,13 @@ export interface ILaunchHttpServerOptions { socketServerOptions?: Partial; routeMiddlewares?: Array; hostname?: string; - onConnectionOpen?: (clientId: string, socket: io.Socket, postMessage: (message: Message) => void) => void; - onConnectionClose?: (clientId: string, postMessage: (message: Message) => void) => void; + onConnectionOpen?: (data: { clientId: string; socket: io.Socket; postMessage: (message: Message) => void }) => void; + onConnectionClose?: (data: { + clientId: string; + postMessage: (message: Message) => void; + hasActiveConnection: boolean; + socket: io.Socket; + }) => void; } export async function launchEngineHttpServer({ diff --git a/packages/runtime-node/src/ws-node-host.ts b/packages/runtime-node/src/ws-node-host.ts index 6c2517ab1..58d861e65 100644 --- a/packages/runtime-node/src/ws-node-host.ts +++ b/packages/runtime-node/src/ws-node-host.ts @@ -71,8 +71,10 @@ export class WsServerHost extends BaseHost implements IDisposable { } for (const handler of this.connectionHandlers) { - handler(clientId, socket, (message: Message) => { - this.postMessage(message); + handler({ + clientId, + socket, + postMessage: (message: Message) => this.postMessage(message), }); } @@ -108,13 +110,17 @@ export class WsServerHost extends BaseHost implements IDisposable { }); } } - if (this.clientIdToSocket.get(clientId) === socket) { + const isActiveConnectionClosed = this.clientIdToSocket.get(clientId) === socket; + if (isActiveConnectionClosed) { this.clientIdToSocket.delete(clientId); - for (const handler of this.disconnectionHandlers) { - handler(clientId, (message: Message) => { - this.postMessage(message); - }); - } + } + for (const handler of this.disconnectionHandlers) { + handler({ + clientId, + postMessage: (message: Message) => this.postMessage(message), + hasActiveConnection: !isActiveConnectionClosed, + socket, + }); } }); }; diff --git a/packages/runtime-node/test/node-env.manager.unit.ts b/packages/runtime-node/test/node-env.manager.unit.ts index f5e035733..8e6447717 100644 --- a/packages/runtime-node/test/node-env.manager.unit.ts +++ b/packages/runtime-node/test/node-env.manager.unit.ts @@ -1,8 +1,8 @@ import { expect } from 'chai'; import sinon from 'sinon'; -import type io from 'socket.io'; import { BaseHost, COM, Communication, WsClientHost } from '@dazl/engine-core'; import { + ILaunchHttpServerOptions, launchEngineHttpServer, NodeEnvManager, type NodeEnvsFeatureMapping, @@ -179,41 +179,86 @@ describe('NodeEnvManager', () => { onConnectionClose: disconnectionHandler, }); - // Create a client connection + // Create a client connection 1 const initialClientId = 'test-client-id'; - const client = new WsClientHost('http://localhost:' + port, { + const client1 = new WsClientHost('http://localhost:' + port, { auth: { clientId: initialClientId, }, }); - const communication = new Communication(new BaseHost(), testCommunicationId); - communication.registerEnv(aEnv.env, client); - communication.registerMessageHandler(client); + const communication1 = new Communication(new BaseHost(), testCommunicationId); + communication1.registerEnv(aEnv.env, client1); + communication1.registerMessageHandler(client1); - await client.connected; + await client1.connected; // Verify connection handler was called expect(connectionHandler.callCount).to.equal(1); - const [clientId, socket, postMessage] = connectionHandler.firstCall.args; - expect(clientId).to.equal(initialClientId); - expect(socket).to.have.property('id'); - expect(postMessage).to.be.a('function'); - - // Disconnect the client - await new Promise((resolve) => { - (socket as io.Socket).on('disconnect', () => { + const [args1] = connectionHandler.firstCall.args as Parameters< + Required['onConnectionOpen'] + >; + expect(args1.clientId).to.equal(initialClientId); + expect(args1.socket).to.have.property('id'); + expect(args1.postMessage).to.be.a('function'); + + // Replace a client connection + const waitDisconnectFirstSocket = new Promise((resolve) => { + args1.socket.on('disconnect', () => { resolve(); }); - client.disconnectSocket(); }); + const client2 = new WsClientHost('http://localhost:' + port, { + auth: { + clientId: initialClientId, + }, + }); + const communication2 = new Communication(new BaseHost(), testCommunicationId); + communication2.registerEnv(aEnv.env, client2); + communication2.registerMessageHandler(client2); - // Verify disconnection handler was called + await client2.connected; + + // Verify connection handler was called + expect(connectionHandler.callCount).to.equal(2); + const [args2] = connectionHandler.secondCall.args as Parameters< + Required['onConnectionOpen'] + >; + expect(args2.clientId).to.equal(initialClientId); + expect(args2.socket).to.have.property('id'); + expect(args2.postMessage).to.be.a('function'); + expect(args2.socket.id).to.not.equal(args1.socket.id); + + // Verify disconnection handler was called after the client connection was replaced + await waitDisconnectFirstSocket; expect(disconnectionHandler.callCount).to.equal(1); - const [disconnectedClientId, disconnectPostMessage] = disconnectionHandler.firstCall.args; - expect(disconnectedClientId).to.equal(clientId); - expect(disconnectPostMessage).to.be.a('function'); + const [disconnectArgs1] = disconnectionHandler.firstCall.args as Parameters< + Required['onConnectionClose'] + >; + expect(disconnectArgs1.clientId).to.equal(initialClientId); + expect(disconnectArgs1.postMessage).to.be.a('function'); + expect(disconnectArgs1.socket.id).to.equal(args1.socket.id); + expect(disconnectArgs1.hasActiveConnection).to.equal(true); + + // Disconnect an active socket + await new Promise((resolve) => { + args2.socket.on('disconnect', () => { + resolve(); + }); + client2.disconnectSocket(); + }); - await communication.dispose(); + // Verify disconnection handler for active connection was called + expect(disconnectionHandler.callCount).to.equal(2); + const [disconnectArgs2] = disconnectionHandler.secondCall.args as Parameters< + Required['onConnectionClose'] + >; + expect(disconnectArgs2.clientId).to.equal(initialClientId); + expect(disconnectArgs2.postMessage).to.be.a('function'); + expect(disconnectArgs2.hasActiveConnection).to.equal(false); + expect(disconnectArgs2.socket.id).to.equal(args2.socket.id); + + await communication1.dispose(); + await communication2.dispose(); }); }); From 3ab6e695bc4f696d48df1eaad003ce1f14f8ee57 Mon Sep 17 00:00:00 2001 From: Vladyslav Babak Date: Thu, 12 Feb 2026 15:37:46 +0100 Subject: [PATCH 15/19] better types --- .../runtime-node/src/launch-http-server.ts | 21 ++++++++++++------- packages/runtime-node/src/ws-node-host.ts | 15 +++++++------ .../test/node-env.manager.unit.ts | 19 ++++++----------- 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/packages/runtime-node/src/launch-http-server.ts b/packages/runtime-node/src/launch-http-server.ts index ee5a24c21..4d56a0b72 100644 --- a/packages/runtime-node/src/launch-http-server.ts +++ b/packages/runtime-node/src/launch-http-server.ts @@ -16,19 +16,26 @@ export interface RouteMiddleware { handlers: express.RequestHandler | express.RequestHandler[]; } +export interface IConnectionOpenEvent { + clientId: string; + socket: io.Socket; + postMessage: (message: Message) => void; +} +export type IConnectionOpenHandler = (event: IConnectionOpenEvent) => void; + +export interface IConnectionCloseEvent extends IConnectionOpenEvent { + hasActiveConnection: boolean; +} +export type IConnectionCloseHandler = (event: IConnectionCloseEvent) => void; + export interface ILaunchHttpServerOptions { staticDirPath?: string; httpServerPort?: number; socketServerOptions?: Partial; routeMiddlewares?: Array; hostname?: string; - onConnectionOpen?: (data: { clientId: string; socket: io.Socket; postMessage: (message: Message) => void }) => void; - onConnectionClose?: (data: { - clientId: string; - postMessage: (message: Message) => void; - hasActiveConnection: boolean; - socket: io.Socket; - }) => void; + onConnectionOpen?: IConnectionOpenHandler; + onConnectionClose?: IConnectionCloseHandler; } export async function launchEngineHttpServer({ diff --git a/packages/runtime-node/src/ws-node-host.ts b/packages/runtime-node/src/ws-node-host.ts index 58d861e65..8fc1092e4 100644 --- a/packages/runtime-node/src/ws-node-host.ts +++ b/packages/runtime-node/src/ws-node-host.ts @@ -1,7 +1,7 @@ import type io from 'socket.io'; import { BaseHost, type Message } from '@dazl/engine-core'; import { SafeDisposable, type IDisposable } from '@dazl/patterns'; -import type { ILaunchHttpServerOptions } from './launch-http-server.js'; +import type { IConnectionCloseHandler, IConnectionOpenHandler } from './launch-http-server.js'; export class WsHost extends BaseHost { constructor(private socket: io.Socket) { @@ -16,8 +16,8 @@ export class WsHost extends BaseHost { } export class WsServerHost extends BaseHost implements IDisposable { - private connectionHandlers = new Set['onConnectionOpen']>(); - private disconnectionHandlers = new Set['onConnectionClose']>(); + private connectionHandlers = new Set(); + private disconnectionHandlers = new Set(); private socketToEnvId = new Map(); private clientIdToSocket = new Map(); private disposables = new SafeDisposable(WsServerHost.name); @@ -31,14 +31,14 @@ export class WsServerHost extends BaseHost implements IDisposable { this.disposables.add('clear handlers', () => this.handlers.clear()); } - public registerConnectionHandler(handler: Required['onConnectionOpen']) { + public registerConnectionHandler(handler: IConnectionOpenHandler) { this.connectionHandlers.add(handler); return () => { this.connectionHandlers.delete(handler); }; } - public registerDisconnectionHandler(handler: Required['onConnectionClose']) { + public registerDisconnectionHandler(handler: IConnectionCloseHandler) { this.disconnectionHandlers.add(handler); return () => { this.disconnectionHandlers.delete(handler); @@ -110,15 +110,14 @@ export class WsServerHost extends BaseHost implements IDisposable { }); } } - const isActiveConnectionClosed = this.clientIdToSocket.get(clientId) === socket; - if (isActiveConnectionClosed) { + if (this.clientIdToSocket.get(clientId) === socket) { this.clientIdToSocket.delete(clientId); } for (const handler of this.disconnectionHandlers) { handler({ clientId, postMessage: (message: Message) => this.postMessage(message), - hasActiveConnection: !isActiveConnectionClosed, + hasActiveConnection: this.clientIdToSocket.has(clientId), socket, }); } diff --git a/packages/runtime-node/test/node-env.manager.unit.ts b/packages/runtime-node/test/node-env.manager.unit.ts index 8e6447717..1f517eb9a 100644 --- a/packages/runtime-node/test/node-env.manager.unit.ts +++ b/packages/runtime-node/test/node-env.manager.unit.ts @@ -2,7 +2,8 @@ import { expect } from 'chai'; import sinon from 'sinon'; import { BaseHost, COM, Communication, WsClientHost } from '@dazl/engine-core'; import { - ILaunchHttpServerOptions, + IConnectionCloseHandler, + IConnectionOpenHandler, launchEngineHttpServer, NodeEnvManager, type NodeEnvsFeatureMapping, @@ -194,9 +195,7 @@ describe('NodeEnvManager', () => { // Verify connection handler was called expect(connectionHandler.callCount).to.equal(1); - const [args1] = connectionHandler.firstCall.args as Parameters< - Required['onConnectionOpen'] - >; + const [args1] = connectionHandler.firstCall.args as Parameters; expect(args1.clientId).to.equal(initialClientId); expect(args1.socket).to.have.property('id'); expect(args1.postMessage).to.be.a('function'); @@ -220,9 +219,7 @@ describe('NodeEnvManager', () => { // Verify connection handler was called expect(connectionHandler.callCount).to.equal(2); - const [args2] = connectionHandler.secondCall.args as Parameters< - Required['onConnectionOpen'] - >; + const [args2] = connectionHandler.secondCall.args as Parameters; expect(args2.clientId).to.equal(initialClientId); expect(args2.socket).to.have.property('id'); expect(args2.postMessage).to.be.a('function'); @@ -231,9 +228,7 @@ describe('NodeEnvManager', () => { // Verify disconnection handler was called after the client connection was replaced await waitDisconnectFirstSocket; expect(disconnectionHandler.callCount).to.equal(1); - const [disconnectArgs1] = disconnectionHandler.firstCall.args as Parameters< - Required['onConnectionClose'] - >; + const [disconnectArgs1] = disconnectionHandler.firstCall.args as Parameters; expect(disconnectArgs1.clientId).to.equal(initialClientId); expect(disconnectArgs1.postMessage).to.be.a('function'); expect(disconnectArgs1.socket.id).to.equal(args1.socket.id); @@ -249,9 +244,7 @@ describe('NodeEnvManager', () => { // Verify disconnection handler for active connection was called expect(disconnectionHandler.callCount).to.equal(2); - const [disconnectArgs2] = disconnectionHandler.secondCall.args as Parameters< - Required['onConnectionClose'] - >; + const [disconnectArgs2] = disconnectionHandler.secondCall.args as Parameters; expect(disconnectArgs2.clientId).to.equal(initialClientId); expect(disconnectArgs2.postMessage).to.be.a('function'); expect(disconnectArgs2.hasActiveConnection).to.equal(false); From e462e9e733c647d4ebeddd290300336be0c18f93 Mon Sep 17 00:00:00 2001 From: Vladyslav Babak Date: Fri, 13 Feb 2026 12:42:24 +0100 Subject: [PATCH 16/19] reconnection handler, error handling --- .../runtime-node/src/launch-http-server.ts | 14 ++-- packages/runtime-node/src/node-env-manager.ts | 4 ++ packages/runtime-node/src/ws-node-host.ts | 64 ++++++++++++------- .../test/node-env.manager.unit.ts | 35 +++++----- 4 files changed, 64 insertions(+), 53 deletions(-) diff --git a/packages/runtime-node/src/launch-http-server.ts b/packages/runtime-node/src/launch-http-server.ts index 4d56a0b72..d5a5b9c21 100644 --- a/packages/runtime-node/src/launch-http-server.ts +++ b/packages/runtime-node/src/launch-http-server.ts @@ -16,17 +16,12 @@ export interface RouteMiddleware { handlers: express.RequestHandler | express.RequestHandler[]; } -export interface IConnectionOpenEvent { +export interface IConnectionEvent { clientId: string; socket: io.Socket; postMessage: (message: Message) => void; } -export type IConnectionOpenHandler = (event: IConnectionOpenEvent) => void; - -export interface IConnectionCloseEvent extends IConnectionOpenEvent { - hasActiveConnection: boolean; -} -export type IConnectionCloseHandler = (event: IConnectionCloseEvent) => void; +export type IConnectionHandler = (event: IConnectionEvent) => void; export interface ILaunchHttpServerOptions { staticDirPath?: string; @@ -34,8 +29,9 @@ export interface ILaunchHttpServerOptions { socketServerOptions?: Partial; routeMiddlewares?: Array; hostname?: string; - onConnectionOpen?: IConnectionOpenHandler; - onConnectionClose?: IConnectionCloseHandler; + onConnectionOpen?: IConnectionHandler; + onConnectionClose?: IConnectionHandler; + onConnectionReconnect?: IConnectionHandler; } export async function launchEngineHttpServer({ diff --git a/packages/runtime-node/src/node-env-manager.ts b/packages/runtime-node/src/node-env-manager.ts index fd815ecc9..95e744915 100644 --- a/packages/runtime-node/src/node-env-manager.ts +++ b/packages/runtime-node/src/node-env-manager.ts @@ -66,9 +66,13 @@ export class NodeEnvManager implements IDisposable { const disposeOnConnectionClose = serverOptions.onConnectionClose ? clientsHost.registerDisconnectionHandler(serverOptions.onConnectionClose) : undefined; + const disposeOnReconnection = serverOptions.onConnectionReconnect + ? clientsHost.registerReconnectionHandler(serverOptions.onConnectionReconnect) + : undefined; const disposeConnectionHandlers = () => { disposeOnConnectionOpen?.(); disposeOnConnectionClose?.(); + disposeOnReconnection?.(); }; clientsHost.addEventListener('message', handleRegistrationOnMessage); const forwardingCom = new Communication(clientsHost, 'clients-host-com'); diff --git a/packages/runtime-node/src/ws-node-host.ts b/packages/runtime-node/src/ws-node-host.ts index 8fc1092e4..0b02dce51 100644 --- a/packages/runtime-node/src/ws-node-host.ts +++ b/packages/runtime-node/src/ws-node-host.ts @@ -1,7 +1,7 @@ import type io from 'socket.io'; -import { BaseHost, type Message } from '@dazl/engine-core'; +import { BaseHost, LoggerService, LogLevel, type Message } from '@dazl/engine-core'; import { SafeDisposable, type IDisposable } from '@dazl/patterns'; -import type { IConnectionCloseHandler, IConnectionOpenHandler } from './launch-http-server.js'; +import type { IConnectionEvent, IConnectionHandler } from './launch-http-server.js'; export class WsHost extends BaseHost { constructor(private socket: io.Socket) { @@ -16,11 +16,13 @@ export class WsHost extends BaseHost { } export class WsServerHost extends BaseHost implements IDisposable { - private connectionHandlers = new Set(); - private disconnectionHandlers = new Set(); + private connectionHandlers = new Set(); + private disconnectionHandlers = new Set(); + private reconnectionHandlers = new Set(); private socketToEnvId = new Map(); private clientIdToSocket = new Map(); private disposables = new SafeDisposable(WsServerHost.name); + private logger = new LoggerService([], {}, { logToConsole: true, severity: LogLevel.DEBUG }); dispose = this.disposables.dispose; isDisposed = this.disposables.isDisposed; @@ -31,20 +33,27 @@ export class WsServerHost extends BaseHost implements IDisposable { this.disposables.add('clear handlers', () => this.handlers.clear()); } - public registerConnectionHandler(handler: IConnectionOpenHandler) { + public registerConnectionHandler(handler: IConnectionHandler) { this.connectionHandlers.add(handler); return () => { this.connectionHandlers.delete(handler); }; } - public registerDisconnectionHandler(handler: IConnectionCloseHandler) { + public registerDisconnectionHandler(handler: IConnectionHandler) { this.disconnectionHandlers.add(handler); return () => { this.disconnectionHandlers.delete(handler); }; } + public registerReconnectionHandler(handler: IConnectionHandler) { + this.reconnectionHandlers.add(handler); + return () => { + this.reconnectionHandlers.delete(handler); + }; + } + public postMessage(data: Message) { if (data.to !== '*') { if (this.socketToEnvId.has(data.to)) { @@ -59,23 +68,35 @@ export class WsServerHost extends BaseHost implements IDisposable { } } + private callWithErrorHandling(callback: () => void): void { + try { + callback(); + } catch (error) { + this.logger.error(error instanceof Error ? `${error.message} ${error.stack}` : String(error)); + } + } + private onConnection = (socket: io.Socket): void => { const clientId = socket.handshake.auth?.clientId || socket.id; const existingSocket = this.clientIdToSocket.get(clientId); this.clientIdToSocket.set(clientId, socket); - // disconnect previous connection - if (existingSocket && existingSocket.connected) { - existingSocket.disconnect(true); - } + const connectionEvent: IConnectionEvent = { + clientId, + socket, + postMessage: (message: Message) => this.postMessage(message), + }; - for (const handler of this.connectionHandlers) { - handler({ - clientId, - socket, - postMessage: (message: Message) => this.postMessage(message), - }); + if (existingSocket && existingSocket.connected) { + existingSocket.disconnect(true); // disconnect previous connection + for (const handler of this.reconnectionHandlers) { + this.callWithErrorHandling(() => handler(connectionEvent)); + } + } else { + for (const handler of this.connectionHandlers) { + this.callWithErrorHandling(() => handler(connectionEvent)); + } } const nameSpace = (original: string) => `${clientId}/${original}`; @@ -112,14 +133,9 @@ export class WsServerHost extends BaseHost implements IDisposable { } if (this.clientIdToSocket.get(clientId) === socket) { this.clientIdToSocket.delete(clientId); - } - for (const handler of this.disconnectionHandlers) { - handler({ - clientId, - postMessage: (message: Message) => this.postMessage(message), - hasActiveConnection: this.clientIdToSocket.has(clientId), - socket, - }); + for (const handler of this.disconnectionHandlers) { + this.callWithErrorHandling(() => handler(connectionEvent)); + } } }); }; diff --git a/packages/runtime-node/test/node-env.manager.unit.ts b/packages/runtime-node/test/node-env.manager.unit.ts index 1f517eb9a..6eb7994b0 100644 --- a/packages/runtime-node/test/node-env.manager.unit.ts +++ b/packages/runtime-node/test/node-env.manager.unit.ts @@ -2,8 +2,7 @@ import { expect } from 'chai'; import sinon from 'sinon'; import { BaseHost, COM, Communication, WsClientHost } from '@dazl/engine-core'; import { - IConnectionCloseHandler, - IConnectionOpenHandler, + IConnectionHandler, launchEngineHttpServer, NodeEnvManager, type NodeEnvsFeatureMapping, @@ -157,9 +156,10 @@ describe('NodeEnvManager', () => { }); describe('NodeEnvManager with connection handlers', () => { - it('should call connection and disconnection handlers', async () => { + it('should call connection, reconnection and disconnection handlers', async () => { const connectionHandler = sinon.spy(); const disconnectionHandler = sinon.spy(); + const reconnectionHandler = sinon.spy(); const featureEnvironmentsMapping: NodeEnvsFeatureMapping = { featureToEnvironments: { @@ -178,6 +178,7 @@ describe('NodeEnvManager', () => { const { port } = await manager.autoLaunch(new Map([['feature', 'test-feature']]), { onConnectionOpen: connectionHandler, onConnectionClose: disconnectionHandler, + onConnectionReconnect: reconnectionHandler, }); // Create a client connection 1 @@ -195,7 +196,7 @@ describe('NodeEnvManager', () => { // Verify connection handler was called expect(connectionHandler.callCount).to.equal(1); - const [args1] = connectionHandler.firstCall.args as Parameters; + const [args1] = connectionHandler.firstCall.args as Parameters; expect(args1.clientId).to.equal(initialClientId); expect(args1.socket).to.have.property('id'); expect(args1.postMessage).to.be.a('function'); @@ -217,22 +218,17 @@ describe('NodeEnvManager', () => { await client2.connected; - // Verify connection handler was called - expect(connectionHandler.callCount).to.equal(2); - const [args2] = connectionHandler.secondCall.args as Parameters; + // Verify reconnection handler was called + expect(reconnectionHandler.callCount).to.equal(1); + const [args2] = reconnectionHandler.firstCall.args as Parameters; expect(args2.clientId).to.equal(initialClientId); expect(args2.socket).to.have.property('id'); expect(args2.postMessage).to.be.a('function'); expect(args2.socket.id).to.not.equal(args1.socket.id); - // Verify disconnection handler was called after the client connection was replaced + // Verify disconnection handler was not called after the client connection was replaced await waitDisconnectFirstSocket; - expect(disconnectionHandler.callCount).to.equal(1); - const [disconnectArgs1] = disconnectionHandler.firstCall.args as Parameters; - expect(disconnectArgs1.clientId).to.equal(initialClientId); - expect(disconnectArgs1.postMessage).to.be.a('function'); - expect(disconnectArgs1.socket.id).to.equal(args1.socket.id); - expect(disconnectArgs1.hasActiveConnection).to.equal(true); + expect(disconnectionHandler.callCount).to.equal(0); // Disconnect an active socket await new Promise((resolve) => { @@ -243,12 +239,11 @@ describe('NodeEnvManager', () => { }); // Verify disconnection handler for active connection was called - expect(disconnectionHandler.callCount).to.equal(2); - const [disconnectArgs2] = disconnectionHandler.secondCall.args as Parameters; - expect(disconnectArgs2.clientId).to.equal(initialClientId); - expect(disconnectArgs2.postMessage).to.be.a('function'); - expect(disconnectArgs2.hasActiveConnection).to.equal(false); - expect(disconnectArgs2.socket.id).to.equal(args2.socket.id); + expect(disconnectionHandler.callCount).to.equal(1); + const [disconnectArgs1] = disconnectionHandler.firstCall.args as Parameters; + expect(disconnectArgs1.clientId).to.equal(initialClientId); + expect(disconnectArgs1.postMessage).to.be.a('function'); + expect(disconnectArgs1.socket.id).to.equal(args2.socket.id); await communication1.dispose(); await communication2.dispose(); From 9bff8eb36c25290bf22808bb1d6fa0aa38606fa0 Mon Sep 17 00:00:00 2001 From: Vladyslav Babak Date: Mon, 16 Feb 2026 09:55:30 +0100 Subject: [PATCH 17/19] refactor --- .../runtime-node/src/launch-http-server.ts | 11 ---------- packages/runtime-node/src/node-env-manager.ts | 21 ++++++++++++------- packages/runtime-node/src/ws-node-host.ts | 14 +++++++++++-- .../test/node-env.manager.unit.ts | 11 +++++----- 4 files changed, 30 insertions(+), 27 deletions(-) diff --git a/packages/runtime-node/src/launch-http-server.ts b/packages/runtime-node/src/launch-http-server.ts index d5a5b9c21..0fec58d46 100644 --- a/packages/runtime-node/src/launch-http-server.ts +++ b/packages/runtime-node/src/launch-http-server.ts @@ -2,7 +2,6 @@ import express from 'express'; import cors from 'cors'; import { safeListeningHttpServer } from 'create-listening-server'; import * as io from 'socket.io'; -import type { Message } from '@dazl/engine-core'; export const DEFAULT_PORT = 3000; @@ -16,22 +15,12 @@ export interface RouteMiddleware { handlers: express.RequestHandler | express.RequestHandler[]; } -export interface IConnectionEvent { - clientId: string; - socket: io.Socket; - postMessage: (message: Message) => void; -} -export type IConnectionHandler = (event: IConnectionEvent) => void; - export interface ILaunchHttpServerOptions { staticDirPath?: string; httpServerPort?: number; socketServerOptions?: Partial; routeMiddlewares?: Array; hostname?: string; - onConnectionOpen?: IConnectionHandler; - onConnectionClose?: IConnectionHandler; - onConnectionReconnect?: IConnectionHandler; } export async function launchEngineHttpServer({ diff --git a/packages/runtime-node/src/node-env-manager.ts b/packages/runtime-node/src/node-env-manager.ts index 95e744915..93eb7b634 100644 --- a/packages/runtime-node/src/node-env-manager.ts +++ b/packages/runtime-node/src/node-env-manager.ts @@ -10,7 +10,7 @@ import { IDisposable, SetMultiMap } from '@dazl/patterns'; import { fileURLToPath } from 'node:url'; import { parseArgs } from 'node:util'; import { extname } from 'node:path'; -import { WsServerHost } from './ws-node-host.js'; +import { ConnectionHandlers, WsServerHost } from './ws-node-host.js'; import { ILaunchHttpServerOptions, launchEngineHttpServer } from './launch-http-server.js'; import { workerThreadInitializer2 } from './worker-thread-initializer2.js'; import { bindMetricsListener, type PerformanceMetrics } from './metrics-utils.js'; @@ -49,7 +49,12 @@ export class NodeEnvManager implements IDisposable { ) {} public async autoLaunch( runtimeOptions: Map, - serverOptions: ILaunchHttpServerOptions = {}, + { + connectionHandlers, + ...serverOptions + }: ILaunchHttpServerOptions & { + connectionHandlers?: ConnectionHandlers; + } = {}, ) { process.env.ENGINE_FLOW_V2_DIST_URL = this.importMeta.url; const disposeMetricsListener = bindMetricsListener(() => this.collectMetricsFromAllOpenEnvironments()); @@ -60,14 +65,14 @@ export class NodeEnvManager implements IDisposable { runtimeOptions.set('enginePort', port.toString()); const clientsHost = new WsServerHost(socketServer); - const disposeOnConnectionOpen = serverOptions.onConnectionOpen - ? clientsHost.registerConnectionHandler(serverOptions.onConnectionOpen) + const disposeOnConnectionOpen = connectionHandlers?.onConnectionOpen + ? clientsHost.registerConnectionHandler(connectionHandlers.onConnectionOpen) : undefined; - const disposeOnConnectionClose = serverOptions.onConnectionClose - ? clientsHost.registerDisconnectionHandler(serverOptions.onConnectionClose) + const disposeOnConnectionClose = connectionHandlers?.onConnectionClose + ? clientsHost.registerDisconnectionHandler(connectionHandlers.onConnectionClose) : undefined; - const disposeOnReconnection = serverOptions.onConnectionReconnect - ? clientsHost.registerReconnectionHandler(serverOptions.onConnectionReconnect) + const disposeOnReconnection = connectionHandlers?.onConnectionReconnect + ? clientsHost.registerReconnectionHandler(connectionHandlers.onConnectionReconnect) : undefined; const disposeConnectionHandlers = () => { disposeOnConnectionOpen?.(); diff --git a/packages/runtime-node/src/ws-node-host.ts b/packages/runtime-node/src/ws-node-host.ts index 0b02dce51..7e9b51b1d 100644 --- a/packages/runtime-node/src/ws-node-host.ts +++ b/packages/runtime-node/src/ws-node-host.ts @@ -1,7 +1,18 @@ import type io from 'socket.io'; import { BaseHost, LoggerService, LogLevel, type Message } from '@dazl/engine-core'; import { SafeDisposable, type IDisposable } from '@dazl/patterns'; -import type { IConnectionEvent, IConnectionHandler } from './launch-http-server.js'; + +export interface IConnectionEvent { + clientId: string; + socket: io.Socket; +} +export type IConnectionHandler = (event: IConnectionEvent) => void; + +export interface ConnectionHandlers { + onConnectionOpen?: IConnectionHandler; + onConnectionClose?: IConnectionHandler; + onConnectionReconnect?: IConnectionHandler; +} export class WsHost extends BaseHost { constructor(private socket: io.Socket) { @@ -85,7 +96,6 @@ export class WsServerHost extends BaseHost implements IDisposable { const connectionEvent: IConnectionEvent = { clientId, socket, - postMessage: (message: Message) => this.postMessage(message), }; if (existingSocket && existingSocket.connected) { diff --git a/packages/runtime-node/test/node-env.manager.unit.ts b/packages/runtime-node/test/node-env.manager.unit.ts index 6eb7994b0..3d36145c1 100644 --- a/packages/runtime-node/test/node-env.manager.unit.ts +++ b/packages/runtime-node/test/node-env.manager.unit.ts @@ -176,9 +176,11 @@ describe('NodeEnvManager', () => { const manager = disposeAfterTest(new NodeEnvManager(meta, featureEnvironmentsMapping)); const { port } = await manager.autoLaunch(new Map([['feature', 'test-feature']]), { - onConnectionOpen: connectionHandler, - onConnectionClose: disconnectionHandler, - onConnectionReconnect: reconnectionHandler, + connectionHandlers: { + onConnectionOpen: connectionHandler, + onConnectionClose: disconnectionHandler, + onConnectionReconnect: reconnectionHandler, + }, }); // Create a client connection 1 @@ -199,7 +201,6 @@ describe('NodeEnvManager', () => { const [args1] = connectionHandler.firstCall.args as Parameters; expect(args1.clientId).to.equal(initialClientId); expect(args1.socket).to.have.property('id'); - expect(args1.postMessage).to.be.a('function'); // Replace a client connection const waitDisconnectFirstSocket = new Promise((resolve) => { @@ -223,7 +224,6 @@ describe('NodeEnvManager', () => { const [args2] = reconnectionHandler.firstCall.args as Parameters; expect(args2.clientId).to.equal(initialClientId); expect(args2.socket).to.have.property('id'); - expect(args2.postMessage).to.be.a('function'); expect(args2.socket.id).to.not.equal(args1.socket.id); // Verify disconnection handler was not called after the client connection was replaced @@ -242,7 +242,6 @@ describe('NodeEnvManager', () => { expect(disconnectionHandler.callCount).to.equal(1); const [disconnectArgs1] = disconnectionHandler.firstCall.args as Parameters; expect(disconnectArgs1.clientId).to.equal(initialClientId); - expect(disconnectArgs1.postMessage).to.be.a('function'); expect(disconnectArgs1.socket.id).to.equal(args2.socket.id); await communication1.dispose(); From 8eac7855d48da3111e0575fab8f98a62e7362ec8 Mon Sep 17 00:00:00 2001 From: Vladyslav Babak Date: Mon, 16 Feb 2026 10:25:06 +0100 Subject: [PATCH 18/19] refactor log error --- packages/core/src/com/logger-service.ts | 2 +- packages/runtime-node/src/ws-node-host.ts | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/core/src/com/logger-service.ts b/packages/core/src/com/logger-service.ts index 9a2e05088..57664d864 100644 --- a/packages/core/src/com/logger-service.ts +++ b/packages/core/src/com/logger-service.ts @@ -97,7 +97,7 @@ function getValue(message: LogValue): LogValueData { return logValue; } -function logToConsole({ message, metadata = {}, level }: LogMessage): void { +export function logToConsole({ message, metadata = {}, level }: LogMessage): void { switch (level) { case LogLevel.DEBUG: console.log(message, metadata); diff --git a/packages/runtime-node/src/ws-node-host.ts b/packages/runtime-node/src/ws-node-host.ts index 7e9b51b1d..4f19d3392 100644 --- a/packages/runtime-node/src/ws-node-host.ts +++ b/packages/runtime-node/src/ws-node-host.ts @@ -1,5 +1,5 @@ import type io from 'socket.io'; -import { BaseHost, LoggerService, LogLevel, type Message } from '@dazl/engine-core'; +import { BaseHost, LogLevel, logToConsole, type Message } from '@dazl/engine-core'; import { SafeDisposable, type IDisposable } from '@dazl/patterns'; export interface IConnectionEvent { @@ -33,7 +33,6 @@ export class WsServerHost extends BaseHost implements IDisposable { private socketToEnvId = new Map(); private clientIdToSocket = new Map(); private disposables = new SafeDisposable(WsServerHost.name); - private logger = new LoggerService([], {}, { logToConsole: true, severity: LogLevel.DEBUG }); dispose = this.disposables.dispose; isDisposed = this.disposables.isDisposed; @@ -83,7 +82,11 @@ export class WsServerHost extends BaseHost implements IDisposable { try { callback(); } catch (error) { - this.logger.error(error instanceof Error ? `${error.message} ${error.stack}` : String(error)); + logToConsole({ + message: error instanceof Error ? `${error.message} ${error.stack}` : String(error), + level: LogLevel.ERROR, + timestamp: Date.now(), + }); } } From b733ed9ac27fbfdaf0153ad8989eae35791f4a32 Mon Sep 17 00:00:00 2001 From: Vladyslav Babak Date: Mon, 16 Feb 2026 10:28:27 +0100 Subject: [PATCH 19/19] clear handlers --- packages/runtime-node/src/ws-node-host.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/runtime-node/src/ws-node-host.ts b/packages/runtime-node/src/ws-node-host.ts index 4f19d3392..33d1196dc 100644 --- a/packages/runtime-node/src/ws-node-host.ts +++ b/packages/runtime-node/src/ws-node-host.ts @@ -41,6 +41,9 @@ export class WsServerHost extends BaseHost implements IDisposable { this.server.on('connection', this.onConnection); this.disposables.add('connection', () => this.server.off('connection', this.onConnection)); this.disposables.add('clear handlers', () => this.handlers.clear()); + this.disposables.add('clear connection handlers', () => this.connectionHandlers.clear()); + this.disposables.add('clear disconnection handlers', () => this.disconnectionHandlers.clear()); + this.disposables.add('clear reconnection handlers', () => this.reconnectionHandlers.clear()); } public registerConnectionHandler(handler: IConnectionHandler) {