BREAKING CHANGE(smartsocket): Replace setExternalServer with hooks-based SmartServe integration and refactor SocketServer to support standalone and hooks modes

This commit is contained in:
2025-12-03 09:22:44 +00:00
parent 1d62c9c695
commit 09dbb00179
7 changed files with 161 additions and 175 deletions

View File

@@ -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

View File

@@ -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<any>;
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();

91
test/test.smartserve.ts Normal file
View File

@@ -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<any>;
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();

View File

@@ -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();

View File

@@ -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.'
}

View File

@@ -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();
}
/**

View File

@@ -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,47 +24,20 @@ 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 no port specified, we're in hooks mode - nothing to start
if (!this.smartsocket.options.port) {
return;
}
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 done = plugins.smartpromise.defer();
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;
@@ -89,7 +55,6 @@ export class SocketServer {
);
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();
}
},
};