@api.global/typedsocket 4.0.0 → 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,109 +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
- ): Promise<TypedSocket> {
43
- const smartsocketServer = new plugins.smartsocket.Smartsocket({
44
- alias: 'typedsocketServer',
45
- port: 3000,
46
- });
47
-
48
- smartsocketServer.socketFunctions.add(
49
- new plugins.smartsocket.SocketFunction({
50
- funcName: 'processMessage',
51
- funcDef: async (dataArg, socketConnectionArg) => {
52
- return typedrouterArg.routeAndAddResponse(dataArg);
53
- },
54
- })
55
- );
56
- const typedsocket = new TypedSocket(
57
- 'server',
58
- typedrouterArg,
59
- async <T extends plugins.typedrequestInterfaces.ITypedRequest>(
60
- dataArg: T,
61
- targetConnectionArg?: plugins.smartsocket.SocketConnection
62
- ): Promise<T> => {
63
- if (!targetConnectionArg) {
64
- if ((smartsocketServer.socketConnections.getArray().length = 1)) {
65
- console.log(
66
- 'Since no targetConnection was supplied and there is only one active one present, choosing that one automatically'
67
- );
68
- targetConnectionArg = smartsocketServer.socketConnections.getArray()[0];
69
- } else {
70
- throw new Error(
71
- 'you need to specify the wanted targetConnection. Currently no target is selectable automatically.'
72
- );
73
- }
74
- }
75
- const response: T = (await smartsocketServer.clientCall(
76
- 'processMessage',
77
- dataArg,
78
- targetConnectionArg
79
- )) as any;
80
- return response;
81
- },
82
- smartsocketServer
83
- );
84
- await smartsocketServer.start();
85
-
86
- return typedsocket;
87
- }
88
-
89
80
  public static async createClient(
90
81
  typedrouterArg: plugins.typedrequest.TypedRouter,
91
82
  serverUrlArg: string,
92
- aliasArg = 'clientArg'
83
+ options: ITypedSocketClientOptions = {}
93
84
  ): Promise<TypedSocket> {
94
- const domain = new plugins.smartstring.Domain(serverUrlArg);
95
-
96
- const socketOptions: plugins.smartsocket.ISmartsocketClientOptions = {
97
- alias: aliasArg,
98
- port: domain.port || 3000,
99
- url: `${domain.nodeParsedUrl.protocol}//${domain.nodeParsedUrl.hostname}`,
85
+ const defaultOptions: Required<ITypedSocketClientOptions> = {
100
86
  autoReconnect: true,
87
+ maxRetries: 100,
88
+ initialBackoffMs: 1000,
89
+ maxBackoffMs: 60000,
101
90
  };
102
- console.log(`starting typedsocket with the following settings:`);
103
- console.log(socketOptions);
104
- const smartsocketClient = new plugins.smartsocket.SmartsocketClient(socketOptions);
105
- smartsocketClient.addSocketFunction(
106
- new plugins.smartsocket.SocketFunction({
107
- funcName: 'processMessage',
108
- funcDef: async (dataArg, socketConnectionArg) => {
109
- return typedrouterArg.routeAndAddResponse(dataArg);
110
- },
111
- })
112
- );
113
- const typedsocket = new TypedSocket(
114
- 'client',
115
- typedrouterArg,
116
- async <T extends plugins.typedrequestInterfaces.ITypedRequest>(dataArg: T): Promise<T> => {
117
- const response: T = smartsocketClient.serverCall('processMessage', dataArg) as any as T;
118
- return response;
119
- },
120
- smartsocketClient
121
- );
122
- console.log(`typedsocket triggering smartsocket to connect...`);
123
- const before = Date.now();
124
- await smartsocketClient.connect();
125
- console.log(`typedsocket triggered smartsocket connected in ${Date.now() - before}ms!!!`)
91
+ const opts = { ...defaultOptions, ...options };
126
92
 
127
- return typedsocket;
128
- }
93
+ const typedSocket = new TypedSocket('client', typedrouterArg);
94
+ typedSocket.clientOptions = opts;
95
+ typedSocket.serverUrl = serverUrlArg;
96
+ typedSocket.currentBackoff = opts.initialBackoffMs;
97
+
98
+ await typedSocket.connect();
129
99
 
130
- public static useWindowLocationOriginUrl = () => {
131
- const windowLocationResult = plugins.smarturl.Smarturl.createFromUrl(globalThis.location.origin).toString();
132
- return windowLocationResult;
100
+ return typedSocket;
133
101
  }
134
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
+
135
111
  /**
136
112
  * Creates a TypedSocket server from an existing SmartServe instance.
137
- * 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.
138
114
  *
139
115
  * @param smartServeArg - SmartServe instance with typedRouter configured in websocket options
140
116
  * @param typedRouterArg - TypedRouter for handling requests (must match SmartServe's typedRouter)
@@ -159,249 +135,479 @@ export class TypedSocket {
159
135
  ): TypedSocket {
160
136
  const connectionWrappers = new Map<string, ISmartServeConnectionWrapper>();
161
137
 
162
- // Create the postMethod for server-initiated requests
163
- const postMethod = async <T extends plugins.typedrequestInterfaces.ITypedRequest>(
164
- dataArg: T,
165
- targetConnectionArg?: ISmartServeConnectionWrapper
166
- ): Promise<T> => {
167
- if (!targetConnectionArg) {
168
- 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();
169
427
  if (allConnections.length === 1) {
170
- console.log(
171
- 'Since no targetConnection was supplied and there is only one active one present, choosing that one automatically'
172
- );
173
428
  const peer = allConnections[0];
174
- let wrapper = connectionWrappers.get(peer.id);
175
- if (!wrapper) {
176
- wrapper = wrapSmartServePeer(peer);
177
- connectionWrappers.set(peer.id, wrapper);
178
- }
179
- targetConnectionArg = wrapper;
429
+ target = this.getOrCreateWrapper(peer);
180
430
  } else if (allConnections.length === 0) {
181
431
  throw new Error('No WebSocket connections available');
182
432
  } else {
183
- throw new Error(
184
- 'you need to specify the wanted targetConnection. Currently no target is selectable automatically.'
185
- );
433
+ throw new Error('Multiple connections available - specify targetConnection');
186
434
  }
187
435
  }
188
436
 
189
- // Register interest for the response using correlation ID
190
- const interest = await typedRouterArg.fireEventInterestMap.addInterest(
191
- dataArg.correlation.id,
192
- dataArg
437
+ // Register interest for response
438
+ const interest = await this.typedrouter.fireEventInterestMap.addInterest(
439
+ requestDataArg.correlation.id,
440
+ requestDataArg
193
441
  );
194
442
 
195
- // Send the request to the client
196
- targetConnectionArg.peer.send(plugins.smartjson.stringify(dataArg));
443
+ // Send request
444
+ target.peer.send(plugins.smartjson.stringify(requestDataArg));
197
445
 
198
- // Wait for the response (TypedRouter will fulfill via routeAndAddResponse when response arrives)
199
- const response = await interest.interestFullfilled as T;
200
- return response;
446
+ // Wait for response
447
+ return await interest.interestFullfilled as T;
201
448
  };
202
449
 
203
- const typedSocket = new TypedSocket(
204
- 'server',
205
- typedRouterArg,
206
- postMethod as any,
207
- 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
208
453
  );
454
+ }
209
455
 
210
- typedSocket.smartServeRef = smartServeArg;
211
- typedSocket.smartServeConnectionWrappers = connectionWrappers;
456
+ /**
457
+ * Gets the current connection status
458
+ */
459
+ public getStatus(): TConnectionStatus {
460
+ return this.connectionStatus;
461
+ }
212
462
 
213
- 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
+ }
214
480
  }
215
481
 
216
- // INSTANCE
217
- public side: TTypedSocketSide;
218
- 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
+ }
219
498
 
220
- // SmartServe mode properties
221
- private smartServeRef?: plugins.SmartServe;
222
- private smartServeConnectionWrappers: Map<string, ISmartServeConnectionWrapper> = new Map();
499
+ const request = this.createTypedRequest<IReq_SetClientTag>('__typedsocket_setTag');
500
+ const response = await request.fire({ name, payload });
223
501
 
224
- public get eventSubject(): plugins.smartrx.rxjs.Subject<plugins.smartsocket.TConnectionStatus> {
225
- if (this.smartServeRef) {
226
- // SmartServe doesn't provide an eventSubject, return a new Subject
227
- // In SmartServe mode, connection events are handled via onConnectionOpen/onConnectionClose hooks
228
- console.warn('eventSubject is not fully supported in SmartServe mode. Use SmartServe hooks instead.');
229
- return new plugins.smartrx.rxjs.Subject();
502
+ if (!response.success) {
503
+ throw new Error('Failed to set tag on server');
230
504
  }
231
- return this.socketServerOrClient.eventSubject;
232
- }
233
- private postMethod: plugins.typedrequest.IPostMethod &
234
- ((
235
- typedRequestPostObject: plugins.typedrequestInterfaces.ITypedRequest,
236
- socketConnectionArg?: plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper
237
- ) => Promise<plugins.typedrequestInterfaces.ITypedRequest>);
238
- private socketServerOrClient:
239
- | plugins.smartsocket.Smartsocket
240
- | plugins.smartsocket.SmartsocketClient;
241
- constructor(
242
- sideArg: TTypedSocketSide,
243
- typedrouterArg: plugins.typedrequest.TypedRouter,
244
- postMethodArg: plugins.typedrequest.IPostMethod,
245
- socketServerOrClientArg: plugins.smartsocket.Smartsocket | plugins.smartsocket.SmartsocketClient
246
- ) {
247
- this.side = sideArg;
248
- this.typedrouter = typedrouterArg;
249
- this.postMethod = postMethodArg;
250
- this.socketServerOrClient = socketServerOrClientArg;
251
505
  }
252
506
 
253
- public addTag<T extends plugins.typedrequestInterfaces.ITag = any>(
254
- nameArg: T['name'],
255
- payloadArg: T['payload']
256
- ) {
257
- if (
258
- this.side === 'client' &&
259
- this.socketServerOrClient instanceof plugins.smartsocket.SmartsocketClient
260
- ) {
261
- this.socketServerOrClient.socketConnection.addTag({
262
- id: nameArg,
263
- payload: payloadArg,
264
- });
265
- } else {
266
- 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');
267
521
  }
268
522
  }
269
523
 
270
- public createTypedRequest<T extends plugins.typedrequestInterfaces.ITypedRequest>(
271
- methodName: T['method'],
272
- targetConnection?: plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper
273
- ): plugins.typedrequest.TypedRequest<T> {
274
- const typedrequest = new plugins.typedrequest.TypedRequest<T>(
275
- new plugins.typedrequest.TypedTarget({
276
- postMethod: async (requestDataArg) => {
277
- const result = await this.postMethod(requestDataArg, targetConnection as any);
278
- return result;
279
- },
280
- }),
281
- methodName
282
- );
283
- 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;
284
538
  }
285
539
 
286
540
  /**
287
- * returns all matching target connections
288
- * Works with both Smartsocket and SmartServe backends
289
- * @param asyncFindFuncArg - async filter function
290
- * @returns array of matching connections
541
+ * Finds all connections matching the filter function.
542
+ * @server-only
291
543
  */
292
544
  public async findAllTargetConnections(
293
- asyncFindFuncArg: (connectionArg: plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper) => Promise<boolean>
294
- ): Promise<(plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper)[]> {
295
- // SmartServe mode
296
- if (this.smartServeRef) {
297
- const matchingConnections: ISmartServeConnectionWrapper[] = [];
298
- for (const peer of this.smartServeRef.getWebSocketConnections()) {
299
- let wrapper = this.smartServeConnectionWrappers.get(peer.id);
300
- if (!wrapper) {
301
- wrapper = wrapSmartServePeer(peer);
302
- this.smartServeConnectionWrappers.set(peer.id, wrapper);
303
- }
304
- if (await asyncFindFuncArg(wrapper)) {
305
- matchingConnections.push(wrapper);
306
- }
307
- }
308
- 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');
309
549
  }
310
550
 
311
- // Smartsocket mode
312
- if (this.socketServerOrClient instanceof plugins.smartsocket.Smartsocket) {
313
- const matchingSockets: plugins.smartsocket.SocketConnection[] = [];
314
- for (const socketConnection of this.socketServerOrClient.socketConnections.getArray()) {
315
- if (await asyncFindFuncArg(socketConnection)) {
316
- matchingSockets.push(socketConnection);
317
- }
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);
318
556
  }
319
- return matchingSockets;
320
557
  }
321
-
322
- throw new Error('this method >>findTargetConnection<< is only available from the server');
558
+ return matchingConnections;
323
559
  }
324
560
 
325
561
  /**
326
- * returns a single target connection by returning the first one of all matching ones
327
- * @param asyncFindFuncArg
328
- * @returns
562
+ * Finds the first connection matching the filter function.
563
+ * @server-only
329
564
  */
330
565
  public async findTargetConnection(
331
- asyncFindFuncArg: (connectionArg: plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper) => Promise<boolean>
332
- ): Promise<plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper | undefined> {
566
+ asyncFindFuncArg: (connectionArg: ISmartServeConnectionWrapper) => Promise<boolean>
567
+ ): Promise<ISmartServeConnectionWrapper | undefined> {
333
568
  const allMatching = await this.findAllTargetConnections(asyncFindFuncArg);
334
569
  return allMatching[0];
335
570
  }
336
571
 
337
572
  /**
338
- * Find all connections that have a specific tag
339
- * Works with both Smartsocket and SmartServe backends
573
+ * Finds all connections with the specified tag.
574
+ * @server-only
340
575
  */
341
- public async findAllTargetConnectionsByTag<
342
- TTag extends plugins.typedrequestInterfaces.ITag = any
343
- >(keyArg: TTag['name'], payloadArg?: TTag['payload']): Promise<(plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper)[]> {
344
- // SmartServe mode - use native filtering for better performance
345
- if (this.smartServeRef) {
346
- const peers = this.smartServeRef.getWebSocketConnectionsByTag(keyArg);
347
- const results: ISmartServeConnectionWrapper[] = [];
348
-
349
- for (const peer of peers) {
350
- let wrapper = this.smartServeConnectionWrappers.get(peer.id);
351
- if (!wrapper) {
352
- wrapper = wrapSmartServePeer(peer);
353
- this.smartServeConnectionWrappers.set(peer.id, wrapper);
354
- }
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[] = [];
355
586
 
356
- // If payload specified, also filter by payload stored in peer.data
357
- if (payloadArg !== undefined) {
358
- const tag = await wrapper.getTagById(keyArg);
359
- if (plugins.smartjson.stringify(tag?.payload) !== plugins.smartjson.stringify(payloadArg)) {
360
- continue;
361
- }
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;
362
595
  }
363
- results.push(wrapper);
364
596
  }
365
- return results;
597
+ results.push(wrapper);
366
598
  }
367
-
368
- // Smartsocket mode - use existing logic
369
- return this.findAllTargetConnections(async (socketConnectionArg) => {
370
- let result: boolean;
371
- if (!payloadArg) {
372
- result = !!(await (socketConnectionArg as plugins.smartsocket.SocketConnection).getTagById(keyArg));
373
- } else {
374
- result = !!(
375
- plugins.smartjson.stringify((await (socketConnectionArg as plugins.smartsocket.SocketConnection).getTagById(keyArg))?.payload) ===
376
- plugins.smartjson.stringify(payloadArg)
377
- );
378
- }
379
- return result;
380
- });
599
+ return results;
381
600
  }
382
601
 
383
602
  /**
384
- * Find a single connection by tag
603
+ * Finds the first connection with the specified tag.
604
+ * @server-only
385
605
  */
386
606
  public async findTargetConnectionByTag<TTag extends plugins.typedrequestInterfaces.ITag = any>(
387
607
  keyArg: TTag['name'],
388
608
  payloadArg?: TTag['payload']
389
- ): Promise<plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper | undefined> {
609
+ ): Promise<ISmartServeConnectionWrapper | undefined> {
390
610
  const allResults = await this.findAllTargetConnectionsByTag(keyArg, payloadArg);
391
611
  return allResults[0];
392
612
  }
393
-
394
- /**
395
- * Stop the TypedSocket server/client
396
- * Note: In SmartServe mode, SmartServe manages its own lifecycle
397
- */
398
- public async stop() {
399
- if (this.smartServeRef) {
400
- // SmartServe manages its own lifecycle
401
- // Clear our connection wrappers
402
- this.smartServeConnectionWrappers.clear();
403
- return;
404
- }
405
- await this.socketServerOrClient.stop();
406
- }
407
613
  }