@api.global/typedsocket 3.1.1 → 4.1.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,13 +1,37 @@
1
1
  import * as plugins from './typedsocket.plugins.js';
2
2
 
3
- const publicRoleName = 'publicRoleName';
4
- const publicRolePass = 'publicRolePass';
5
-
6
3
  export type TTypedSocketSide = 'server' | 'client';
4
+ export type TConnectionStatus = 'new' | 'connecting' | 'connected' | 'disconnected' | 'reconnecting';
5
+
6
+ const TAG_PREFIX = '__typedsocket_tag__';
7
+
8
+ /**
9
+ * Internal TypedRequest interfaces for tag management
10
+ */
11
+ interface IReq_SetClientTag extends plugins.typedrequestInterfaces.ITypedRequest {
12
+ method: '__typedsocket_setTag';
13
+ request: { name: string; payload: any };
14
+ response: { success: boolean };
15
+ }
16
+
17
+ interface IReq_RemoveClientTag extends plugins.typedrequestInterfaces.ITypedRequest {
18
+ method: '__typedsocket_removeTag';
19
+ request: { name: string };
20
+ response: { success: boolean };
21
+ }
22
+
23
+ /**
24
+ * Options for creating a TypedSocket client
25
+ */
26
+ export interface ITypedSocketClientOptions {
27
+ autoReconnect?: boolean;
28
+ maxRetries?: number;
29
+ initialBackoffMs?: number;
30
+ maxBackoffMs?: number;
31
+ }
7
32
 
8
33
  /**
9
34
  * Wrapper for SmartServe's IWebSocketPeer to provide tag compatibility
10
- * SmartServe uses Set<string> for tags, while TypedSocket uses {id, payload} format
11
35
  */
12
36
  export interface ISmartServeConnectionWrapper {
13
37
  peer: plugins.IWebSocketPeer;
@@ -18,7 +42,6 @@ export interface ISmartServeConnectionWrapper {
18
42
  * Creates a wrapper around IWebSocketPeer for tag compatibility
19
43
  */
20
44
  function wrapSmartServePeer(peer: plugins.IWebSocketPeer): ISmartServeConnectionWrapper {
21
- const TAG_PREFIX = '__typedsocket_tag__';
22
45
  return {
23
46
  peer,
24
47
  async getTagById(tagId: string): Promise<{ id: string; payload: any } | undefined> {
@@ -32,113 +55,62 @@ function wrapSmartServePeer(peer: plugins.IWebSocketPeer): ISmartServeConnection
32
55
  }
33
56
 
34
57
  export class TypedSocket {
35
- // STATIC
58
+ // ============================================================================
59
+ // STATIC METHODS
60
+ // ============================================================================
61
+
36
62
  /**
37
- * creates a typedsocket server
38
- * note: this will fail in browser environments as server libs are not bundled.
63
+ * Creates a TypedSocket client using native WebSocket.
64
+ * Works in both browser and Node.js environments.
65
+ *
66
+ * @param typedrouterArg - TypedRouter for handling server-initiated requests
67
+ * @param serverUrlArg - Server URL (e.g., 'http://localhost:3000' or 'wss://example.com')
68
+ * @param options - Connection options
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * const typedRouter = new TypedRouter();
73
+ * const client = await TypedSocket.createClient(
74
+ * typedRouter,
75
+ * 'http://localhost:3000',
76
+ * { autoReconnect: true }
77
+ * );
78
+ * ```
39
79
  */
40
- public static async createServer(
41
- typedrouterArg: plugins.typedrequest.TypedRouter,
42
- smartexpressServerArg?: any
43
- ): Promise<TypedSocket> {
44
- const smartsocketServer = new plugins.smartsocket.Smartsocket({
45
- alias: 'typedsocketServer',
46
- port: 3000,
47
- });
48
- if (smartexpressServerArg) {
49
- smartsocketServer.setExternalServer('smartexpress', smartexpressServerArg);
50
- }
51
-
52
- smartsocketServer.socketFunctions.add(
53
- new plugins.smartsocket.SocketFunction({
54
- funcName: 'processMessage',
55
- funcDef: async (dataArg, socketConnectionArg) => {
56
- return typedrouterArg.routeAndAddResponse(dataArg);
57
- },
58
- })
59
- );
60
- const typedsocket = new TypedSocket(
61
- 'server',
62
- typedrouterArg,
63
- async <T extends plugins.typedrequestInterfaces.ITypedRequest>(
64
- dataArg: T,
65
- targetConnectionArg?: plugins.smartsocket.SocketConnection
66
- ): Promise<T> => {
67
- if (!targetConnectionArg) {
68
- if ((smartsocketServer.socketConnections.getArray().length = 1)) {
69
- console.log(
70
- 'Since no targetConnection was supplied and there is only one active one present, choosing that one automatically'
71
- );
72
- targetConnectionArg = smartsocketServer.socketConnections.getArray()[0];
73
- } else {
74
- throw new Error(
75
- 'you need to specify the wanted targetConnection. Currently no target is selectable automatically.'
76
- );
77
- }
78
- }
79
- const response: T = (await smartsocketServer.clientCall(
80
- 'processMessage',
81
- dataArg,
82
- targetConnectionArg
83
- )) as any;
84
- return response;
85
- },
86
- smartsocketServer
87
- );
88
- await smartsocketServer.start();
89
-
90
- return typedsocket;
91
- }
92
-
93
80
  public static async createClient(
94
81
  typedrouterArg: plugins.typedrequest.TypedRouter,
95
82
  serverUrlArg: string,
96
- aliasArg = 'clientArg'
83
+ options: ITypedSocketClientOptions = {}
97
84
  ): Promise<TypedSocket> {
98
- const domain = new plugins.smartstring.Domain(serverUrlArg);
99
-
100
- const socketOptions: plugins.smartsocket.ISmartsocketClientOptions = {
101
- alias: aliasArg,
102
- port: domain.port || 3000,
103
- url: `${domain.nodeParsedUrl.protocol}//${domain.nodeParsedUrl.hostname}`,
85
+ const defaultOptions: Required<ITypedSocketClientOptions> = {
104
86
  autoReconnect: true,
87
+ maxRetries: 100,
88
+ initialBackoffMs: 1000,
89
+ maxBackoffMs: 60000,
105
90
  };
106
- console.log(`starting typedsocket with the following settings:`);
107
- console.log(socketOptions);
108
- const smartsocketClient = new plugins.smartsocket.SmartsocketClient(socketOptions);
109
- smartsocketClient.addSocketFunction(
110
- new plugins.smartsocket.SocketFunction({
111
- funcName: 'processMessage',
112
- funcDef: async (dataArg, socketConnectionArg) => {
113
- return typedrouterArg.routeAndAddResponse(dataArg);
114
- },
115
- })
116
- );
117
- const typedsocket = new TypedSocket(
118
- 'client',
119
- typedrouterArg,
120
- async <T extends plugins.typedrequestInterfaces.ITypedRequest>(dataArg: T): Promise<T> => {
121
- const response: T = smartsocketClient.serverCall('processMessage', dataArg) as any as T;
122
- return response;
123
- },
124
- smartsocketClient
125
- );
126
- console.log(`typedsocket triggering smartsocket to connect...`);
127
- const before = Date.now();
128
- await smartsocketClient.connect();
129
- console.log(`typedsocket triggered smartsocket connected in ${Date.now() - before}ms!!!`)
91
+ const opts = { ...defaultOptions, ...options };
130
92
 
131
- return typedsocket;
132
- }
93
+ const typedSocket = new TypedSocket('client', typedrouterArg);
94
+ typedSocket.clientOptions = opts;
95
+ typedSocket.serverUrl = serverUrlArg;
96
+ typedSocket.currentBackoff = opts.initialBackoffMs;
133
97
 
134
- public static useWindowLocationOriginUrl = () => {
135
- const windowLocationResult = plugins.smarturl.Smarturl.createFromUrl(globalThis.location.origin).toString();
136
- return windowLocationResult;
98
+ await typedSocket.connect();
99
+
100
+ return typedSocket;
137
101
  }
138
102
 
103
+ /**
104
+ * Returns the current window location origin URL.
105
+ * Useful in browser environments for connecting to the same origin.
106
+ */
107
+ public static useWindowLocationOriginUrl = (): string => {
108
+ return plugins.smarturl.Smarturl.createFromUrl(globalThis.location.origin).toString();
109
+ };
110
+
139
111
  /**
140
112
  * Creates a TypedSocket server from an existing SmartServe instance.
141
- * Use this when you want TypedSocket to work with SmartServe's WebSocket handling.
113
+ * This is the only way to create a server-side TypedSocket.
142
114
  *
143
115
  * @param smartServeArg - SmartServe instance with typedRouter configured in websocket options
144
116
  * @param typedRouterArg - TypedRouter for handling requests (must match SmartServe's typedRouter)
@@ -163,249 +135,479 @@ export class TypedSocket {
163
135
  ): TypedSocket {
164
136
  const connectionWrappers = new Map<string, ISmartServeConnectionWrapper>();
165
137
 
166
- // Create the postMethod for server-initiated requests
167
- const postMethod = async <T extends plugins.typedrequestInterfaces.ITypedRequest>(
168
- dataArg: T,
169
- targetConnectionArg?: ISmartServeConnectionWrapper
170
- ): Promise<T> => {
171
- if (!targetConnectionArg) {
172
- const allConnections = smartServeArg.getWebSocketConnections();
138
+ // Register built-in tag handlers
139
+ TypedSocket.registerTagHandlers(typedRouterArg);
140
+
141
+ const typedSocket = new TypedSocket('server', typedRouterArg);
142
+ typedSocket.smartServeRef = smartServeArg;
143
+ typedSocket.smartServeConnectionWrappers = connectionWrappers;
144
+
145
+ return typedSocket;
146
+ }
147
+
148
+ /**
149
+ * Registers built-in TypedHandlers for tag management
150
+ */
151
+ private static registerTagHandlers(typedRouter: plugins.typedrequest.TypedRouter): void {
152
+ // Set tag handler
153
+ typedRouter.addTypedHandler<IReq_SetClientTag>(
154
+ new plugins.typedrequest.TypedHandler('__typedsocket_setTag', async (data, meta) => {
155
+ const peer = meta?.localData?.peer as plugins.IWebSocketPeer;
156
+ if (!peer) {
157
+ console.warn('setTag: No peer found in request context');
158
+ return { success: false };
159
+ }
160
+
161
+ peer.tags.add(data.name);
162
+ peer.data.set(`${TAG_PREFIX}${data.name}`, data.payload);
163
+
164
+ return { success: true };
165
+ })
166
+ );
167
+
168
+ // Remove tag handler
169
+ typedRouter.addTypedHandler<IReq_RemoveClientTag>(
170
+ new plugins.typedrequest.TypedHandler('__typedsocket_removeTag', async (data, meta) => {
171
+ const peer = meta?.localData?.peer as plugins.IWebSocketPeer;
172
+ if (!peer) {
173
+ console.warn('removeTag: No peer found in request context');
174
+ return { success: false };
175
+ }
176
+
177
+ peer.tags.delete(data.name);
178
+ peer.data.delete(`${TAG_PREFIX}${data.name}`);
179
+
180
+ return { success: true };
181
+ })
182
+ );
183
+ }
184
+
185
+ // ============================================================================
186
+ // INSTANCE PROPERTIES
187
+ // ============================================================================
188
+
189
+ public readonly side: TTypedSocketSide;
190
+ public readonly typedrouter: plugins.typedrequest.TypedRouter;
191
+
192
+ // Connection status observable
193
+ public statusSubject = new plugins.smartrx.rxjs.Subject<TConnectionStatus>();
194
+ private connectionStatus: TConnectionStatus = 'new';
195
+
196
+ // Client-specific properties
197
+ private websocket: WebSocket | null = null;
198
+ private clientOptions: Required<ITypedSocketClientOptions> | null = null;
199
+ private serverUrl: string = '';
200
+ private retryCount = 0;
201
+ private currentBackoff = 1000;
202
+ private pendingRequests = new Map<string, {
203
+ resolve: (response: any) => void;
204
+ reject: (error: Error) => void;
205
+ }>();
206
+
207
+ // Server-specific properties (SmartServe mode)
208
+ private smartServeRef: plugins.SmartServe | null = null;
209
+ private smartServeConnectionWrappers = new Map<string, ISmartServeConnectionWrapper>();
210
+
211
+ // ============================================================================
212
+ // CONSTRUCTOR
213
+ // ============================================================================
214
+
215
+ private constructor(
216
+ sideArg: TTypedSocketSide,
217
+ typedrouterArg: plugins.typedrequest.TypedRouter
218
+ ) {
219
+ this.side = sideArg;
220
+ this.typedrouter = typedrouterArg;
221
+ }
222
+
223
+ // ============================================================================
224
+ // CLIENT METHODS
225
+ // ============================================================================
226
+
227
+ /**
228
+ * Connects the client to the server using native WebSocket
229
+ */
230
+ private async connect(): Promise<void> {
231
+ const done = plugins.smartpromise.defer<void>();
232
+
233
+ this.updateStatus('connecting');
234
+
235
+ // Convert HTTP URL to WebSocket URL
236
+ const wsUrl = this.toWebSocketUrl(this.serverUrl);
237
+ console.log(`TypedSocket connecting to ${wsUrl}...`);
238
+
239
+ this.websocket = new WebSocket(wsUrl);
240
+
241
+ const connectionTimeout = setTimeout(() => {
242
+ if (this.connectionStatus !== 'connected') {
243
+ console.warn('TypedSocket connection timeout');
244
+ this.websocket?.close();
245
+ done.reject(new Error('Connection timeout'));
246
+ }
247
+ }, 10000);
248
+
249
+ this.websocket.onopen = () => {
250
+ clearTimeout(connectionTimeout);
251
+ console.log('TypedSocket connected!');
252
+ this.updateStatus('connected');
253
+ this.retryCount = 0;
254
+ this.currentBackoff = this.clientOptions?.initialBackoffMs ?? 1000;
255
+ done.resolve();
256
+ };
257
+
258
+ this.websocket.onmessage = async (event) => {
259
+ await this.handleMessage(event.data);
260
+ };
261
+
262
+ this.websocket.onclose = () => {
263
+ clearTimeout(connectionTimeout);
264
+ this.handleDisconnect();
265
+ };
266
+
267
+ this.websocket.onerror = (error) => {
268
+ console.error('TypedSocket WebSocket error:', error);
269
+ };
270
+
271
+ try {
272
+ await done.promise;
273
+ } catch (err) {
274
+ clearTimeout(connectionTimeout);
275
+ if (this.clientOptions?.autoReconnect) {
276
+ await this.scheduleReconnect();
277
+ } else {
278
+ throw err;
279
+ }
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Converts an HTTP(S) URL to a WebSocket URL
285
+ */
286
+ private toWebSocketUrl(url: string): string {
287
+ const parsed = new URL(url);
288
+ const wsProtocol = parsed.protocol === 'https:' ? 'wss:' : 'ws:';
289
+ return `${wsProtocol}//${parsed.host}${parsed.pathname}`;
290
+ }
291
+
292
+ /**
293
+ * Handles incoming WebSocket messages
294
+ */
295
+ private async handleMessage(data: string | ArrayBuffer): Promise<void> {
296
+ try {
297
+ const messageText = typeof data === 'string' ? data : new TextDecoder().decode(data);
298
+ const message = plugins.smartjson.parse(messageText) as plugins.typedrequestInterfaces.ITypedRequest;
299
+
300
+ // Check if this is a response to a pending request
301
+ if (message.correlation?.id && this.pendingRequests.has(message.correlation.id)) {
302
+ const pending = this.pendingRequests.get(message.correlation.id)!;
303
+ this.pendingRequests.delete(message.correlation.id);
304
+ pending.resolve(message);
305
+ return;
306
+ }
307
+
308
+ // Server-initiated request - route through TypedRouter
309
+ const response = await this.typedrouter.routeAndAddResponse(message);
310
+ if (response && this.websocket?.readyState === WebSocket.OPEN) {
311
+ this.websocket.send(plugins.smartjson.stringify(response));
312
+ }
313
+ } catch (err) {
314
+ console.error('TypedSocket failed to process message:', err);
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Handles WebSocket disconnection
320
+ */
321
+ private handleDisconnect(): void {
322
+ if (this.connectionStatus === 'disconnected') {
323
+ return; // Already handled
324
+ }
325
+
326
+ this.updateStatus('disconnected');
327
+
328
+ if (this.clientOptions?.autoReconnect && this.retryCount < this.clientOptions.maxRetries) {
329
+ this.scheduleReconnect();
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Schedules a reconnection attempt with exponential backoff
335
+ */
336
+ private async scheduleReconnect(): Promise<void> {
337
+ if (!this.clientOptions) return;
338
+
339
+ this.updateStatus('reconnecting');
340
+ this.retryCount++;
341
+
342
+ // Exponential backoff with jitter
343
+ const jitter = this.currentBackoff * 0.2 * (Math.random() * 2 - 1);
344
+ const delay = Math.min(this.currentBackoff + jitter, this.clientOptions.maxBackoffMs);
345
+
346
+ console.log(`TypedSocket reconnecting in ${Math.round(delay)}ms (attempt ${this.retryCount}/${this.clientOptions.maxRetries})`);
347
+
348
+ await plugins.smartdelay.delayFor(delay);
349
+
350
+ // Increase backoff for next time
351
+ this.currentBackoff = Math.min(this.currentBackoff * 2, this.clientOptions.maxBackoffMs);
352
+
353
+ try {
354
+ await this.connect();
355
+ } catch (err) {
356
+ console.error('TypedSocket reconnection failed:', err);
357
+ }
358
+ }
359
+
360
+ /**
361
+ * Updates connection status and notifies subscribers
362
+ */
363
+ private updateStatus(status: TConnectionStatus): void {
364
+ if (this.connectionStatus !== status) {
365
+ this.connectionStatus = status;
366
+ this.statusSubject.next(status);
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Sends a request to the server and waits for response (client-side)
372
+ */
373
+ private async sendRequest<T extends plugins.typedrequestInterfaces.ITypedRequest>(
374
+ request: T
375
+ ): Promise<T> {
376
+ if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN) {
377
+ throw new Error('WebSocket not connected');
378
+ }
379
+
380
+ return new Promise((resolve, reject) => {
381
+ const timeout = setTimeout(() => {
382
+ this.pendingRequests.delete(request.correlation.id);
383
+ reject(new Error('Request timeout'));
384
+ }, 30000);
385
+
386
+ this.pendingRequests.set(request.correlation.id, {
387
+ resolve: (response) => {
388
+ clearTimeout(timeout);
389
+ resolve(response);
390
+ },
391
+ reject: (error) => {
392
+ clearTimeout(timeout);
393
+ reject(error);
394
+ },
395
+ });
396
+
397
+ this.websocket!.send(plugins.smartjson.stringify(request));
398
+ });
399
+ }
400
+
401
+ // ============================================================================
402
+ // PUBLIC API - SHARED
403
+ // ============================================================================
404
+
405
+ /**
406
+ * Creates a TypedRequest for the specified method.
407
+ * On clients, sends to the server.
408
+ * On servers, sends to the specified target connection.
409
+ */
410
+ public createTypedRequest<T extends plugins.typedrequestInterfaces.ITypedRequest>(
411
+ methodName: T['method'],
412
+ targetConnection?: ISmartServeConnectionWrapper
413
+ ): plugins.typedrequest.TypedRequest<T> {
414
+ const postMethod = async (requestDataArg: T): Promise<T> => {
415
+ if (this.side === 'client') {
416
+ return this.sendRequest(requestDataArg);
417
+ }
418
+
419
+ // Server-side: send to target connection
420
+ if (!this.smartServeRef) {
421
+ throw new Error('Server not initialized');
422
+ }
423
+
424
+ let target = targetConnection;
425
+ if (!target) {
426
+ const allConnections = this.smartServeRef.getWebSocketConnections();
173
427
  if (allConnections.length === 1) {
174
- console.log(
175
- 'Since no targetConnection was supplied and there is only one active one present, choosing that one automatically'
176
- );
177
428
  const peer = allConnections[0];
178
- let wrapper = connectionWrappers.get(peer.id);
179
- if (!wrapper) {
180
- wrapper = wrapSmartServePeer(peer);
181
- connectionWrappers.set(peer.id, wrapper);
182
- }
183
- targetConnectionArg = wrapper;
429
+ target = this.getOrCreateWrapper(peer);
184
430
  } else if (allConnections.length === 0) {
185
431
  throw new Error('No WebSocket connections available');
186
432
  } else {
187
- throw new Error(
188
- 'you need to specify the wanted targetConnection. Currently no target is selectable automatically.'
189
- );
433
+ throw new Error('Multiple connections available - specify targetConnection');
190
434
  }
191
435
  }
192
436
 
193
- // Register interest for the response using correlation ID
194
- const interest = await typedRouterArg.fireEventInterestMap.addInterest(
195
- dataArg.correlation.id,
196
- dataArg
437
+ // Register interest for response
438
+ const interest = await this.typedrouter.fireEventInterestMap.addInterest(
439
+ requestDataArg.correlation.id,
440
+ requestDataArg
197
441
  );
198
442
 
199
- // Send the request to the client
200
- targetConnectionArg.peer.send(plugins.smartjson.stringify(dataArg));
443
+ // Send request
444
+ target.peer.send(plugins.smartjson.stringify(requestDataArg));
201
445
 
202
- // Wait for the response (TypedRouter will fulfill via routeAndAddResponse when response arrives)
203
- const response = await interest.interestFullfilled as T;
204
- return response;
446
+ // Wait for response
447
+ return await interest.interestFullfilled as T;
205
448
  };
206
449
 
207
- const typedSocket = new TypedSocket(
208
- 'server',
209
- typedRouterArg,
210
- postMethod as any,
211
- null as any // No smartsocket server/client when using SmartServe
450
+ return new plugins.typedrequest.TypedRequest<T>(
451
+ new plugins.typedrequest.TypedTarget({ postMethod }),
452
+ methodName
212
453
  );
454
+ }
213
455
 
214
- typedSocket.smartServeRef = smartServeArg;
215
- typedSocket.smartServeConnectionWrappers = connectionWrappers;
456
+ /**
457
+ * Gets the current connection status
458
+ */
459
+ public getStatus(): TConnectionStatus {
460
+ return this.connectionStatus;
461
+ }
216
462
 
217
- return typedSocket;
463
+ /**
464
+ * Stops the TypedSocket client or cleans up server state
465
+ */
466
+ public async stop(): Promise<void> {
467
+ if (this.side === 'client') {
468
+ if (this.clientOptions) {
469
+ this.clientOptions.autoReconnect = false;
470
+ }
471
+ if (this.websocket) {
472
+ this.websocket.close();
473
+ this.websocket = null;
474
+ }
475
+ this.pendingRequests.clear();
476
+ } else {
477
+ // Server mode - just clear wrappers (SmartServe manages its own lifecycle)
478
+ this.smartServeConnectionWrappers.clear();
479
+ }
218
480
  }
219
481
 
220
- // INSTANCE
221
- public side: TTypedSocketSide;
222
- public typedrouter: plugins.typedrequest.TypedRouter;
482
+ // ============================================================================
483
+ // CLIENT-ONLY METHODS
484
+ // ============================================================================
485
+
486
+ /**
487
+ * Sets a tag on this client connection.
488
+ * Tags are stored on the server and can be used for filtering.
489
+ * @client-only
490
+ */
491
+ public async setTag<T extends plugins.typedrequestInterfaces.ITag>(
492
+ name: T['name'],
493
+ payload: T['payload']
494
+ ): Promise<void> {
495
+ if (this.side !== 'client') {
496
+ throw new Error('setTag is only available on clients');
497
+ }
223
498
 
224
- // SmartServe mode properties
225
- private smartServeRef?: plugins.SmartServe;
226
- private smartServeConnectionWrappers: Map<string, ISmartServeConnectionWrapper> = new Map();
499
+ const request = this.createTypedRequest<IReq_SetClientTag>('__typedsocket_setTag');
500
+ const response = await request.fire({ name, payload });
227
501
 
228
- public get eventSubject(): plugins.smartrx.rxjs.Subject<plugins.smartsocket.TConnectionStatus> {
229
- if (this.smartServeRef) {
230
- // SmartServe doesn't provide an eventSubject, return a new Subject
231
- // In SmartServe mode, connection events are handled via onConnectionOpen/onConnectionClose hooks
232
- console.warn('eventSubject is not fully supported in SmartServe mode. Use SmartServe hooks instead.');
233
- return new plugins.smartrx.rxjs.Subject();
502
+ if (!response.success) {
503
+ throw new Error('Failed to set tag on server');
234
504
  }
235
- return this.socketServerOrClient.eventSubject;
236
- }
237
- private postMethod: plugins.typedrequest.IPostMethod &
238
- ((
239
- typedRequestPostObject: plugins.typedrequestInterfaces.ITypedRequest,
240
- socketConnectionArg?: plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper
241
- ) => Promise<plugins.typedrequestInterfaces.ITypedRequest>);
242
- private socketServerOrClient:
243
- | plugins.smartsocket.Smartsocket
244
- | plugins.smartsocket.SmartsocketClient;
245
- constructor(
246
- sideArg: TTypedSocketSide,
247
- typedrouterArg: plugins.typedrequest.TypedRouter,
248
- postMethodArg: plugins.typedrequest.IPostMethod,
249
- socketServerOrClientArg: plugins.smartsocket.Smartsocket | plugins.smartsocket.SmartsocketClient
250
- ) {
251
- this.side = sideArg;
252
- this.typedrouter = typedrouterArg;
253
- this.postMethod = postMethodArg;
254
- this.socketServerOrClient = socketServerOrClientArg;
255
505
  }
256
506
 
257
- public addTag<T extends plugins.typedrequestInterfaces.ITag = any>(
258
- nameArg: T['name'],
259
- payloadArg: T['payload']
260
- ) {
261
- if (
262
- this.side === 'client' &&
263
- this.socketServerOrClient instanceof plugins.smartsocket.SmartsocketClient
264
- ) {
265
- this.socketServerOrClient.socketConnection.addTag({
266
- id: nameArg,
267
- payload: payloadArg,
268
- });
269
- } else {
270
- throw new Error('tagging is only supported on clients');
507
+ /**
508
+ * Removes a tag from this client connection.
509
+ * @client-only
510
+ */
511
+ public async removeTag(name: string): Promise<void> {
512
+ if (this.side !== 'client') {
513
+ throw new Error('removeTag is only available on clients');
514
+ }
515
+
516
+ const request = this.createTypedRequest<IReq_RemoveClientTag>('__typedsocket_removeTag');
517
+ const response = await request.fire({ name });
518
+
519
+ if (!response.success) {
520
+ throw new Error('Failed to remove tag on server');
271
521
  }
272
522
  }
273
523
 
274
- public createTypedRequest<T extends plugins.typedrequestInterfaces.ITypedRequest>(
275
- methodName: T['method'],
276
- targetConnection?: plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper
277
- ): plugins.typedrequest.TypedRequest<T> {
278
- const typedrequest = new plugins.typedrequest.TypedRequest<T>(
279
- new plugins.typedrequest.TypedTarget({
280
- postMethod: async (requestDataArg) => {
281
- const result = await this.postMethod(requestDataArg, targetConnection as any);
282
- return result;
283
- },
284
- }),
285
- methodName
286
- );
287
- return typedrequest;
524
+ // ============================================================================
525
+ // SERVER-ONLY METHODS
526
+ // ============================================================================
527
+
528
+ /**
529
+ * Gets or creates a connection wrapper for a peer
530
+ */
531
+ private getOrCreateWrapper(peer: plugins.IWebSocketPeer): ISmartServeConnectionWrapper {
532
+ let wrapper = this.smartServeConnectionWrappers.get(peer.id);
533
+ if (!wrapper) {
534
+ wrapper = wrapSmartServePeer(peer);
535
+ this.smartServeConnectionWrappers.set(peer.id, wrapper);
536
+ }
537
+ return wrapper;
288
538
  }
289
539
 
290
540
  /**
291
- * returns all matching target connections
292
- * Works with both Smartsocket and SmartServe backends
293
- * @param asyncFindFuncArg - async filter function
294
- * @returns array of matching connections
541
+ * Finds all connections matching the filter function.
542
+ * @server-only
295
543
  */
296
544
  public async findAllTargetConnections(
297
- asyncFindFuncArg: (connectionArg: plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper) => Promise<boolean>
298
- ): Promise<(plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper)[]> {
299
- // SmartServe mode
300
- if (this.smartServeRef) {
301
- const matchingConnections: ISmartServeConnectionWrapper[] = [];
302
- for (const peer of this.smartServeRef.getWebSocketConnections()) {
303
- let wrapper = this.smartServeConnectionWrappers.get(peer.id);
304
- if (!wrapper) {
305
- wrapper = wrapSmartServePeer(peer);
306
- this.smartServeConnectionWrappers.set(peer.id, wrapper);
307
- }
308
- if (await asyncFindFuncArg(wrapper)) {
309
- matchingConnections.push(wrapper);
310
- }
311
- }
312
- return matchingConnections;
545
+ asyncFindFuncArg: (connectionArg: ISmartServeConnectionWrapper) => Promise<boolean>
546
+ ): Promise<ISmartServeConnectionWrapper[]> {
547
+ if (this.side !== 'server' || !this.smartServeRef) {
548
+ throw new Error('findAllTargetConnections is only available on servers');
313
549
  }
314
550
 
315
- // Smartsocket mode
316
- if (this.socketServerOrClient instanceof plugins.smartsocket.Smartsocket) {
317
- const matchingSockets: plugins.smartsocket.SocketConnection[] = [];
318
- for (const socketConnection of this.socketServerOrClient.socketConnections.getArray()) {
319
- if (await asyncFindFuncArg(socketConnection)) {
320
- matchingSockets.push(socketConnection);
321
- }
551
+ const matchingConnections: ISmartServeConnectionWrapper[] = [];
552
+ for (const peer of this.smartServeRef.getWebSocketConnections()) {
553
+ const wrapper = this.getOrCreateWrapper(peer);
554
+ if (await asyncFindFuncArg(wrapper)) {
555
+ matchingConnections.push(wrapper);
322
556
  }
323
- return matchingSockets;
324
557
  }
325
-
326
- throw new Error('this method >>findTargetConnection<< is only available from the server');
558
+ return matchingConnections;
327
559
  }
328
560
 
329
561
  /**
330
- * returns a single target connection by returning the first one of all matching ones
331
- * @param asyncFindFuncArg
332
- * @returns
562
+ * Finds the first connection matching the filter function.
563
+ * @server-only
333
564
  */
334
565
  public async findTargetConnection(
335
- asyncFindFuncArg: (connectionArg: plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper) => Promise<boolean>
336
- ): Promise<plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper | undefined> {
566
+ asyncFindFuncArg: (connectionArg: ISmartServeConnectionWrapper) => Promise<boolean>
567
+ ): Promise<ISmartServeConnectionWrapper | undefined> {
337
568
  const allMatching = await this.findAllTargetConnections(asyncFindFuncArg);
338
569
  return allMatching[0];
339
570
  }
340
571
 
341
572
  /**
342
- * Find all connections that have a specific tag
343
- * Works with both Smartsocket and SmartServe backends
573
+ * Finds all connections with the specified tag.
574
+ * @server-only
344
575
  */
345
- public async findAllTargetConnectionsByTag<
346
- TTag extends plugins.typedrequestInterfaces.ITag = any
347
- >(keyArg: TTag['name'], payloadArg?: TTag['payload']): Promise<(plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper)[]> {
348
- // SmartServe mode - use native filtering for better performance
349
- if (this.smartServeRef) {
350
- const peers = this.smartServeRef.getWebSocketConnectionsByTag(keyArg);
351
- const results: ISmartServeConnectionWrapper[] = [];
352
-
353
- for (const peer of peers) {
354
- let wrapper = this.smartServeConnectionWrappers.get(peer.id);
355
- if (!wrapper) {
356
- wrapper = wrapSmartServePeer(peer);
357
- this.smartServeConnectionWrappers.set(peer.id, wrapper);
358
- }
576
+ public async findAllTargetConnectionsByTag<TTag extends plugins.typedrequestInterfaces.ITag = any>(
577
+ keyArg: TTag['name'],
578
+ payloadArg?: TTag['payload']
579
+ ): Promise<ISmartServeConnectionWrapper[]> {
580
+ if (this.side !== 'server' || !this.smartServeRef) {
581
+ throw new Error('findAllTargetConnectionsByTag is only available on servers');
582
+ }
583
+
584
+ const peers = this.smartServeRef.getWebSocketConnectionsByTag(keyArg);
585
+ const results: ISmartServeConnectionWrapper[] = [];
359
586
 
360
- // If payload specified, also filter by payload stored in peer.data
361
- if (payloadArg !== undefined) {
362
- const tag = await wrapper.getTagById(keyArg);
363
- if (plugins.smartjson.stringify(tag?.payload) !== plugins.smartjson.stringify(payloadArg)) {
364
- continue;
365
- }
587
+ for (const peer of peers) {
588
+ const wrapper = this.getOrCreateWrapper(peer);
589
+
590
+ // If payload specified, also filter by payload
591
+ if (payloadArg !== undefined) {
592
+ const tag = await wrapper.getTagById(keyArg);
593
+ if (plugins.smartjson.stringify(tag?.payload) !== plugins.smartjson.stringify(payloadArg)) {
594
+ continue;
366
595
  }
367
- results.push(wrapper);
368
596
  }
369
- return results;
597
+ results.push(wrapper);
370
598
  }
371
-
372
- // Smartsocket mode - use existing logic
373
- return this.findAllTargetConnections(async (socketConnectionArg) => {
374
- let result: boolean;
375
- if (!payloadArg) {
376
- result = !!(await (socketConnectionArg as plugins.smartsocket.SocketConnection).getTagById(keyArg));
377
- } else {
378
- result = !!(
379
- plugins.smartjson.stringify((await (socketConnectionArg as plugins.smartsocket.SocketConnection).getTagById(keyArg))?.payload) ===
380
- plugins.smartjson.stringify(payloadArg)
381
- );
382
- }
383
- return result;
384
- });
599
+ return results;
385
600
  }
386
601
 
387
602
  /**
388
- * Find a single connection by tag
603
+ * Finds the first connection with the specified tag.
604
+ * @server-only
389
605
  */
390
606
  public async findTargetConnectionByTag<TTag extends plugins.typedrequestInterfaces.ITag = any>(
391
607
  keyArg: TTag['name'],
392
608
  payloadArg?: TTag['payload']
393
- ): Promise<plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper | undefined> {
609
+ ): Promise<ISmartServeConnectionWrapper | undefined> {
394
610
  const allResults = await this.findAllTargetConnectionsByTag(keyArg, payloadArg);
395
611
  return allResults[0];
396
612
  }
397
-
398
- /**
399
- * Stop the TypedSocket server/client
400
- * Note: In SmartServe mode, SmartServe manages its own lifecycle
401
- */
402
- public async stop() {
403
- if (this.smartServeRef) {
404
- // SmartServe manages its own lifecycle
405
- // Clear our connection wrappers
406
- this.smartServeConnectionWrappers.clear();
407
- return;
408
- }
409
- await this.socketServerOrClient.stop();
410
- }
411
613
  }