@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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/typedsocket.classes.typedsocket.d.ts +114 -31
- package/dist_ts/typedsocket.classes.typedsocket.js +367 -191
- package/dist_ts/typedsocket.plugins.d.ts +3 -2
- package/dist_ts/typedsocket.plugins.js +4 -3
- package/package.json +8 -12
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/typedsocket.classes.typedsocket.ts +485 -279
- package/ts/typedsocket.plugins.ts +4 -3
|
@@ -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
|
-
//
|
|
58
|
+
// ============================================================================
|
|
59
|
+
// STATIC METHODS
|
|
60
|
+
// ============================================================================
|
|
61
|
+
|
|
36
62
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
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
|
-
|
|
83
|
+
options: ITypedSocketClientOptions = {}
|
|
93
84
|
): Promise<TypedSocket> {
|
|
94
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
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
|
|
190
|
-
const interest = await
|
|
191
|
-
|
|
192
|
-
|
|
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
|
|
196
|
-
|
|
443
|
+
// Send request
|
|
444
|
+
target.peer.send(plugins.smartjson.stringify(requestDataArg));
|
|
197
445
|
|
|
198
|
-
// Wait for
|
|
199
|
-
|
|
200
|
-
return response;
|
|
446
|
+
// Wait for response
|
|
447
|
+
return await interest.interestFullfilled as T;
|
|
201
448
|
};
|
|
202
449
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
211
|
-
|
|
456
|
+
/**
|
|
457
|
+
* Gets the current connection status
|
|
458
|
+
*/
|
|
459
|
+
public getStatus(): TConnectionStatus {
|
|
460
|
+
return this.connectionStatus;
|
|
461
|
+
}
|
|
212
462
|
|
|
213
|
-
|
|
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
|
-
//
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
221
|
-
|
|
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
|
-
|
|
225
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
throw new Error('
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
return
|
|
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
|
-
*
|
|
288
|
-
*
|
|
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:
|
|
294
|
-
): Promise<
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
*
|
|
327
|
-
* @
|
|
328
|
-
* @returns
|
|
562
|
+
* Finds the first connection matching the filter function.
|
|
563
|
+
* @server-only
|
|
329
564
|
*/
|
|
330
565
|
public async findTargetConnection(
|
|
331
|
-
asyncFindFuncArg: (connectionArg:
|
|
332
|
-
): Promise<
|
|
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
|
-
*
|
|
339
|
-
*
|
|
573
|
+
* Finds all connections with the specified tag.
|
|
574
|
+
* @server-only
|
|
340
575
|
*/
|
|
341
|
-
public async findAllTargetConnectionsByTag<
|
|
342
|
-
TTag
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
if (this.smartServeRef) {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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<
|
|
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
|
}
|