@api.global/typedsocket 4.0.0 → 4.1.2
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 +372 -191
- package/dist_ts/typedsocket.plugins.d.ts +3 -2
- package/dist_ts/typedsocket.plugins.js +4 -3
- package/npmextra.json +13 -7
- package/package.json +14 -17
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/typedsocket.classes.typedsocket.ts +491 -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;
|
|
129
97
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
return
|
|
98
|
+
await typedSocket.connect();
|
|
99
|
+
|
|
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,485 @@ 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
|
+
// Reject all pending requests — the connection is gone and they'll never receive a response
|
|
329
|
+
for (const [id, pending] of this.pendingRequests) {
|
|
330
|
+
pending.reject(new Error('TypedSocket disconnected'));
|
|
331
|
+
}
|
|
332
|
+
this.pendingRequests.clear();
|
|
333
|
+
|
|
334
|
+
if (this.clientOptions?.autoReconnect && this.retryCount < this.clientOptions.maxRetries) {
|
|
335
|
+
this.scheduleReconnect();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Schedules a reconnection attempt with exponential backoff
|
|
341
|
+
*/
|
|
342
|
+
private async scheduleReconnect(): Promise<void> {
|
|
343
|
+
if (!this.clientOptions) return;
|
|
344
|
+
|
|
345
|
+
this.updateStatus('reconnecting');
|
|
346
|
+
this.retryCount++;
|
|
347
|
+
|
|
348
|
+
// Exponential backoff with jitter
|
|
349
|
+
const jitter = this.currentBackoff * 0.2 * (Math.random() * 2 - 1);
|
|
350
|
+
const delay = Math.min(this.currentBackoff + jitter, this.clientOptions.maxBackoffMs);
|
|
351
|
+
|
|
352
|
+
console.log(`TypedSocket reconnecting in ${Math.round(delay)}ms (attempt ${this.retryCount}/${this.clientOptions.maxRetries})`);
|
|
353
|
+
|
|
354
|
+
await plugins.smartdelay.delayFor(delay);
|
|
355
|
+
|
|
356
|
+
// Increase backoff for next time
|
|
357
|
+
this.currentBackoff = Math.min(this.currentBackoff * 2, this.clientOptions.maxBackoffMs);
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
await this.connect();
|
|
361
|
+
} catch (err) {
|
|
362
|
+
console.error('TypedSocket reconnection failed:', err);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Updates connection status and notifies subscribers
|
|
368
|
+
*/
|
|
369
|
+
private updateStatus(status: TConnectionStatus): void {
|
|
370
|
+
if (this.connectionStatus !== status) {
|
|
371
|
+
this.connectionStatus = status;
|
|
372
|
+
this.statusSubject.next(status);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Sends a request to the server and waits for response (client-side)
|
|
378
|
+
*/
|
|
379
|
+
private async sendRequest<T extends plugins.typedrequestInterfaces.ITypedRequest>(
|
|
380
|
+
request: T
|
|
381
|
+
): Promise<T> {
|
|
382
|
+
if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN) {
|
|
383
|
+
throw new Error('WebSocket not connected');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return new Promise((resolve, reject) => {
|
|
387
|
+
const timeout = setTimeout(() => {
|
|
388
|
+
this.pendingRequests.delete(request.correlation.id);
|
|
389
|
+
reject(new Error('Request timeout'));
|
|
390
|
+
}, 30000);
|
|
391
|
+
|
|
392
|
+
this.pendingRequests.set(request.correlation.id, {
|
|
393
|
+
resolve: (response) => {
|
|
394
|
+
clearTimeout(timeout);
|
|
395
|
+
resolve(response);
|
|
396
|
+
},
|
|
397
|
+
reject: (error) => {
|
|
398
|
+
clearTimeout(timeout);
|
|
399
|
+
reject(error);
|
|
400
|
+
},
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
this.websocket!.send(plugins.smartjson.stringify(request));
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ============================================================================
|
|
408
|
+
// PUBLIC API - SHARED
|
|
409
|
+
// ============================================================================
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Creates a TypedRequest for the specified method.
|
|
413
|
+
* On clients, sends to the server.
|
|
414
|
+
* On servers, sends to the specified target connection.
|
|
415
|
+
*/
|
|
416
|
+
public createTypedRequest<T extends plugins.typedrequestInterfaces.ITypedRequest>(
|
|
417
|
+
methodName: T['method'],
|
|
418
|
+
targetConnection?: ISmartServeConnectionWrapper
|
|
419
|
+
): plugins.typedrequest.TypedRequest<T> {
|
|
420
|
+
const postMethod = async (requestDataArg: T): Promise<T> => {
|
|
421
|
+
if (this.side === 'client') {
|
|
422
|
+
return this.sendRequest(requestDataArg);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Server-side: send to target connection
|
|
426
|
+
if (!this.smartServeRef) {
|
|
427
|
+
throw new Error('Server not initialized');
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
let target = targetConnection;
|
|
431
|
+
if (!target) {
|
|
432
|
+
const allConnections = this.smartServeRef.getWebSocketConnections();
|
|
169
433
|
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
434
|
const peer = allConnections[0];
|
|
174
|
-
|
|
175
|
-
if (!wrapper) {
|
|
176
|
-
wrapper = wrapSmartServePeer(peer);
|
|
177
|
-
connectionWrappers.set(peer.id, wrapper);
|
|
178
|
-
}
|
|
179
|
-
targetConnectionArg = wrapper;
|
|
435
|
+
target = this.getOrCreateWrapper(peer);
|
|
180
436
|
} else if (allConnections.length === 0) {
|
|
181
437
|
throw new Error('No WebSocket connections available');
|
|
182
438
|
} else {
|
|
183
|
-
throw new Error(
|
|
184
|
-
'you need to specify the wanted targetConnection. Currently no target is selectable automatically.'
|
|
185
|
-
);
|
|
439
|
+
throw new Error('Multiple connections available - specify targetConnection');
|
|
186
440
|
}
|
|
187
441
|
}
|
|
188
442
|
|
|
189
|
-
// Register interest for
|
|
190
|
-
const interest = await
|
|
191
|
-
|
|
192
|
-
|
|
443
|
+
// Register interest for response
|
|
444
|
+
const interest = await this.typedrouter.fireEventInterestMap.addInterest(
|
|
445
|
+
requestDataArg.correlation.id,
|
|
446
|
+
requestDataArg
|
|
193
447
|
);
|
|
194
448
|
|
|
195
|
-
// Send
|
|
196
|
-
|
|
449
|
+
// Send request
|
|
450
|
+
target.peer.send(plugins.smartjson.stringify(requestDataArg));
|
|
197
451
|
|
|
198
|
-
// Wait for
|
|
199
|
-
|
|
200
|
-
return response;
|
|
452
|
+
// Wait for response
|
|
453
|
+
return await interest.interestFullfilled as T;
|
|
201
454
|
};
|
|
202
455
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
postMethod as any,
|
|
207
|
-
null as any // No smartsocket server/client when using SmartServe
|
|
456
|
+
return new plugins.typedrequest.TypedRequest<T>(
|
|
457
|
+
new plugins.typedrequest.TypedTarget({ postMethod }),
|
|
458
|
+
methodName
|
|
208
459
|
);
|
|
460
|
+
}
|
|
209
461
|
|
|
210
|
-
|
|
211
|
-
|
|
462
|
+
/**
|
|
463
|
+
* Gets the current connection status
|
|
464
|
+
*/
|
|
465
|
+
public getStatus(): TConnectionStatus {
|
|
466
|
+
return this.connectionStatus;
|
|
467
|
+
}
|
|
212
468
|
|
|
213
|
-
|
|
469
|
+
/**
|
|
470
|
+
* Stops the TypedSocket client or cleans up server state
|
|
471
|
+
*/
|
|
472
|
+
public async stop(): Promise<void> {
|
|
473
|
+
if (this.side === 'client') {
|
|
474
|
+
if (this.clientOptions) {
|
|
475
|
+
this.clientOptions.autoReconnect = false;
|
|
476
|
+
}
|
|
477
|
+
if (this.websocket) {
|
|
478
|
+
this.websocket.close();
|
|
479
|
+
this.websocket = null;
|
|
480
|
+
}
|
|
481
|
+
this.pendingRequests.clear();
|
|
482
|
+
} else {
|
|
483
|
+
// Server mode - just clear wrappers (SmartServe manages its own lifecycle)
|
|
484
|
+
this.smartServeConnectionWrappers.clear();
|
|
485
|
+
}
|
|
214
486
|
}
|
|
215
487
|
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
|
|
488
|
+
// ============================================================================
|
|
489
|
+
// CLIENT-ONLY METHODS
|
|
490
|
+
// ============================================================================
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Sets a tag on this client connection.
|
|
494
|
+
* Tags are stored on the server and can be used for filtering.
|
|
495
|
+
* @client-only
|
|
496
|
+
*/
|
|
497
|
+
public async setTag<T extends plugins.typedrequestInterfaces.ITag>(
|
|
498
|
+
name: T['name'],
|
|
499
|
+
payload: T['payload']
|
|
500
|
+
): Promise<void> {
|
|
501
|
+
if (this.side !== 'client') {
|
|
502
|
+
throw new Error('setTag is only available on clients');
|
|
503
|
+
}
|
|
219
504
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
private smartServeConnectionWrappers: Map<string, ISmartServeConnectionWrapper> = new Map();
|
|
505
|
+
const request = this.createTypedRequest<IReq_SetClientTag>('__typedsocket_setTag');
|
|
506
|
+
const response = await request.fire({ name, payload });
|
|
223
507
|
|
|
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();
|
|
508
|
+
if (!response.success) {
|
|
509
|
+
throw new Error('Failed to set tag on server');
|
|
230
510
|
}
|
|
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
511
|
}
|
|
252
512
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
throw new Error('
|
|
513
|
+
/**
|
|
514
|
+
* Removes a tag from this client connection.
|
|
515
|
+
* @client-only
|
|
516
|
+
*/
|
|
517
|
+
public async removeTag(name: string): Promise<void> {
|
|
518
|
+
if (this.side !== 'client') {
|
|
519
|
+
throw new Error('removeTag is only available on clients');
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const request = this.createTypedRequest<IReq_RemoveClientTag>('__typedsocket_removeTag');
|
|
523
|
+
const response = await request.fire({ name });
|
|
524
|
+
|
|
525
|
+
if (!response.success) {
|
|
526
|
+
throw new Error('Failed to remove tag on server');
|
|
267
527
|
}
|
|
268
528
|
}
|
|
269
529
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
return
|
|
530
|
+
// ============================================================================
|
|
531
|
+
// SERVER-ONLY METHODS
|
|
532
|
+
// ============================================================================
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Gets or creates a connection wrapper for a peer
|
|
536
|
+
*/
|
|
537
|
+
private getOrCreateWrapper(peer: plugins.IWebSocketPeer): ISmartServeConnectionWrapper {
|
|
538
|
+
let wrapper = this.smartServeConnectionWrappers.get(peer.id);
|
|
539
|
+
if (!wrapper) {
|
|
540
|
+
wrapper = wrapSmartServePeer(peer);
|
|
541
|
+
this.smartServeConnectionWrappers.set(peer.id, wrapper);
|
|
542
|
+
}
|
|
543
|
+
return wrapper;
|
|
284
544
|
}
|
|
285
545
|
|
|
286
546
|
/**
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
* @param asyncFindFuncArg - async filter function
|
|
290
|
-
* @returns array of matching connections
|
|
547
|
+
* Finds all connections matching the filter function.
|
|
548
|
+
* @server-only
|
|
291
549
|
*/
|
|
292
550
|
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;
|
|
551
|
+
asyncFindFuncArg: (connectionArg: ISmartServeConnectionWrapper) => Promise<boolean>
|
|
552
|
+
): Promise<ISmartServeConnectionWrapper[]> {
|
|
553
|
+
if (this.side !== 'server' || !this.smartServeRef) {
|
|
554
|
+
throw new Error('findAllTargetConnections is only available on servers');
|
|
309
555
|
}
|
|
310
556
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
matchingSockets.push(socketConnection);
|
|
317
|
-
}
|
|
557
|
+
const matchingConnections: ISmartServeConnectionWrapper[] = [];
|
|
558
|
+
for (const peer of this.smartServeRef.getWebSocketConnections()) {
|
|
559
|
+
const wrapper = this.getOrCreateWrapper(peer);
|
|
560
|
+
if (await asyncFindFuncArg(wrapper)) {
|
|
561
|
+
matchingConnections.push(wrapper);
|
|
318
562
|
}
|
|
319
|
-
return matchingSockets;
|
|
320
563
|
}
|
|
321
|
-
|
|
322
|
-
throw new Error('this method >>findTargetConnection<< is only available from the server');
|
|
564
|
+
return matchingConnections;
|
|
323
565
|
}
|
|
324
566
|
|
|
325
567
|
/**
|
|
326
|
-
*
|
|
327
|
-
* @
|
|
328
|
-
* @returns
|
|
568
|
+
* Finds the first connection matching the filter function.
|
|
569
|
+
* @server-only
|
|
329
570
|
*/
|
|
330
571
|
public async findTargetConnection(
|
|
331
|
-
asyncFindFuncArg: (connectionArg:
|
|
332
|
-
): Promise<
|
|
572
|
+
asyncFindFuncArg: (connectionArg: ISmartServeConnectionWrapper) => Promise<boolean>
|
|
573
|
+
): Promise<ISmartServeConnectionWrapper | undefined> {
|
|
333
574
|
const allMatching = await this.findAllTargetConnections(asyncFindFuncArg);
|
|
334
575
|
return allMatching[0];
|
|
335
576
|
}
|
|
336
577
|
|
|
337
578
|
/**
|
|
338
|
-
*
|
|
339
|
-
*
|
|
579
|
+
* Finds all connections with the specified tag.
|
|
580
|
+
* @server-only
|
|
340
581
|
*/
|
|
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
|
-
}
|
|
582
|
+
public async findAllTargetConnectionsByTag<TTag extends plugins.typedrequestInterfaces.ITag = any>(
|
|
583
|
+
keyArg: TTag['name'],
|
|
584
|
+
payloadArg?: TTag['payload']
|
|
585
|
+
): Promise<ISmartServeConnectionWrapper[]> {
|
|
586
|
+
if (this.side !== 'server' || !this.smartServeRef) {
|
|
587
|
+
throw new Error('findAllTargetConnectionsByTag is only available on servers');
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const peers = this.smartServeRef.getWebSocketConnectionsByTag(keyArg);
|
|
591
|
+
const results: ISmartServeConnectionWrapper[] = [];
|
|
355
592
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
593
|
+
for (const peer of peers) {
|
|
594
|
+
const wrapper = this.getOrCreateWrapper(peer);
|
|
595
|
+
|
|
596
|
+
// If payload specified, also filter by payload
|
|
597
|
+
if (payloadArg !== undefined) {
|
|
598
|
+
const tag = await wrapper.getTagById(keyArg);
|
|
599
|
+
if (plugins.smartjson.stringify(tag?.payload) !== plugins.smartjson.stringify(payloadArg)) {
|
|
600
|
+
continue;
|
|
362
601
|
}
|
|
363
|
-
results.push(wrapper);
|
|
364
602
|
}
|
|
365
|
-
|
|
603
|
+
results.push(wrapper);
|
|
366
604
|
}
|
|
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
|
-
});
|
|
605
|
+
return results;
|
|
381
606
|
}
|
|
382
607
|
|
|
383
608
|
/**
|
|
384
|
-
*
|
|
609
|
+
* Finds the first connection with the specified tag.
|
|
610
|
+
* @server-only
|
|
385
611
|
*/
|
|
386
612
|
public async findTargetConnectionByTag<TTag extends plugins.typedrequestInterfaces.ITag = any>(
|
|
387
613
|
keyArg: TTag['name'],
|
|
388
614
|
payloadArg?: TTag['payload']
|
|
389
|
-
): Promise<
|
|
615
|
+
): Promise<ISmartServeConnectionWrapper | undefined> {
|
|
390
616
|
const allResults = await this.findAllTargetConnectionsByTag(keyArg, payloadArg);
|
|
391
617
|
return allResults[0];
|
|
392
618
|
}
|
|
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
619
|
}
|