diff --git a/changelog.md b/changelog.md index 453d674..922f2ab 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # Changelog +## 2025-12-03 - 3.0.0 - BREAKING CHANGE(smartsocket) +Replace setExternalServer with hooks-based SmartServe integration and refactor SocketServer to support standalone and hooks modes + +- Remove setExternalServer API and add getSmartserveWebSocketHooks on Smartsocket to provide SmartServe-compatible websocket hooks. +- SocketServer.start now becomes a no-op when no port is provided (hooks mode). When a port is set, it starts a standalone HTTP + ws server as before. +- Introduce an adapter (createWsLikeFromPeer) to adapt SmartServe peers to a WebSocket-like interface and route onMessage/onClose/onError via the adapter. +- Dispatch smartserve messages through the adapter: text/binary handling for onMessage, and dispatchClose/dispatchError for close/error events. +- Update tests: add smartserve integration test (test.smartserve.ts), adjust tagging test cleanup to stop client and delay before exit, remove outdated expressserver test. + ## 2025-03-10 - 2.1.0 - feat(SmartsocketClient) Improve client reconnection logic with exponential backoff and jitter; update socket.io and @types/node dependencies diff --git a/test/test.expressserver.ts b/test/test.expressserver.ts deleted file mode 100644 index 757c0e9..0000000 --- a/test/test.expressserver.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { expect, tap } from '@git.zone/tstest/tapbundle'; - -import * as smartsocket from '../ts/index.js'; - -let testSmartsocket: smartsocket.Smartsocket; -let testSmartsocketClient: smartsocket.SmartsocketClient; -let testSocketFunction1: smartsocket.SocketFunction; - -const testConfig = { - port: 3000, -}; - -// class smartsocket -tap.test('should create a new smartsocket', async () => { - testSmartsocket = new smartsocket.Smartsocket({ alias: 'testserver', port: testConfig.port }); - expect(testSmartsocket).toBeInstanceOf(smartsocket.Smartsocket); -}); - -// class SocketFunction -tap.test('should register a new Function', async () => { - testSocketFunction1 = new smartsocket.SocketFunction({ - funcDef: async (dataArg, socketConnectionArg) => { - return dataArg; - }, - funcName: 'testFunction1', - }); - testSmartsocket.addSocketFunction(testSocketFunction1); - console.log(testSmartsocket.socketFunctions); -}); - -tap.test('should start listening when .started is called', async () => { - await testSmartsocket.start(); -}); - -// class SmartsocketClient -tap.test('should react to a new websocket connection from client', async () => { - testSmartsocketClient = new smartsocket.SmartsocketClient({ - port: testConfig.port, - url: 'http://localhost', - alias: 'testClient1', - }); - testSmartsocketClient.addSocketFunction(testSocketFunction1); - await testSmartsocketClient.connect(); -}); - -tap.test('client should disconnect and reconnect', async (tools) => { - await testSmartsocketClient.disconnect(); - await tools.delayFor(100); - await testSmartsocketClient.connect(); -}); - -tap.test('2 clients should connect in parallel', async () => { - // TODO: implement parallel test -}); - -tap.test('should be able to make a functionCall from client to server', async () => { - const totalCycles = 100; // Reduced for faster test - let counter = 0; - let startTime = Date.now(); - while (counter < totalCycles) { - const randomString = `hello ${Math.random()}`; - const response: any = await testSmartsocketClient.serverCall('testFunction1', { - value1: randomString, - }); - expect(response.value1).toEqual(randomString); - if (counter % 50 === 0) { - console.log( - `processed 50 more messages in ${Date.now() - startTime}ms. ${ - totalCycles - counter - } messages to go.` - ); - startTime = Date.now(); - } - counter++; - } -}); - -tap.test('should be able to make a functionCall from server to client', async () => {}); - -// terminate -tap.test('should close the server', async () => { - await testSmartsocketClient.stop(); - await testSmartsocket.stop(); -}); - -export default tap.start(); diff --git a/test/test.smartserve.ts b/test/test.smartserve.ts new file mode 100644 index 0000000..bb89e8c --- /dev/null +++ b/test/test.smartserve.ts @@ -0,0 +1,91 @@ +import { expect, tap } from '@git.zone/tstest/tapbundle'; +import * as smartsocket from '../ts/index.js'; +import { SmartServe } from '@push.rocks/smartserve'; + +let smartserveInstance: SmartServe; +let testSmartsocket: smartsocket.Smartsocket; +let testSmartsocketClient: smartsocket.SmartsocketClient; +let testSocketFunction: smartsocket.SocketFunction; + +const testConfig = { + port: 3000, +}; + +// Setup smartsocket with smartserve integration +tap.test('should create smartsocket and smartserve with websocket hooks', async () => { + // Create smartsocket (no port - hooks mode for smartserve integration) + testSmartsocket = new smartsocket.Smartsocket({ alias: 'testserver-smartserve' }); + expect(testSmartsocket).toBeInstanceOf(smartsocket.Smartsocket); + + // Get websocket hooks from smartsocket and pass to smartserve + const wsHooks = testSmartsocket.getSmartserveWebSocketHooks(); + smartserveInstance = new SmartServe({ + port: testConfig.port, + websocket: wsHooks, + }); + // That's it! No setExternalServer needed - hooks connect everything +}); + +tap.test('should register a socket function', async () => { + testSocketFunction = new smartsocket.SocketFunction({ + funcDef: async (dataArg, socketConnectionArg) => { + return dataArg; + }, + funcName: 'testFunction1', + }); + testSmartsocket.addSocketFunction(testSocketFunction); +}); + +tap.test('should start smartserve', async () => { + await smartserveInstance.start(); + // No need to call testSmartsocket.start() - hooks mode doesn't need it +}); + +tap.test('should connect client through smartserve', async () => { + testSmartsocketClient = new smartsocket.SmartsocketClient({ + port: testConfig.port, + url: 'http://localhost', + alias: 'testClient1', + }); + testSmartsocketClient.addSocketFunction(testSocketFunction); + await testSmartsocketClient.connect(); +}); + +tap.test('should be able to make a functionCall from client to server', async () => { + const response: any = await testSmartsocketClient.serverCall('testFunction1', { + value1: 'hello from smartserve test', + }); + expect(response.value1).toEqual('hello from smartserve test'); +}); + +tap.test('should be able to make multiple function calls', async () => { + for (let i = 0; i < 10; i++) { + const randomString = `message-${i}-${Math.random()}`; + const response: any = await testSmartsocketClient.serverCall('testFunction1', { + value1: randomString, + }); + expect(response.value1).toEqual(randomString); + } +}); + +tap.test('client should disconnect and reconnect through smartserve', async (tools) => { + await testSmartsocketClient.disconnect(); + await tools.delayFor(100); + await testSmartsocketClient.connect(); + + // Verify connection still works after reconnect + const response: any = await testSmartsocketClient.serverCall('testFunction1', { + value1: 'after reconnect', + }); + expect(response.value1).toEqual('after reconnect'); +}); + +// Cleanup +tap.test('should close the server', async (tools) => { + await testSmartsocketClient.stop(); + await testSmartsocket.stop(); + await smartserveInstance.stop(); + tools.delayFor(1000).then(() => process.exit(0)); +}); + +export default tap.start(); diff --git a/test/test.tagging.ts b/test/test.tagging.ts index f747f41..f27096d 100644 --- a/test/test.tagging.ts +++ b/test/test.tagging.ts @@ -139,8 +139,10 @@ tap.test('should be able to locate a connection tag after reconnect', async (too }); // terminate -tap.test('should close the server', async () => { +tap.test('should close the server', async (tools) => { + await testSmartsocketClient.stop(); await testSmartsocket.stop(); + tools.delayFor(1000).then(() => process.exit(0)); }); export default tap.start(); diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index cdb65a0..0d7f5cb 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/smartsocket', - version: '2.1.0', + version: '3.0.0', description: 'Provides easy and secure websocket communication mechanisms, including server and client implementation, function call routing, connection management, and tagging.' } diff --git a/ts/smartsocket.classes.smartsocket.ts b/ts/smartsocket.classes.smartsocket.ts index aed3a95..c6326cb 100644 --- a/ts/smartsocket.classes.smartsocket.ts +++ b/ts/smartsocket.classes.smartsocket.ts @@ -40,14 +40,11 @@ export class Smartsocket { } /** - * Set an external server (smartserve) for WebSocket handling + * Returns WebSocket hooks for integration with smartserve + * Pass these hooks to SmartServe's websocket config */ - public async setExternalServer( - serverType: 'smartserve', - serverArg: any, - websocketHooks?: pluginsTyped.ISmartserveWebSocketHooks - ) { - await this.socketServer.setExternalServer(serverType, serverArg, websocketHooks); + public getSmartserveWebSocketHooks(): pluginsTyped.ISmartserveWebSocketHooks { + return this.socketServer.getSmartserveWebSocketHooks(); } /** diff --git a/ts/smartsocket.classes.socketserver.ts b/ts/smartsocket.classes.socketserver.ts index 312409f..14a7e64 100644 --- a/ts/smartsocket.classes.socketserver.ts +++ b/ts/smartsocket.classes.socketserver.ts @@ -7,7 +7,7 @@ import { logger } from './smartsocket.logging.js'; /** * class SocketServer - * handles the WebSocket server, either standalone or integrated with smartserve + * handles the WebSocket server in standalone mode, or provides hooks for smartserve integration */ export class SocketServer { private smartsocket: Smartsocket; @@ -15,14 +15,7 @@ export class SocketServer { private wsServer: pluginsTyped.ws.WebSocketServer; /** - * whether we're using an external server (smartserve) - */ - private externalServerMode = false; - private externalServer: any = null; - private externalWebSocketHooks: pluginsTyped.ISmartserveWebSocketHooks = null; - - /** - * whether httpServer is standalone + * whether httpServer is standalone (created by us) */ private standaloneServer = false; @@ -31,66 +24,38 @@ export class SocketServer { } /** - * Set an external server (smartserve) for WebSocket handling - */ - public async setExternalServer( - serverType: 'smartserve', - serverArg: any, - websocketHooks?: pluginsTyped.ISmartserveWebSocketHooks - ) { - if (serverType !== 'smartserve') { - throw new Error(`Unsupported server type: ${serverType}. Only 'smartserve' is supported.`); - } - this.externalServerMode = true; - this.externalServer = serverArg; - this.externalWebSocketHooks = websocketHooks || null; - } - - /** - * starts listening to incoming websocket connections + * Starts listening to incoming websocket connections (standalone mode). + * If no port is specified, this is a no-op (hooks mode via smartserve). */ public async start() { - const done = plugins.smartpromise.defer(); - - if (this.externalServerMode) { - // Using external smartserve server - // The smartserve server should be configured with websocket hooks - // that call our handleNewConnection method - logger.log('info', 'Using external smartserve server for WebSocket handling'); - - // If smartserve provides a way to get the underlying http server for upgrade, - // we could attach ws to it. For now, we expect smartserve to handle WS - // and call us back via the hooks. - done.resolve(); - } else { - // Standalone mode - create our own HTTP server and WebSocket server - const httpModule = await this.smartsocket.smartenv.getSafeNodeModule('http'); - const wsModule = await this.smartsocket.smartenv.getSafeNodeModule('ws'); - - if (!this.smartsocket.options.port) { - logger.log('error', 'there should be a port specified for smartsocket!'); - throw new Error('there should be a port specified for smartsocket'); - } - - this.httpServer = httpModule.createServer(); - this.standaloneServer = true; - - // Create WebSocket server attached to HTTP server - this.wsServer = new wsModule.WebSocketServer({ server: this.httpServer }); - - this.wsServer.on('connection', (ws: pluginsTyped.ws.WebSocket) => { - this.smartsocket.handleNewConnection(ws); - }); - - this.httpServer.listen(this.smartsocket.options.port, () => { - logger.log( - 'success', - `Server started in standalone mode on port ${this.smartsocket.options.port}` - ); - done.resolve(); - }); + // If no port specified, we're in hooks mode - nothing to start + if (!this.smartsocket.options.port) { + return; } + // Standalone mode - create our own HTTP server and WebSocket server + const done = plugins.smartpromise.defer(); + const httpModule = await this.smartsocket.smartenv.getSafeNodeModule('http'); + const wsModule = await this.smartsocket.smartenv.getSafeNodeModule('ws'); + + this.httpServer = httpModule.createServer(); + this.standaloneServer = true; + + // Create WebSocket server attached to HTTP server + this.wsServer = new wsModule.WebSocketServer({ server: this.httpServer }); + + this.wsServer.on('connection', (ws: pluginsTyped.ws.WebSocket) => { + this.smartsocket.handleNewConnection(ws); + }); + + this.httpServer.listen(this.smartsocket.options.port, () => { + logger.log( + 'success', + `Server started in standalone mode on port ${this.smartsocket.options.port}` + ); + done.resolve(); + }); + await done.promise; } @@ -141,8 +106,8 @@ export class SocketServer { } /** - * Returns WebSocket hooks for integration with smartserve - * Call this to get hooks that you can pass to smartserve's websocket config + * Returns WebSocket hooks for integration with smartserve. + * Pass these hooks to SmartServe's websocket config. */ public getSmartserveWebSocketHooks(): pluginsTyped.ISmartserveWebSocketHooks { return { @@ -150,27 +115,35 @@ export class SocketServer { // Create a wrapper that adapts ISmartserveWebSocketPeer to WebSocket-like interface const wsLikeSocket = this.createWsLikeFromPeer(peer); await this.smartsocket.handleNewConnection(wsLikeSocket as any); - - // Call external hooks if provided - if (this.externalWebSocketHooks?.onOpen) { - await this.externalWebSocketHooks.onOpen(peer); - } }, onMessage: async (peer: pluginsTyped.ISmartserveWebSocketPeer, message: pluginsTyped.ISmartserveWebSocketMessage) => { - // Messages are handled by SocketConnection via the adapter - // But we still call external hooks if provided - if (this.externalWebSocketHooks?.onMessage) { - await this.externalWebSocketHooks.onMessage(peer, message); + // Dispatch message to the SocketConnection via the adapter + const adapter = peer.data.get('smartsocket_adapter') as any; + if (adapter) { + let textData: string | undefined; + if (message.type === 'text' && message.text) { + textData = message.text; + } else if (message.type === 'binary' && message.data) { + // Convert binary to text (Buffer/Uint8Array to string) + textData = new TextDecoder().decode(message.data); + } + if (textData) { + adapter.dispatchMessage(textData); + } } }, onClose: async (peer: pluginsTyped.ISmartserveWebSocketPeer, code: number, reason: string) => { - if (this.externalWebSocketHooks?.onClose) { - await this.externalWebSocketHooks.onClose(peer, code, reason); + // Dispatch close to the SocketConnection via the adapter + const adapter = peer.data.get('smartsocket_adapter') as any; + if (adapter) { + adapter.dispatchClose(); } }, onError: async (peer: pluginsTyped.ISmartserveWebSocketPeer, error: Error) => { - if (this.externalWebSocketHooks?.onError) { - await this.externalWebSocketHooks.onError(peer, error); + // Dispatch error to the SocketConnection via the adapter + const adapter = peer.data.get('smartsocket_adapter') as any; + if (adapter) { + adapter.dispatchError(); } }, };