Compare commits

...

4 Commits

Author SHA1 Message Date
jkunz 38ab4703ea v4.0.1
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-04-30 15:22:54 +00:00
jkunz 554af7752e fix(build): tighten TypeScript compatibility and update project build configuration 2026-04-30 15:22:54 +00:00
jkunz d75486ac6e v4.0.0
Default (tags) / security (push) Failing after 19s
Default (tags) / test (push) Failing after 13s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-04 07:59:44 +00:00
jkunz 0499db71b8 BREAKING CHANGE(socketconnection): Stricter typings, smartserve hooks, connection fixes, and tag API change 2025-12-04 07:59:44 +00:00
18 changed files with 2916 additions and 4314 deletions
+38
View File
@@ -0,0 +1,38 @@
{
"@git.zone/cli": {
"projectType": "npm",
"module": {
"githost": "code.foss.global",
"gitscope": "push.rocks",
"gitrepo": "smartsocket",
"description": "Provides easy and secure websocket communication mechanisms, including server and client implementation, function call routing, connection management, and tagging.",
"npmPackagename": "@push.rocks/smartsocket",
"license": "MIT",
"projectDomain": "push.rocks"
},
"release": {
"registries": [
"https://verdaccio.lossless.digital",
"https://registry.npmjs.org"
],
"accessLevel": "public"
}
},
"@git.zone/tsdoc": {
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
},
"@git.zone/tsbundle": {
"bundles": [
{
"from": "./ts/index.ts",
"to": "./dist_bundle/bundle.js",
"outputMode": "bundle",
"bundler": "esbuild"
}
]
},
"@ship.zone/szci": {
"npmGlobalTools": [],
"npmRegistryUrl": "registry.npmjs.org"
}
}
+21
View File
@@ -1,5 +1,26 @@
# Changelog
## 2026-04-30 - 4.0.1 - fix(build)
tighten TypeScript compatibility and update project build configuration
- enable stricter null and implicit-any-safe typing across client, connection, request, and server classes
- add runtime guards for missing socket connections and missing function request ids
- update build and release configuration for tsbuild/tsbundle and npm publishing metadata
- refresh dependency versions and include smart config and license files in published package
- strengthen reconnect and tagging tests with explicit existence checks
## 2025-12-04 - 4.0.0 - BREAKING CHANGE(socketconnection)
Stricter typings, smartserve hooks, connection fixes, and tag API change
- Add unified WebSocket types and adapter interface (TWebSocket, TMessageEvent, IWebSocketLike) for browser/Node and smartserve peers
- Expose smartserve integration via getSmartserveWebSocketHooks() and adapt smartserve peers to a WebSocket-like interface (readyState getter, message/close/error dispatch)
- Improve SmartsocketClient connection logic: prevent duplicate connect attempts with isConnecting flag, ensure flags are cleared on success/failure, and tighten timeout/reconnect behavior
- Improve message parsing logging to surface JSON parse errors during authentication and normal operation
- Tighten TypeScript message and payload types (use unknown instead of any, Record<string, unknown> for tag payloads)
- SocketConnection API change: getTagById() and removeTagById() are now synchronous (no longer return Promises) — this is a breaking API change
- SocketRequest: log errors when function invocation fails and ensure pending requests are removed on errors
- Bump dependency @push.rocks/smartserve to ^1.1.2 and update README to reflect SmartServe integration changes
## 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
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2026 Task Venture Capital GmbH
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+14 -7
View File
@@ -1,5 +1,5 @@
{
"gitzone": {
"@git.zone/cli": {
"projectType": "npm",
"module": {
"githost": "code.foss.global",
@@ -21,13 +21,20 @@
"function routing",
"secure"
]
},
"release": {
"registries": [
"https://verdaccio.lossless.digital",
"https://registry.npmjs.org"
],
"accessLevel": "public"
}
},
"npmci": {
"npmGlobalTools": [],
"npmAccessLevel": "public"
},
"tsdoc": {
"@git.zone/tsdoc": {
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
},
"@ship.zone/szci": {
"npmGlobalTools": [],
"npmRegistryUrl": "registry.npmjs.org"
}
}
}
+18 -15
View File
@@ -1,13 +1,13 @@
{
"name": "@push.rocks/smartsocket",
"version": "3.0.0",
"version": "4.0.1",
"description": "Provides easy and secure websocket communication mechanisms, including server and client implementation, function call routing, connection management, and tagging.",
"main": "dist_ts/index.js",
"typings": "dist_ts/index.d.ts",
"type": "module",
"scripts": {
"test": "(tstest test/ --verbose)",
"build": "(tsbuild --web --allowimplicitany && tsbundle --from ./ts/index.ts --to dist_bundle/bundle.js)",
"build": "tsbuild --web && tsbundle",
"buildDocs": "tsdoc"
},
"repository": {
@@ -17,30 +17,30 @@
"author": "Task Venture Capital GmbH",
"license": "MIT",
"bugs": {
"url": "https://community.foss.global/"
"url": "https://code.foss.global/push.rocks/smartsocket/issues"
},
"homepage": "https://code.foss.global/push.rocks/smartsocket",
"dependencies": {
"@api.global/typedrequest-interfaces": "^3.0.19",
"@push.rocks/isohash": "^2.0.1",
"@push.rocks/isounique": "^1.0.5",
"@push.rocks/lik": "^6.2.2",
"@push.rocks/lik": "^6.4.1",
"@push.rocks/smartdelay": "^3.0.5",
"@push.rocks/smartenv": "^6.0.0",
"@push.rocks/smartjson": "^5.2.0",
"@push.rocks/smartlog": "^3.1.10",
"@push.rocks/smartjson": "^6.0.1",
"@push.rocks/smartlog": "^3.2.2",
"@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartrx": "^3.0.10",
"@push.rocks/smarttime": "^4.1.1",
"ws": "^8.18.3"
"@push.rocks/smarttime": "^4.2.3",
"ws": "^8.20.0"
},
"devDependencies": {
"@git.zone/tsbuild": "^3.1.2",
"@git.zone/tsbundle": "^2.6.3",
"@git.zone/tsrun": "^2.0.0",
"@git.zone/tstest": "^3.1.3",
"@push.rocks/smartserve": "^1.1.0",
"@types/node": "^24.10.1",
"@git.zone/tsbuild": "^4.4.0",
"@git.zone/tsbundle": "^2.10.1",
"@git.zone/tsrun": "^2.0.3",
"@git.zone/tstest": "^3.6.3",
"@push.rocks/smartserve": "^2.0.4",
"@types/node": "^25.6.0",
"@types/ws": "^8.18.1"
},
"private": false,
@@ -53,6 +53,8 @@
"dist_ts_web/**/*",
"assets/**/*",
"cli.js",
".smartconfig.json",
"license",
"npmextra.json",
"readme.md"
],
@@ -71,5 +73,6 @@
"function routing",
"secure",
"rpc"
]
],
"packageManager": "pnpm@10.28.2"
}
+2663 -4228
View File
File diff suppressed because it is too large Load Diff
+14 -10
View File
@@ -143,20 +143,24 @@ await socketConnection.addTag({
Use smartsocket with `@push.rocks/smartserve` for advanced HTTP/WebSocket handling:
```typescript
import { Smartserve } from '@push.rocks/smartserve';
import { SmartServe } from '@push.rocks/smartserve';
import { Smartsocket } from '@push.rocks/smartsocket';
const smartserve = new Smartserve({ port: 3000 });
// Create smartsocket without a port (hooks mode)
const smartsocket = new Smartsocket({ alias: 'myServer' });
// Set smartserve as external server
await smartsocket.setExternalServer('smartserve', smartserve);
// Get WebSocket hooks and pass them to SmartServe
const wsHooks = smartsocket.getSmartserveWebSocketHooks();
const smartserve = new SmartServe({
port: 3000,
websocket: wsHooks
});
// Get WebSocket hooks for smartserve
const wsHooks = smartsocket.socketServer.getSmartserveWebSocketHooks();
// Add socket functions as usual
smartsocket.addSocketFunction(myFunction);
// Configure smartserve with the hooks
// (see smartserve documentation for integration details)
// Start smartserve (smartsocket hooks mode doesn't need start())
await smartserve.start();
```
### Handling Disconnections
@@ -223,11 +227,11 @@ const response = await client.serverCall<IGreetRequest>('greet', { name: 'Bob' }
| Method | Description |
|--------|-------------|
| `start()` | Start the WebSocket server |
| `start()` | Start the WebSocket server (not needed in hooks mode) |
| `stop()` | Stop the server and close all connections |
| `addSocketFunction(fn)` | Register a function that clients can call |
| `clientCall(funcName, data, connection)` | Call a function on a specific client |
| `setExternalServer(type, server)` | Use an external server (smartserve) |
| `getSmartserveWebSocketHooks()` | Get hooks for smartserve integration |
### SmartsocketClient
+12
View File
@@ -81,6 +81,9 @@ tap.test('should be able to tag a connection from client', async (tools) => {
return true;
})
.getTagById('awesome');
if (!tagOnServerSide) {
throw new Error('Expected server-side tag awesome to exist');
}
expect(tagOnServerSide.payload).toEqual('yes');
});
@@ -93,7 +96,13 @@ tap.test('should be able to tag a connection from server', async (tools) => {
id: 'awesome2',
payload: 'absolutely',
});
if (!testSmartsocketClient.socketConnection) {
throw new Error('Expected client socket connection to exist');
}
const tagOnClientSide = await testSmartsocketClient.socketConnection.getTagById('awesome2');
if (!tagOnClientSide) {
throw new Error('Expected client-side tag awesome2 to exist');
}
expect(tagOnClientSide.payload).toEqual('absolutely');
});
@@ -142,6 +151,9 @@ tap.test('should be able to locate a connection tag after reconnect', async (too
return true;
})
.getTagById('awesome');
if (!tagOnServerSide) {
throw new Error('Expected server-side tag awesome to exist after reconnect');
}
expect(tagOnServerSide.payload).toEqual('yes');
});
+12
View File
@@ -81,6 +81,9 @@ tap.test('should be able to tag a connection from client', async (tools) => {
return true;
})
.getTagById('awesome');
if (!tagOnServerSide) {
throw new Error('Expected server-side tag awesome to exist');
}
expect(tagOnServerSide.payload).toEqual('yes');
});
@@ -93,7 +96,13 @@ tap.test('should be able to tag a connection from server', async (tools) => {
id: 'awesome2',
payload: 'absolutely',
});
if (!testSmartsocketClient.socketConnection) {
throw new Error('Expected client socket connection to exist');
}
const tagOnClientSide = await testSmartsocketClient.socketConnection.getTagById('awesome2');
if (!tagOnClientSide) {
throw new Error('Expected client-side tag awesome2 to exist');
}
expect(tagOnClientSide.payload).toEqual('absolutely');
});
@@ -135,6 +144,9 @@ tap.test('should be able to locate a connection tag after reconnect', async (too
return true;
})
.getTagById('awesome');
if (!tagOnServerSide) {
throw new Error('Expected server-side tag awesome to exist after reconnect');
}
expect(tagOnServerSide.payload).toEqual('yes');
});
+1 -1
View File
@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartsocket',
version: '3.0.0',
version: '4.0.1',
description: 'Provides easy and secure websocket communication mechanisms, including server and client implementation, function call routing, connection management, and tagging.'
}
+4 -4
View File
@@ -13,7 +13,7 @@ export type TMessageType =
/**
* Base message interface for all smartsocket messages
*/
export interface ISocketMessage<T = any> {
export interface ISocketMessage<T = unknown> {
type: TMessageType;
id?: string; // For request/response correlation
payload: T;
@@ -44,16 +44,16 @@ export interface IAuthResponsePayload {
/**
* Function call payload
*/
export interface IFunctionCallPayload {
export interface IFunctionCallPayload<T = unknown> {
funcName: string;
funcData: any;
funcData: T;
}
/**
* Tag update payload
*/
export interface ITagUpdatePayload {
tags: { [key: string]: any };
tags: Record<string, unknown>;
}
/**
+3 -3
View File
@@ -58,7 +58,7 @@ export class Smartsocket {
* Handle a new WebSocket connection
* Called by SocketServer when a new connection is established
*/
public async handleNewConnection(socket: WebSocket | pluginsTyped.ws.WebSocket) {
public async handleNewConnection(socket: pluginsTyped.TWebSocket | pluginsTyped.IWebSocketLike) {
const socketConnection: SocketConnection = new SocketConnection({
alias: undefined,
authenticated: false,
@@ -76,8 +76,8 @@ export class Smartsocket {
socketConnection.eventSubject.next('disconnected');
};
socket.addEventListener('close', handleClose);
socket.addEventListener('error', handleClose);
(socket as pluginsTyped.IWebSocketLike).addEventListener('close', handleClose);
(socket as pluginsTyped.IWebSocketLike).addEventListener('error', handleClose);
try {
await socketConnection.authenticate();
+23 -10
View File
@@ -28,10 +28,10 @@ export class SmartsocketClient {
public shortId = plugins.isounique.uni();
// the shortId of the remote we connect to
public remoteShortId: string = null;
public remoteShortId: string | null = null;
public alias: string;
public socketConnection: SocketConnection;
public socketConnection?: SocketConnection;
public serverUrl: string;
public serverPort: number;
public autoReconnect: boolean;
@@ -50,7 +50,7 @@ export class SmartsocketClient {
// tagStore
private tagStore: { [key: string]: interfaces.ITag } = {};
private tagStoreSubscription: plugins.smartrx.rxjs.Subscription;
private tagStoreSubscription?: plugins.smartrx.rxjs.Subscription;
/**
* adds a tag to a connection
@@ -86,7 +86,7 @@ export class SmartsocketClient {
this.alias = optionsArg.alias;
this.serverUrl = optionsArg.url;
this.serverPort = optionsArg.port;
this.autoReconnect = optionsArg.autoReconnect;
this.autoReconnect = optionsArg.autoReconnect ?? false;
this.maxRetries = optionsArg.maxRetries ?? 100; // Default to 100 retries
this.initialBackoffDelay = optionsArg.initialBackoffDelay ?? 1000; // Default to 1 second
this.maxBackoffDelay = optionsArg.maxBackoffDelay ?? 60000; // Default to 1 minute
@@ -98,11 +98,18 @@ export class SmartsocketClient {
}
private isReconnecting = false;
private isConnecting = false;
/**
* connect the client to the server
*/
public async connect() {
// Prevent duplicate connection attempts
if (this.isConnecting) {
return;
}
this.isConnecting = true;
// Only reset retry counters on fresh connection (not during auto-reconnect)
if (!this.isReconnecting) {
this.currentRetryCount = 0;
@@ -141,6 +148,7 @@ export class SmartsocketClient {
smartsocketHost: this,
socket: socket as any,
});
const socketConnection = this.socketConnection;
// Increment attempt ID to invalidate any pending timers from previous attempts
this.connectionAttemptId++;
@@ -176,7 +184,7 @@ export class SmartsocketClient {
this.remoteShortId = authRequestPayload.serverAlias;
// Send authentication data
this.socketConnection.sendMessage({
socketConnection.sendMessage({
type: 'auth',
payload: { alias: this.alias },
});
@@ -186,7 +194,7 @@ export class SmartsocketClient {
const authResponse = message.payload as interfaces.IAuthResponsePayload;
if (authResponse.success) {
logger.log('info', 'client is authenticated');
this.socketConnection.authenticated = true;
socketConnection.authenticated = true;
} else {
logger.log('warn', `authentication failed: ${authResponse.error}`);
await this.disconnect();
@@ -195,15 +203,15 @@ export class SmartsocketClient {
case 'serverReady':
// Set up function request listening
await this.socketConnection.listenToFunctionRequests();
await socketConnection.listenToFunctionRequests();
// Handle retagging
const oldTagStore = this.tagStore;
this.tagStoreSubscription?.unsubscribe();
for (const keyArg of Object.keys(this.tagStore)) {
this.socketConnection.addTag(this.tagStore[keyArg]);
socketConnection.addTag(this.tagStore[keyArg]);
}
this.tagStoreSubscription = this.socketConnection.tagStoreObservable.subscribe(
this.tagStoreSubscription = socketConnection.tagStoreObservable.subscribe(
(tagStoreArg) => {
this.tagStore = tagStoreArg;
}
@@ -213,12 +221,13 @@ export class SmartsocketClient {
await this.addTag(oldTagStore[tag]);
}
this.updateStatus('connected');
this.isConnecting = false;
done.resolve();
break;
default:
// Other messages are handled by SocketConnection
this.socketConnection.handleMessage(message);
socketConnection.handleMessage(message);
break;
}
} catch (err) {
@@ -262,6 +271,7 @@ export class SmartsocketClient {
return;
}
this.disconnectRunning = true;
this.isConnecting = false;
this.updateStatus('disconnecting');
this.tagStoreSubscription?.unsubscribe();
@@ -332,6 +342,9 @@ export class SmartsocketClient {
functionNameArg: T['method'],
dataArg: T['request']
): Promise<T['response']> {
if (!this.socketConnection) {
throw new Error('Cannot call server without an active socket connection');
}
const socketRequest = new SocketRequest<T>(this, {
side: 'requesting',
originSocketConnection: this.socketConnection,
+28 -21
View File
@@ -22,11 +22,11 @@ export type TSocketConnectionSide = 'server' | 'client';
* interface for constructor of class SocketConnection
*/
export interface ISocketConnectionConstructorOptions {
alias: string;
alias?: string;
authenticated: boolean;
side: TSocketConnectionSide;
smartsocketHost: Smartsocket | SmartsocketClient;
socket: WebSocket | pluginsTyped.ws.WebSocket;
socket: pluginsTyped.TWebSocket | pluginsTyped.IWebSocketLike;
}
/**
@@ -43,11 +43,11 @@ export let allSocketConnections = new plugins.lik.ObjectMap<SocketConnection>();
* class SocketConnection represents a websocket connection
*/
export class SocketConnection {
public alias: string;
public alias?: string;
public side: TSocketConnectionSide;
public authenticated: boolean = false;
public smartsocketRef: Smartsocket | SmartsocketClient;
public socket: WebSocket | pluginsTyped.ws.WebSocket;
public socket: pluginsTyped.TWebSocket | pluginsTyped.IWebSocketLike;
public eventSubject = new plugins.smartrx.rxjs.Subject<interfaces.TConnectionStatus>();
public eventStatus: interfaces.TConnectionStatus = 'new';
@@ -82,13 +82,13 @@ export class SocketConnection {
public handleMessage(messageData: interfaces.ISocketMessage): void {
switch (messageData.type) {
case 'function':
this.handleFunctionCall(messageData);
this.handleFunctionCall(messageData as interfaces.ISocketMessage<interfaces.IFunctionCallPayload>);
break;
case 'functionResponse':
this.handleFunctionResponse(messageData);
this.handleFunctionResponse(messageData as interfaces.ISocketMessage<interfaces.IFunctionCallPayload>);
break;
case 'tagUpdate':
this.handleTagUpdate(messageData);
this.handleTagUpdate(messageData as interfaces.ISocketMessage<interfaces.ITagUpdatePayload>);
break;
default:
// Authentication messages are handled by the server/client classes
@@ -96,7 +96,11 @@ export class SocketConnection {
}
}
private handleFunctionCall(messageData: interfaces.ISocketMessage): void {
private handleFunctionCall(messageData: interfaces.ISocketMessage<interfaces.IFunctionCallPayload>): void {
if (!messageData.id) {
logger.log('warn', 'received function call without request id');
return;
}
const requestData: ISocketRequestDataObject<any> = {
funcCallData: {
funcName: messageData.payload.funcName,
@@ -123,7 +127,11 @@ export class SocketConnection {
}
}
private handleFunctionResponse(messageData: interfaces.ISocketMessage): void {
private handleFunctionResponse(messageData: interfaces.ISocketMessage<interfaces.IFunctionCallPayload>): void {
if (!messageData.id) {
logger.log('warn', 'received function response without request id');
return;
}
const responseData: ISocketRequestDataObject<any> = {
funcCallData: {
funcName: messageData.payload.funcName,
@@ -141,7 +149,7 @@ export class SocketConnection {
}
}
private handleTagUpdate(messageData: interfaces.ISocketMessage): void {
private handleTagUpdate(messageData: interfaces.ISocketMessage<interfaces.ITagUpdatePayload>): void {
const tagStoreArg = messageData.payload.tags as interfaces.TTagStore;
if (!plugins.smartjson.deepEqualObjects(this.tagStore, tagStoreArg)) {
this.tagStore = tagStoreArg;
@@ -181,17 +189,16 @@ export class SocketConnection {
}
/**
* gets a tag by id
* @param tagIdArg
* Gets a tag by id
*/
public async getTagById(tagIdArg: interfaces.ITag['id']) {
public getTagById(tagIdArg: interfaces.ITag['id']): interfaces.ITag | undefined {
return this.tagStore[tagIdArg];
}
/**
* removes a tag from a connection
* Removes a tag from a connection
*/
public async removeTagById(tagIdArg: interfaces.ITag['id']) {
public removeTagById(tagIdArg: interfaces.ITag['id']): void {
delete this.tagStore[tagIdArg];
this.tagStoreObservable.next(this.tagStore);
this.sendMessage({
@@ -209,7 +216,7 @@ export class SocketConnection {
const done = plugins.smartpromise.defer<SocketConnection>();
// Set up message handler for authentication
const messageHandler = (event: MessageEvent | { data: string }) => {
const messageHandler = (event: pluginsTyped.TMessageEvent) => {
try {
const data = typeof event.data === 'string' ? event.data : event.data.toString();
const message: interfaces.ISocketMessage = JSON.parse(data);
@@ -241,11 +248,11 @@ export class SocketConnection {
}
}
} catch (err) {
// Not a valid message, ignore
logger.log('warn', `Failed to parse auth message: ${err instanceof Error ? err.message : String(err)}`);
}
};
this.socket.addEventListener('message', messageHandler as any);
(this.socket as pluginsTyped.IWebSocketLike).addEventListener('message', messageHandler);
// Request authentication
const requestAuthPayload: interfaces.TAuthRequestMessage = {
@@ -268,17 +275,17 @@ export class SocketConnection {
const done = plugins.smartpromise.defer();
if (this.authenticated) {
// Set up message handler for all messages
const messageHandler = (event: MessageEvent | { data: string }) => {
const messageHandler = (event: pluginsTyped.TMessageEvent) => {
try {
const data = typeof event.data === 'string' ? event.data : event.data.toString();
const message: interfaces.ISocketMessage = JSON.parse(data);
this.handleMessage(message);
} catch (err) {
// Not a valid JSON message, ignore
logger.log('warn', `Failed to parse socket message: ${err instanceof Error ? err.message : String(err)}`);
}
};
this.socket.addEventListener('message', messageHandler as any);
(this.socket as pluginsTyped.IWebSocketLike).addEventListener('message', messageHandler);
logger.log(
'info',
+5 -1
View File
@@ -27,7 +27,7 @@ export interface ISocketRequestConstructorOptions<
side: TSocketRequestSide;
originSocketConnection: SocketConnection;
shortId: string;
funcCallData?: ISocketFunctionCallDataRequest<T>;
funcCallData: ISocketFunctionCallDataRequest<T>;
}
/**
@@ -128,6 +128,10 @@ export class SocketRequest<T extends plugins.typedrequestInterfaces.ITypedReques
};
this.originSocketConnection.sendMessage(message);
this.smartsocketRef.socketRequests.remove(this);
})
.catch((error) => {
logger.log('error', `Function invocation failed for ${this.funcCallData.funcName}: ${error instanceof Error ? error.message : String(error)}`);
this.smartsocketRef.socketRequests.remove(this);
});
}
}
+12 -10
View File
@@ -11,8 +11,8 @@ import { logger } from './smartsocket.logging.js';
*/
export class SocketServer {
private smartsocket: Smartsocket;
private httpServer: pluginsTyped.http.Server | pluginsTyped.https.Server;
private wsServer: pluginsTyped.ws.WebSocketServer;
private httpServer: pluginsTyped.http.Server | pluginsTyped.https.Server | null = null;
private wsServer: pluginsTyped.ws.WebSocketServer | null = null;
/**
* whether httpServer is standalone (created by us)
@@ -38,17 +38,19 @@ export class SocketServer {
const httpModule = await this.smartsocket.smartenv.getSafeNodeModule('http');
const wsModule = await this.smartsocket.smartenv.getSafeNodeModule('ws');
this.httpServer = httpModule.createServer();
const httpServer = httpModule.createServer();
this.httpServer = httpServer;
this.standaloneServer = true;
// Create WebSocket server attached to HTTP server
this.wsServer = new wsModule.WebSocketServer({ server: this.httpServer });
const wsServer = new wsModule.WebSocketServer({ server: httpServer });
this.wsServer = wsServer;
this.wsServer.on('connection', (ws: pluginsTyped.ws.WebSocket) => {
wsServer.on('connection', (ws: pluginsTyped.ws.WebSocket) => {
this.smartsocket.handleNewConnection(ws);
});
this.httpServer.listen(this.smartsocket.options.port, () => {
httpServer.listen(this.smartsocket.options.port, () => {
logger.log(
'success',
`Server started in standalone mode on port ${this.smartsocket.options.port}`
@@ -114,7 +116,7 @@ export class SocketServer {
onOpen: async (peer: pluginsTyped.ISmartserveWebSocketPeer) => {
// Create a wrapper that adapts ISmartserveWebSocketPeer to WebSocket-like interface
const wsLikeSocket = this.createWsLikeFromPeer(peer);
await this.smartsocket.handleNewConnection(wsLikeSocket as any);
await this.smartsocket.handleNewConnection(wsLikeSocket);
},
onMessage: async (peer: pluginsTyped.ISmartserveWebSocketPeer, message: pluginsTyped.ISmartserveWebSocketMessage) => {
// Dispatch message to the SocketConnection via the adapter
@@ -153,8 +155,8 @@ export class SocketServer {
* Creates a WebSocket-like object from a smartserve peer
* This allows our SocketConnection to work with both native WebSocket and smartserve peers
*/
private createWsLikeFromPeer(peer: pluginsTyped.ISmartserveWebSocketPeer): any {
const messageListeners: Array<(event: any) => void> = [];
private createWsLikeFromPeer(peer: pluginsTyped.ISmartserveWebSocketPeer): pluginsTyped.IWebSocketLike {
const messageListeners: Array<(event: pluginsTyped.TMessageEvent) => void> = [];
const closeListeners: Array<() => void> = [];
const errorListeners: Array<() => void> = [];
@@ -174,7 +176,7 @@ export class SocketServer {
});
return {
readyState: peer.readyState,
get readyState() { return peer.readyState; },
send: (data: string) => peer.send(data),
close: (code?: number, reason?: string) => peer.close(code, reason),
addEventListener: (event: string, listener: any) => {
+23
View File
@@ -13,6 +13,29 @@ export namespace ws {
export type RawData = wsTypes.RawData;
}
/**
* Unified WebSocket type supporting both browser and Node.js environments
*/
export type TWebSocket = WebSocket | ws.WebSocket;
/**
* Message event type for WebSocket messages (browser and Node.js compatible)
*/
export type TMessageEvent = MessageEvent | { data: string };
/**
* WebSocket-like interface for adapters (e.g., smartserve peer adapter)
*/
export interface IWebSocketLike {
readyState: number;
send(data: string): void;
close(code?: number, reason?: string): void;
addEventListener(event: 'message', listener: (event: TMessageEvent) => void): void;
addEventListener(event: 'close', listener: () => void): void;
addEventListener(event: 'error', listener: () => void): void;
removeEventListener?(event: string, listener: (...args: any[]) => void): void;
}
// smartserve compatibility interface (for setExternalServer)
// This mirrors the IWebSocketPeer interface from smartserve
export interface ISmartserveWebSocketPeer {
+4 -4
View File
@@ -5,10 +5,10 @@
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"noImplicitAny": true,
"esModuleInterop": true,
"verbatimModuleSyntax": true
"verbatimModuleSyntax": true,
"types": ["node"]
},
"exclude": [
"dist_*/**/*.d.ts"
]
"exclude": ["dist_*/**/*.d.ts"]
}