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