@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.
@@ -16,7 +16,7 @@ import { logger } from './smartsocket.logging.js';
16
16
  export interface ISmartsocketClientOptions {
17
17
  port: number;
18
18
  url: string;
19
- alias: string; // an alias makes it easier to identify this client in a multo client environment
19
+ alias: string; // an alias makes it easier to identify this client in a multi client environment
20
20
  autoReconnect?: boolean;
21
21
  maxRetries?: number; // maximum number of reconnection attempts
22
22
  initialBackoffDelay?: number; // initial backoff delay in ms
@@ -97,115 +97,170 @@ export class SmartsocketClient {
97
97
  this.socketFunctions.add(socketFunction);
98
98
  }
99
99
 
100
+ private isReconnecting = false;
101
+ private isConnecting = false;
102
+
100
103
  /**
101
104
  * connect the client to the server
102
105
  */
103
106
  public async connect() {
104
- // Reset retry counters on new connection attempt
105
- this.currentRetryCount = 0;
106
- this.currentBackoffDelay = this.initialBackoffDelay;
107
-
107
+ // Prevent duplicate connection attempts
108
+ if (this.isConnecting) {
109
+ return;
110
+ }
111
+ this.isConnecting = true;
112
+
113
+ // Only reset retry counters on fresh connection (not during auto-reconnect)
114
+ if (!this.isReconnecting) {
115
+ this.currentRetryCount = 0;
116
+ this.currentBackoffDelay = this.initialBackoffDelay;
117
+ }
118
+ this.isReconnecting = false;
119
+
108
120
  const done = plugins.smartpromise.defer();
109
121
  const smartenvInstance = new plugins.smartenv.Smartenv();
110
- const socketIoClient: any = await smartenvInstance.getEnvAwareModule({
111
- nodeModuleName: 'socket.io-client',
112
- webUrlArg: 'https://cdn.jsdelivr.net/npm/socket.io-client@4/dist/socket.io.js',
113
- getFunction: () => {
114
- const socketIoBrowserModule = (globalThis as any).io;
115
- // console.log('loaded socket.io for browser');
116
- return socketIoBrowserModule;
117
- },
118
- });
119
- // console.log(socketIoClient);
122
+
120
123
  logger.log('info', 'trying to connect...');
121
- const socketUrl = `${this.serverUrl}:${this.serverPort}`;
124
+
125
+ // Construct WebSocket URL
126
+ const protocol = this.serverUrl.startsWith('https') ? 'wss' : 'ws';
127
+ const host = this.serverUrl.replace(/^https?:\/\//, '');
128
+ const socketUrl = `${protocol}://${host}:${this.serverPort}`;
129
+
130
+ // Get WebSocket implementation (native in browser, ws in Node)
131
+ let WebSocketClass: typeof WebSocket;
132
+ if (typeof WebSocket !== 'undefined') {
133
+ // Browser environment
134
+ WebSocketClass = WebSocket;
135
+ } else {
136
+ // Node.js environment
137
+ const wsModule = await smartenvInstance.getSafeNodeModule('ws');
138
+ WebSocketClass = wsModule.default || wsModule;
139
+ }
140
+
141
+ const socket = new WebSocketClass(socketUrl);
142
+ this.currentSocket = socket;
143
+
122
144
  this.socketConnection = new SocketConnection({
123
145
  alias: this.alias,
124
146
  authenticated: false,
125
147
  side: 'client',
126
148
  smartsocketHost: this,
127
- socket: await socketIoClient
128
- .connect(socketUrl, {
129
- multiplex: true,
130
- rememberUpgrade: true,
131
- autoConnect: false,
132
- reconnectionAttempts: 0,
133
- rejectUnauthorized: socketUrl.startsWith('https://localhost') ? false : true,
134
- })
135
- .open(),
149
+ socket: socket as any,
136
150
  });
137
151
 
152
+ // Increment attempt ID to invalidate any pending timers from previous attempts
153
+ this.connectionAttemptId++;
154
+ const currentAttemptId = this.connectionAttemptId;
155
+
138
156
  const timer = new plugins.smarttime.Timer(5000);
139
157
  timer.start();
140
158
  timer.completed.then(() => {
141
- this.updateStatus('timedOut');
142
- logger.log('warn', 'connection to server timed out.');
143
- this.disconnect(true);
159
+ // Only fire timeout if this is still the current connection attempt
160
+ if (currentAttemptId === this.connectionAttemptId && this.eventStatus !== 'connected') {
161
+ this.updateStatus('timedOut');
162
+ logger.log('warn', 'connection to server timed out.');
163
+ this.disconnect(true);
164
+ }
144
165
  });
145
166
 
146
- // authentication flow
147
- this.socketConnection.socket.on('requestAuth', (dataArg: interfaces.IRequestAuthPayload) => {
167
+ // Handle connection open
168
+ socket.addEventListener('open', () => {
148
169
  timer.reset();
149
- logger.log('info', `server ${dataArg.serverAlias} requested authentication`);
150
-
151
- // lets register the authenticated event
152
- this.socketConnection.socket.on('authenticated', async () => {
153
- this.remoteShortId = dataArg.serverAlias;
154
- logger.log('info', 'client is authenticated');
155
- this.socketConnection.authenticated = true;
156
- await this.socketConnection.listenToFunctionRequests();
157
- });
158
-
159
- this.socketConnection.socket.on('serverFullyReactive', async () => {
160
- // lets take care of retagging
161
- const oldTagStore = this.tagStore;
162
- this.tagStoreSubscription?.unsubscribe();
163
- for (const keyArg of Object.keys(this.tagStore)) {
164
- this.socketConnection.addTag(this.tagStore[keyArg]);
165
- }
166
- this.tagStoreSubscription = this.socketConnection.tagStoreObservable.subscribe(
167
- (tagStoreArg) => {
168
- this.tagStore = tagStoreArg;
169
- }
170
- );
170
+ });
171
+
172
+ // Handle messages
173
+ socket.addEventListener('message', async (event: MessageEvent | { data: string }) => {
174
+ try {
175
+ const data = typeof event.data === 'string' ? event.data : event.data.toString();
176
+ const message: interfaces.ISocketMessage = JSON.parse(data);
177
+
178
+ switch (message.type) {
179
+ case 'authRequest':
180
+ timer.reset();
181
+ const authRequestPayload = message.payload as interfaces.IAuthRequestPayload;
182
+ logger.log('info', `server ${authRequestPayload.serverAlias} requested authentication`);
183
+ this.remoteShortId = authRequestPayload.serverAlias;
171
184
 
172
- for (const tag of Object.keys(oldTagStore)) {
173
- await this.addTag(oldTagStore[tag]);
185
+ // Send authentication data
186
+ this.socketConnection.sendMessage({
187
+ type: 'auth',
188
+ payload: { alias: this.alias },
189
+ });
190
+ break;
191
+
192
+ case 'authResponse':
193
+ const authResponse = message.payload as interfaces.IAuthResponsePayload;
194
+ if (authResponse.success) {
195
+ logger.log('info', 'client is authenticated');
196
+ this.socketConnection.authenticated = true;
197
+ } else {
198
+ logger.log('warn', `authentication failed: ${authResponse.error}`);
199
+ await this.disconnect();
200
+ }
201
+ break;
202
+
203
+ case 'serverReady':
204
+ // Set up function request listening
205
+ await this.socketConnection.listenToFunctionRequests();
206
+
207
+ // Handle retagging
208
+ const oldTagStore = this.tagStore;
209
+ this.tagStoreSubscription?.unsubscribe();
210
+ for (const keyArg of Object.keys(this.tagStore)) {
211
+ this.socketConnection.addTag(this.tagStore[keyArg]);
212
+ }
213
+ this.tagStoreSubscription = this.socketConnection.tagStoreObservable.subscribe(
214
+ (tagStoreArg) => {
215
+ this.tagStore = tagStoreArg;
216
+ }
217
+ );
218
+
219
+ for (const tag of Object.keys(oldTagStore)) {
220
+ await this.addTag(oldTagStore[tag]);
221
+ }
222
+ this.updateStatus('connected');
223
+ this.isConnecting = false;
224
+ done.resolve();
225
+ break;
226
+
227
+ default:
228
+ // Other messages are handled by SocketConnection
229
+ this.socketConnection.handleMessage(message);
230
+ break;
174
231
  }
175
- this.updateStatus('connected');
176
- done.resolve();
177
- });
178
-
179
- // lets register the forbidden event
180
- this.socketConnection.socket.on('forbidden', async () => {
181
- logger.log('warn', `disconnecting due to being forbidden to use the ressource`);
182
- await this.disconnect();
183
- });
184
-
185
- // lets provide the actual auth data
186
- this.socketConnection.socket.emit('dataAuth', {
187
- alias: this.alias,
188
- });
232
+ } catch (err) {
233
+ // Not a valid JSON message, ignore
234
+ }
189
235
  });
190
236
 
191
- // handle connection
192
- this.socketConnection.socket.on('connect', async () => {});
237
+ // Handle disconnection and errors
238
+ const closeHandler = async () => {
239
+ // Only handle close if this is still the current socket and we're not already disconnecting
240
+ if (this.currentSocket === socket && !this.disconnectRunning) {
241
+ logger.log(
242
+ 'info',
243
+ `SocketConnection with >alias ${this.alias} on >side client disconnected`
244
+ );
245
+ await this.disconnect(true);
246
+ }
247
+ };
193
248
 
194
- // handle disconnection and errors
195
- this.socketConnection.socket.on('disconnect', async () => {
196
- await this.disconnect(true);
197
- });
249
+ const errorHandler = async () => {
250
+ if (this.currentSocket === socket && !this.disconnectRunning) {
251
+ await this.disconnect(true);
252
+ }
253
+ };
254
+
255
+ socket.addEventListener('close', closeHandler);
256
+ socket.addEventListener('error', errorHandler);
198
257
 
199
- this.socketConnection.socket.on('reconnect_failed', async () => {
200
- await this.disconnect(true);
201
- });
202
- this.socketConnection.socket.on('connect_error', async () => {
203
- await this.disconnect(true);
204
- });
205
258
  return done.promise;
206
259
  }
207
260
 
208
261
  private disconnectRunning = false;
262
+ private currentSocket: WebSocket | null = null;
263
+ private connectionAttemptId = 0; // Increment on each connect attempt to invalidate old timers
209
264
 
210
265
  /**
211
266
  * disconnect from the server
@@ -215,13 +270,19 @@ export class SmartsocketClient {
215
270
  return;
216
271
  }
217
272
  this.disconnectRunning = true;
273
+ this.isConnecting = false;
218
274
  this.updateStatus('disconnecting');
219
275
  this.tagStoreSubscription?.unsubscribe();
276
+
277
+ // Store reference to current socket before cleanup
278
+ const socketToClose = this.currentSocket;
279
+ this.currentSocket = null;
280
+
220
281
  if (this.socketConnection) {
221
282
  await this.socketConnection.disconnect();
222
283
  this.socketConnection = undefined;
223
284
  logger.log('ok', 'disconnected socket!');
224
- } else {
285
+ } else if (!socketToClose) {
225
286
  this.disconnectRunning = false;
226
287
  logger.log('warn', 'tried to disconnect, without a SocketConnection');
227
288
  return;
@@ -232,28 +293,29 @@ export class SmartsocketClient {
232
293
 
233
294
  if (this.autoReconnect && useAutoReconnectSetting && this.eventStatus !== 'connecting') {
234
295
  this.updateStatus('connecting');
235
-
296
+
236
297
  // Check if we've exceeded the maximum number of retries
237
298
  if (this.currentRetryCount >= this.maxRetries) {
238
299
  logger.log('warn', `Maximum reconnection attempts (${this.maxRetries}) reached. Giving up.`);
239
300
  this.disconnectRunning = false;
240
301
  return;
241
302
  }
242
-
303
+
243
304
  // Increment retry counter
244
305
  this.currentRetryCount++;
245
-
306
+
246
307
  // Calculate backoff with jitter (±20% randomness)
247
308
  const jitter = this.currentBackoffDelay * 0.2 * (Math.random() * 2 - 1);
248
309
  const delay = Math.min(this.currentBackoffDelay + jitter, this.maxBackoffDelay);
249
-
310
+
250
311
  logger.log('info', `Reconnect attempt ${this.currentRetryCount}/${this.maxRetries} in ${Math.round(delay)}ms`);
251
-
312
+
252
313
  // Apply exponential backoff for next time (doubling with each attempt)
253
314
  this.currentBackoffDelay = Math.min(this.currentBackoffDelay * 2, this.maxBackoffDelay);
254
-
315
+
255
316
  await plugins.smartdelay.delayFor(delay);
256
317
  this.disconnectRunning = false;
318
+ this.isReconnecting = true;
257
319
  await this.connect();
258
320
  } else {
259
321
  this.disconnectRunning = false;
@@ -279,7 +341,6 @@ export class SmartsocketClient {
279
341
  functionNameArg: T['method'],
280
342
  dataArg: T['request']
281
343
  ): Promise<T['response']> {
282
- const done = plugins.smartpromise.defer();
283
344
  const socketRequest = new SocketRequest<T>(this, {
284
345
  side: 'requesting',
285
346
  originSocketConnection: this.socketConnection,
@@ -299,14 +360,14 @@ export class SmartsocketClient {
299
360
  this.eventSubject.next(statusArg);
300
361
  }
301
362
  this.eventStatus = statusArg;
302
-
363
+
303
364
  // Reset reconnection state when connection is successful
304
365
  if (statusArg === 'connected') {
305
366
  this.currentRetryCount = 0;
306
367
  this.currentBackoffDelay = this.initialBackoffDelay;
307
368
  }
308
369
  }
309
-
370
+
310
371
  /**
311
372
  * Resets the reconnection state
312
373
  */
@@ -26,7 +26,7 @@ export interface ISocketConnectionConstructorOptions {
26
26
  authenticated: boolean;
27
27
  side: TSocketConnectionSide;
28
28
  smartsocketHost: Smartsocket | SmartsocketClient;
29
- socket: pluginsTyped.socketIo.Socket | pluginsTyped.socketIoClient.Socket;
29
+ socket: pluginsTyped.TWebSocket | pluginsTyped.IWebSocketLike;
30
30
  }
31
31
 
32
32
  /**
@@ -47,7 +47,7 @@ export class SocketConnection {
47
47
  public side: TSocketConnectionSide;
48
48
  public authenticated: boolean = false;
49
49
  public smartsocketRef: Smartsocket | SmartsocketClient;
50
- public socket: pluginsTyped.socketIo.Socket | pluginsTyped.socketIoClient.Socket;
50
+ public socket: pluginsTyped.TWebSocket | pluginsTyped.IWebSocketLike;
51
51
 
52
52
  public eventSubject = new plugins.smartrx.rxjs.Subject<interfaces.TConnectionStatus>();
53
53
  public eventStatus: interfaces.TConnectionStatus = 'new';
@@ -65,20 +65,94 @@ export class SocketConnection {
65
65
 
66
66
  // standard behaviour that is always true
67
67
  allSocketConnections.add(this);
68
+ }
68
69
 
69
- // handle connection
70
- this.socket.on('connect', async () => {
71
- this.updateStatus('connected');
72
- });
73
- this.socket.on('disconnect', async () => {
74
- logger.log(
75
- 'info',
76
- `SocketConnection with >alias ${this.alias} on >side ${this.side} disconnected`
77
- );
78
- await this.disconnect();
79
- allSocketConnections.remove(this);
80
- this.eventSubject.next('disconnected');
81
- });
70
+ /**
71
+ * Sends a message through the socket
72
+ */
73
+ public sendMessage(message: interfaces.ISocketMessage): void {
74
+ if (this.socket.readyState === 1) { // WebSocket.OPEN
75
+ this.socket.send(JSON.stringify(message));
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Handles incoming messages
81
+ */
82
+ public handleMessage(messageData: interfaces.ISocketMessage): void {
83
+ switch (messageData.type) {
84
+ case 'function':
85
+ this.handleFunctionCall(messageData as interfaces.ISocketMessage<interfaces.IFunctionCallPayload>);
86
+ break;
87
+ case 'functionResponse':
88
+ this.handleFunctionResponse(messageData as interfaces.ISocketMessage<interfaces.IFunctionCallPayload>);
89
+ break;
90
+ case 'tagUpdate':
91
+ this.handleTagUpdate(messageData as interfaces.ISocketMessage<interfaces.ITagUpdatePayload>);
92
+ break;
93
+ default:
94
+ // Authentication messages are handled by the server/client classes
95
+ break;
96
+ }
97
+ }
98
+
99
+ private handleFunctionCall(messageData: interfaces.ISocketMessage<interfaces.IFunctionCallPayload>): void {
100
+ const requestData: ISocketRequestDataObject<any> = {
101
+ funcCallData: {
102
+ funcName: messageData.payload.funcName,
103
+ funcDataArg: messageData.payload.funcData,
104
+ },
105
+ shortId: messageData.id,
106
+ };
107
+
108
+ const referencedFunction: SocketFunction<any> =
109
+ this.smartsocketRef.socketFunctions.findSync((socketFunctionArg) => {
110
+ return socketFunctionArg.name === requestData.funcCallData.funcName;
111
+ });
112
+
113
+ if (referencedFunction) {
114
+ const localSocketRequest = new SocketRequest(this.smartsocketRef, {
115
+ side: 'responding',
116
+ originSocketConnection: this,
117
+ shortId: requestData.shortId,
118
+ funcCallData: requestData.funcCallData,
119
+ });
120
+ localSocketRequest.createResponse();
121
+ } else {
122
+ logger.log('warn', `function ${requestData.funcCallData.funcName} not found or out of scope`);
123
+ }
124
+ }
125
+
126
+ private handleFunctionResponse(messageData: interfaces.ISocketMessage<interfaces.IFunctionCallPayload>): void {
127
+ const responseData: ISocketRequestDataObject<any> = {
128
+ funcCallData: {
129
+ funcName: messageData.payload.funcName,
130
+ funcDataArg: messageData.payload.funcData,
131
+ },
132
+ shortId: messageData.id,
133
+ };
134
+
135
+ const targetSocketRequest = SocketRequest.getSocketRequestById(
136
+ this.smartsocketRef,
137
+ responseData.shortId
138
+ );
139
+ if (targetSocketRequest) {
140
+ targetSocketRequest.handleResponse(responseData);
141
+ }
142
+ }
143
+
144
+ private handleTagUpdate(messageData: interfaces.ISocketMessage<interfaces.ITagUpdatePayload>): void {
145
+ const tagStoreArg = messageData.payload.tags as interfaces.TTagStore;
146
+ if (!plugins.smartjson.deepEqualObjects(this.tagStore, tagStoreArg)) {
147
+ this.tagStore = tagStoreArg;
148
+ // Echo back to confirm
149
+ this.sendMessage({
150
+ type: 'tagUpdate',
151
+ payload: { tags: this.tagStore },
152
+ });
153
+ this.tagStoreObservable.next(this.tagStore);
154
+ }
155
+ this.remoteTagStoreObservable.next(tagStoreArg);
82
156
  }
83
157
 
84
158
  /**
@@ -99,54 +173,88 @@ export class SocketConnection {
99
173
  done.resolve();
100
174
  }
101
175
  });
102
- this.socket.emit('updateTagStore', this.tagStore);
176
+ this.sendMessage({
177
+ type: 'tagUpdate',
178
+ payload: { tags: this.tagStore },
179
+ });
103
180
  await done.promise;
104
181
  }
105
182
 
106
183
  /**
107
- * gets a tag by id
108
- * @param tagIdArg
184
+ * Gets a tag by id
109
185
  */
110
- public async getTagById(tagIdArg: interfaces.ITag['id']) {
186
+ public getTagById(tagIdArg: interfaces.ITag['id']): interfaces.ITag | undefined {
111
187
  return this.tagStore[tagIdArg];
112
188
  }
113
189
 
114
190
  /**
115
- * removes a tag from a connection
191
+ * Removes a tag from a connection
116
192
  */
117
- public async removeTagById(tagIdArg: interfaces.ITag['id']) {
193
+ public removeTagById(tagIdArg: interfaces.ITag['id']): void {
118
194
  delete this.tagStore[tagIdArg];
119
195
  this.tagStoreObservable.next(this.tagStore);
120
- this.socket.emit('updateTagStore', this.tagStore);
196
+ this.sendMessage({
197
+ type: 'tagUpdate',
198
+ payload: { tags: this.tagStore },
199
+ });
121
200
  }
122
201
 
123
202
  // authenticating --------------------------
124
203
 
125
204
  /**
126
- * authenticate the socket
205
+ * authenticate the socket (server side)
127
206
  */
128
- public authenticate() {
129
- const done = plugins.smartpromise.defer();
130
- this.socket.on('dataAuth', async (dataArg: ISocketConnectionAuthenticationObject) => {
131
- logger.log('info', 'received authentication data...');
132
- this.socket.removeAllListeners('dataAuth');
133
- if (dataArg.alias) {
134
- // TODO: authenticate password
135
- this.alias = dataArg.alias;
136
- this.authenticated = true;
137
- this.socket.emit('authenticated');
138
- logger.log('ok', `socket with >>alias ${this.alias} is authenticated!`);
139
- done.resolve(this);
140
- } else {
141
- this.authenticated = false;
142
- await this.disconnect();
143
- done.reject('a socket tried to connect, but could not authenticated.');
207
+ public authenticate(): Promise<SocketConnection> {
208
+ const done = plugins.smartpromise.defer<SocketConnection>();
209
+
210
+ // Set up message handler for authentication
211
+ const messageHandler = (event: pluginsTyped.TMessageEvent) => {
212
+ try {
213
+ const data = typeof event.data === 'string' ? event.data : event.data.toString();
214
+ const message: interfaces.ISocketMessage = JSON.parse(data);
215
+
216
+ if (message.type === 'auth') {
217
+ const authData = message.payload as interfaces.IAuthPayload;
218
+ logger.log('info', 'received authentication data...');
219
+
220
+ if (authData.alias) {
221
+ this.alias = authData.alias;
222
+ this.authenticated = true;
223
+
224
+ // Send authentication response
225
+ this.sendMessage({
226
+ type: 'authResponse',
227
+ payload: { success: true },
228
+ });
229
+
230
+ logger.log('ok', `socket with >>alias ${this.alias} is authenticated!`);
231
+ done.resolve(this);
232
+ } else {
233
+ this.authenticated = false;
234
+ this.sendMessage({
235
+ type: 'authResponse',
236
+ payload: { success: false, error: 'No alias provided' },
237
+ });
238
+ this.disconnect();
239
+ done.reject('a socket tried to connect, but could not authenticate.');
240
+ }
241
+ }
242
+ } catch (err) {
243
+ logger.log('warn', `Failed to parse auth message: ${err instanceof Error ? err.message : String(err)}`);
144
244
  }
145
- });
146
- const requestAuthPayload: interfaces.IRequestAuthPayload = {
147
- serverAlias: this.smartsocketRef.alias,
148
245
  };
149
- this.socket.emit('requestAuth', requestAuthPayload);
246
+
247
+ (this.socket as pluginsTyped.IWebSocketLike).addEventListener('message', messageHandler);
248
+
249
+ // Request authentication
250
+ const requestAuthPayload: interfaces.TAuthRequestMessage = {
251
+ type: 'authRequest',
252
+ payload: {
253
+ serverAlias: (this.smartsocketRef as Smartsocket).alias,
254
+ },
255
+ };
256
+ this.sendMessage(requestAuthPayload);
257
+
150
258
  return done.promise;
151
259
  }
152
260
 
@@ -158,43 +266,18 @@ export class SocketConnection {
158
266
  public listenToFunctionRequests() {
159
267
  const done = plugins.smartpromise.defer();
160
268
  if (this.authenticated) {
161
- this.socket.on('function', (dataArg: ISocketRequestDataObject<any>) => {
162
- // check if requested function is available to the socket's scope
163
- // logger.log('info', 'function request received');
164
- const referencedFunction: SocketFunction<any> =
165
- this.smartsocketRef.socketFunctions.findSync((socketFunctionArg) => {
166
- return socketFunctionArg.name === dataArg.funcCallData.funcName;
167
- });
168
- if (referencedFunction) {
169
- // logger.log('ok', 'function in access scope');
170
- const localSocketRequest = new SocketRequest(this.smartsocketRef, {
171
- side: 'responding',
172
- originSocketConnection: this,
173
- shortId: dataArg.shortId,
174
- funcCallData: dataArg.funcCallData,
175
- });
176
- localSocketRequest.createResponse(); // takes care of creating response and sending it back
177
- } else {
178
- logger.log('warn', 'function not existent or out of access scope');
269
+ // Set up message handler for all messages
270
+ const messageHandler = (event: pluginsTyped.TMessageEvent) => {
271
+ try {
272
+ const data = typeof event.data === 'string' ? event.data : event.data.toString();
273
+ const message: interfaces.ISocketMessage = JSON.parse(data);
274
+ this.handleMessage(message);
275
+ } catch (err) {
276
+ logger.log('warn', `Failed to parse socket message: ${err instanceof Error ? err.message : String(err)}`);
179
277
  }
180
- });
181
- this.socket.on('functionResponse', (dataArg: ISocketRequestDataObject<any>) => {
182
- // logger.log('info', `received response for request with id ${dataArg.shortId}`);
183
- const targetSocketRequest = SocketRequest.getSocketRequestById(
184
- this.smartsocketRef,
185
- dataArg.shortId
186
- );
187
- targetSocketRequest.handleResponse(dataArg);
188
- });
278
+ };
189
279
 
190
- this.socket.on('updateTagStore', async (tagStoreArg: interfaces.TTagStore) => {
191
- if (!plugins.smartjson.deepEqualObjects(this.tagStore, tagStoreArg)) {
192
- this.tagStore = tagStoreArg;
193
- this.socket.emit('updateTagStore', this.tagStore);
194
- this.tagStoreObservable.next(this.tagStore);
195
- }
196
- this.remoteTagStoreObservable.next(tagStoreArg);
197
- });
280
+ (this.socket as pluginsTyped.IWebSocketLike).addEventListener('message', messageHandler);
198
281
 
199
282
  logger.log(
200
283
  'info',
@@ -211,7 +294,10 @@ export class SocketConnection {
211
294
 
212
295
  // disconnecting ----------------------
213
296
  public async disconnect() {
214
- this.socket.disconnect(true);
297
+ if (this.socket.readyState === 1 || this.socket.readyState === 0) {
298
+ this.socket.close();
299
+ }
300
+ allSocketConnections.remove(this);
215
301
  this.updateStatus('disconnected');
216
302
  }
217
303