@push.rocks/smartsocket 2.1.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import * as plugins from './smartsocket.plugins.js';
2
+ import * as interfaces from './interfaces/index.js';
2
3
 
3
4
  // import interfaces
4
5
  import {
@@ -78,11 +79,15 @@ export class SocketRequest<T extends plugins.typedrequestInterfaces.ITypedReques
78
79
  * dispatches a socketrequest from the requesting to the receiving side
79
80
  */
80
81
  public dispatch(): Promise<ISocketFunctionCallDataResponse<T>> {
81
- const requestData: ISocketRequestDataObject<T> = {
82
- funcCallData: this.funcCallData,
83
- shortId: this.shortid,
82
+ const message: interfaces.ISocketMessage<interfaces.IFunctionCallPayload> = {
83
+ type: 'function',
84
+ id: this.shortid,
85
+ payload: {
86
+ funcName: this.funcCallData.funcName,
87
+ funcData: this.funcCallData.funcDataArg,
88
+ },
84
89
  };
85
- this.originSocketConnection.socket.emit('function', requestData);
90
+ this.originSocketConnection.sendMessage(message);
86
91
  return this.done.promise;
87
92
  }
88
93
 
@@ -90,7 +95,6 @@ export class SocketRequest<T extends plugins.typedrequestInterfaces.ITypedReques
90
95
  * handles the response that is received by the requesting side
91
96
  */
92
97
  public async handleResponse(responseDataArg: ISocketRequestDataObject<T>) {
93
- // logger.log('info', 'handling response!');
94
98
  this.done.resolve(responseDataArg.funcCallData);
95
99
  this.smartsocketRef.socketRequests.remove(this);
96
100
  }
@@ -110,16 +114,23 @@ export class SocketRequest<T extends plugins.typedrequestInterfaces.ITypedReques
110
114
  logger.log('error', `There is no SocketFunction defined for ${this.funcCallData.funcName}`);
111
115
  return;
112
116
  }
113
- // logger.log('info', `invoking ${targetSocketFunction.name}`);
117
+
114
118
  targetSocketFunction
115
119
  .invoke(this.funcCallData, this.originSocketConnection)
116
120
  .then((resultData) => {
117
- // logger.log('info', 'got resultData. Sending it to requesting party.');
118
- const responseData: ISocketRequestDataObject<T> = {
119
- funcCallData: resultData,
120
- shortId: this.shortid,
121
+ const message: interfaces.ISocketMessage<interfaces.IFunctionCallPayload> = {
122
+ type: 'functionResponse',
123
+ id: this.shortid,
124
+ payload: {
125
+ funcName: resultData.funcName,
126
+ funcData: resultData.funcDataArg,
127
+ },
121
128
  };
122
- this.originSocketConnection.socket.emit('functionResponse', responseData);
129
+ this.originSocketConnection.sendMessage(message);
130
+ this.smartsocketRef.socketRequests.remove(this);
131
+ })
132
+ .catch((error) => {
133
+ logger.log('error', `Function invocation failed for ${this.funcCallData.funcName}: ${error instanceof Error ? error.message : String(error)}`);
123
134
  this.smartsocketRef.socketRequests.remove(this);
124
135
  });
125
136
  }
@@ -6,16 +6,16 @@ import { Smartsocket } from './smartsocket.classes.smartsocket.js';
6
6
  import { logger } from './smartsocket.logging.js';
7
7
 
8
8
  /**
9
- * class socketServer
10
- * handles the attachment of socketIo to whatever server is in play
9
+ * class SocketServer
10
+ * handles the WebSocket server in standalone mode, or provides hooks for smartserve integration
11
11
  */
12
12
  export class SocketServer {
13
13
  private smartsocket: Smartsocket;
14
- private httpServerDeferred: plugins.smartpromise.Deferred<any>;
15
14
  private httpServer: pluginsTyped.http.Server | pluginsTyped.https.Server;
15
+ private wsServer: pluginsTyped.ws.WebSocketServer;
16
16
 
17
17
  /**
18
- * wether httpServer is standalone
18
+ * whether httpServer is standalone (created by us)
19
19
  */
20
20
  private standaloneServer = false;
21
21
 
@@ -24,71 +24,180 @@ export class SocketServer {
24
24
  }
25
25
 
26
26
  /**
27
- * starts the server with another server
28
- * also works with an express style server
27
+ * Starts listening to incoming websocket connections (standalone mode).
28
+ * If no port is specified, this is a no-op (hooks mode via smartserve).
29
29
  */
30
- public async setExternalServer(
31
- serverType: 'smartexpress',
32
- serverArg: pluginsTyped.typedserver.servertools.Server
33
- ) {
34
- this.httpServerDeferred = plugins.smartpromise.defer();
35
- await serverArg.startedPromise;
36
- this.httpServer = serverArg.httpServer;
37
- this.httpServerDeferred.resolve();
38
- }
39
-
40
- /**
41
- * gets the server for socket.io
42
- */
43
- public async getServerForSocketIo() {
44
- if (this.httpServerDeferred) {
45
- await this.httpServerDeferred.promise;
46
- }
47
- if (this.httpServer) {
48
- return this.httpServer;
49
- } else {
50
- const httpModule = await this.smartsocket.smartenv.getSafeNodeModule('http');
51
- this.httpServer = new httpModule.Server();
52
- this.standaloneServer = true;
53
- return this.httpServer;
30
+ public async start() {
31
+ // If no port specified, we're in hooks mode - nothing to start
32
+ if (!this.smartsocket.options.port) {
33
+ return;
54
34
  }
35
+
36
+ // Standalone mode - create our own HTTP server and WebSocket server
37
+ const done = plugins.smartpromise.defer();
38
+ const httpModule = await this.smartsocket.smartenv.getSafeNodeModule('http');
39
+ const wsModule = await this.smartsocket.smartenv.getSafeNodeModule('ws');
40
+
41
+ this.httpServer = httpModule.createServer();
42
+ this.standaloneServer = true;
43
+
44
+ // Create WebSocket server attached to HTTP server
45
+ this.wsServer = new wsModule.WebSocketServer({ server: this.httpServer });
46
+
47
+ this.wsServer.on('connection', (ws: pluginsTyped.ws.WebSocket) => {
48
+ this.smartsocket.handleNewConnection(ws);
49
+ });
50
+
51
+ this.httpServer.listen(this.smartsocket.options.port, () => {
52
+ logger.log(
53
+ 'success',
54
+ `Server started in standalone mode on port ${this.smartsocket.options.port}`
55
+ );
56
+ done.resolve();
57
+ });
58
+
59
+ await done.promise;
55
60
  }
56
61
 
57
62
  /**
58
- * starts listening to incoming sockets:
63
+ * closes the server
59
64
  */
60
- public async start() {
61
- const done = plugins.smartpromise.defer();
65
+ public async stop() {
66
+ const done = plugins.smartpromise.defer<void>();
67
+ let resolved = false;
68
+
69
+ if (this.wsServer) {
70
+ // Close all WebSocket connections
71
+ this.wsServer.clients.forEach((client) => {
72
+ client.terminate();
73
+ });
74
+ this.wsServer.close();
75
+ this.wsServer = null;
76
+ }
62
77
 
63
- // handle http servers
64
- // in case an external server has been set "this.standaloneServer" should be false
65
78
  if (this.httpServer && this.standaloneServer) {
66
- if (!this.smartsocket.options.port) {
67
- logger.log('error', 'there should be a port specifed for smartsocket!');
68
- throw new Error('there should be a port specified for smartsocket');
69
- }
70
- this.httpServer.listen(this.smartsocket.options.port, () => {
71
- logger.log(
72
- 'success',
73
- `Server started in standalone mode on ${this.smartsocket.options.port}`
74
- );
75
- done.resolve();
79
+ const resolveOnce = () => {
80
+ if (!resolved) {
81
+ resolved = true;
82
+ this.httpServer = null;
83
+ this.standaloneServer = false;
84
+ done.resolve();
85
+ }
86
+ };
87
+
88
+ this.httpServer.close(() => {
89
+ resolveOnce();
76
90
  });
91
+
92
+ // Add a timeout in case close callback doesn't fire
93
+ const timeoutId = setTimeout(() => {
94
+ resolveOnce();
95
+ }, 2000);
96
+
97
+ // Ensure timeout doesn't keep process alive
98
+ if (timeoutId.unref) {
99
+ timeoutId.unref();
100
+ }
77
101
  } else {
78
102
  done.resolve();
79
103
  }
80
104
 
81
- // nothing else to do if express server is set
82
105
  await done.promise;
83
- return;
84
106
  }
85
107
 
86
108
  /**
87
- * closes the server
109
+ * Returns WebSocket hooks for integration with smartserve.
110
+ * Pass these hooks to SmartServe's websocket config.
88
111
  */
89
- public async stop() {
90
- if (this.httpServer) {
91
- this.httpServer.close();
92
- }
112
+ public getSmartserveWebSocketHooks(): pluginsTyped.ISmartserveWebSocketHooks {
113
+ return {
114
+ onOpen: async (peer: pluginsTyped.ISmartserveWebSocketPeer) => {
115
+ // Create a wrapper that adapts ISmartserveWebSocketPeer to WebSocket-like interface
116
+ const wsLikeSocket = this.createWsLikeFromPeer(peer);
117
+ await this.smartsocket.handleNewConnection(wsLikeSocket);
118
+ },
119
+ onMessage: async (peer: pluginsTyped.ISmartserveWebSocketPeer, message: pluginsTyped.ISmartserveWebSocketMessage) => {
120
+ // Dispatch message to the SocketConnection via the adapter
121
+ const adapter = peer.data.get('smartsocket_adapter') as any;
122
+ if (adapter) {
123
+ let textData: string | undefined;
124
+ if (message.type === 'text' && message.text) {
125
+ textData = message.text;
126
+ } else if (message.type === 'binary' && message.data) {
127
+ // Convert binary to text (Buffer/Uint8Array to string)
128
+ textData = new TextDecoder().decode(message.data);
129
+ }
130
+ if (textData) {
131
+ adapter.dispatchMessage(textData);
132
+ }
133
+ }
134
+ },
135
+ onClose: async (peer: pluginsTyped.ISmartserveWebSocketPeer, code: number, reason: string) => {
136
+ // Dispatch close to the SocketConnection via the adapter
137
+ const adapter = peer.data.get('smartsocket_adapter') as any;
138
+ if (adapter) {
139
+ adapter.dispatchClose();
140
+ }
141
+ },
142
+ onError: async (peer: pluginsTyped.ISmartserveWebSocketPeer, error: Error) => {
143
+ // Dispatch error to the SocketConnection via the adapter
144
+ const adapter = peer.data.get('smartsocket_adapter') as any;
145
+ if (adapter) {
146
+ adapter.dispatchError();
147
+ }
148
+ },
149
+ };
150
+ }
151
+
152
+ /**
153
+ * Creates a WebSocket-like object from a smartserve peer
154
+ * This allows our SocketConnection to work with both native WebSocket and smartserve peers
155
+ */
156
+ private createWsLikeFromPeer(peer: pluginsTyped.ISmartserveWebSocketPeer): pluginsTyped.IWebSocketLike {
157
+ const messageListeners: Array<(event: pluginsTyped.TMessageEvent) => void> = [];
158
+ const closeListeners: Array<() => void> = [];
159
+ const errorListeners: Array<() => void> = [];
160
+
161
+ // Store the adapter on the peer for message routing
162
+ peer.data.set('smartsocket_adapter', {
163
+ dispatchMessage: (data: string) => {
164
+ messageListeners.forEach((listener) => {
165
+ listener({ data });
166
+ });
167
+ },
168
+ dispatchClose: () => {
169
+ closeListeners.forEach((listener) => listener());
170
+ },
171
+ dispatchError: () => {
172
+ errorListeners.forEach((listener) => listener());
173
+ },
174
+ });
175
+
176
+ return {
177
+ get readyState() { return peer.readyState; },
178
+ send: (data: string) => peer.send(data),
179
+ close: (code?: number, reason?: string) => peer.close(code, reason),
180
+ addEventListener: (event: string, listener: any) => {
181
+ if (event === 'message') {
182
+ messageListeners.push(listener);
183
+ } else if (event === 'close') {
184
+ closeListeners.push(listener);
185
+ } else if (event === 'error') {
186
+ errorListeners.push(listener);
187
+ }
188
+ },
189
+ removeEventListener: (event: string, listener: any) => {
190
+ if (event === 'message') {
191
+ const idx = messageListeners.indexOf(listener);
192
+ if (idx >= 0) messageListeners.splice(idx, 1);
193
+ } else if (event === 'close') {
194
+ const idx = closeListeners.indexOf(listener);
195
+ if (idx >= 0) closeListeners.splice(idx, 1);
196
+ } else if (event === 'error') {
197
+ const idx = errorListeners.indexOf(listener);
198
+ if (idx >= 0) errorListeners.splice(idx, 1);
199
+ }
200
+ },
201
+ };
93
202
  }
94
203
  }
@@ -4,20 +4,66 @@ import type * as https from 'https';
4
4
 
5
5
  export type { http, https };
6
6
 
7
- // pushrocks scope
8
- import type * as typedserver from '@api.global/typedserver';
7
+ // third party scope - ws types
8
+ import type * as wsTypes from 'ws';
9
9
 
10
- export type { typedserver };
10
+ export namespace ws {
11
+ export type WebSocket = wsTypes.WebSocket;
12
+ export type WebSocketServer = wsTypes.WebSocketServer;
13
+ export type RawData = wsTypes.RawData;
14
+ }
15
+
16
+ /**
17
+ * Unified WebSocket type supporting both browser and Node.js environments
18
+ */
19
+ export type TWebSocket = WebSocket | ws.WebSocket;
11
20
 
12
- // third party scope
13
- import type { Socket as ServerSocket, Server as ServerServer } from 'socket.io';
14
- import type { Socket as ClientSocket, connect as ClientIo } from 'socket.io-client';
21
+ /**
22
+ * Message event type for WebSocket messages (browser and Node.js compatible)
23
+ */
24
+ export type TMessageEvent = MessageEvent | { data: string };
25
+
26
+ /**
27
+ * WebSocket-like interface for adapters (e.g., smartserve peer adapter)
28
+ */
29
+ export interface IWebSocketLike {
30
+ readyState: number;
31
+ send(data: string): void;
32
+ close(code?: number, reason?: string): void;
33
+ addEventListener(event: 'message', listener: (event: TMessageEvent) => void): void;
34
+ addEventListener(event: 'close', listener: () => void): void;
35
+ addEventListener(event: 'error', listener: () => void): void;
36
+ removeEventListener?(event: string, listener: (...args: any[]) => void): void;
37
+ }
15
38
 
16
- export namespace socketIo {
17
- export type Socket = ServerSocket;
18
- export type Server = ServerServer;
39
+ // smartserve compatibility interface (for setExternalServer)
40
+ // This mirrors the IWebSocketPeer interface from smartserve
41
+ export interface ISmartserveWebSocketPeer {
42
+ id: string;
43
+ url: string;
44
+ readyState: 0 | 1 | 2 | 3;
45
+ protocol: string;
46
+ extensions: string;
47
+ send(data: string): void;
48
+ sendBinary(data: Uint8Array | ArrayBuffer): void;
49
+ close(code?: number, reason?: string): void;
50
+ ping(data?: Uint8Array): void;
51
+ terminate(): void;
52
+ context: any;
53
+ data: Map<string, unknown>;
54
+ tags: Set<string>;
19
55
  }
20
- export namespace socketIoClient {
21
- export type Socket = ClientSocket;
22
- export type connect = typeof ClientIo;
56
+
57
+ export interface ISmartserveWebSocketMessage {
58
+ type: 'text' | 'binary';
59
+ text?: string;
60
+ data?: Uint8Array;
61
+ size: number;
62
+ }
63
+
64
+ export interface ISmartserveWebSocketHooks {
65
+ onOpen?: (peer: ISmartserveWebSocketPeer) => void | Promise<void>;
66
+ onMessage?: (peer: ISmartserveWebSocketPeer, message: ISmartserveWebSocketMessage) => void | Promise<void>;
67
+ onClose?: (peer: ISmartserveWebSocketPeer, code: number, reason: string) => void | Promise<void>;
68
+ onError?: (peer: ISmartserveWebSocketPeer, error: Error) => void | Promise<void>;
23
69
  }