socket-function 0.114.0 → 0.116.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/SocketFunction.ts +464 -462
- package/package.json +1 -1
- package/src/CallFactory.ts +785 -779
- package/src/nodeCache.ts +115 -111
- package/time/trueTimeShim.ts +3 -0
package/SocketFunction.ts
CHANGED
|
@@ -1,463 +1,465 @@
|
|
|
1
|
-
/// <reference path="./require/RequireController.ts" />
|
|
2
|
-
|
|
3
|
-
import { SocketExposedInterface, SocketFunctionHook, SocketFunctionClientHook, SocketExposedShape, SocketRegistered, CallerContext, FullCallType, CallType, FncType, SocketRegisterType } from "./SocketFunctionTypes";
|
|
4
|
-
import { exposeClass, registerClass, registerGlobalClientHook, registerGlobalHook, runClientHooks } from "./src/callManager";
|
|
5
|
-
import { SocketServerConfig, startSocketServer } from "./src/webSocketServer";
|
|
6
|
-
import { getCallFactory, getCreateCallFactory, getNodeId, getNodeIdLocation } from "./src/nodeCache";
|
|
7
|
-
import { getCallProxy } from "./src/nodeProxy";
|
|
8
|
-
import { Args, MaybePromise } from "./src/types";
|
|
9
|
-
import { setDefaultHTTPCall } from "./src/callHTTPHandler";
|
|
10
|
-
import debugbreak from "debugbreak";
|
|
11
|
-
import { lazy } from "./src/caching";
|
|
12
|
-
import { delay } from "./src/batching";
|
|
13
|
-
import { blue, magenta } from "./src/formatting/logColors";
|
|
14
|
-
import { JSONLACKS } from "./src/JSONLACKS/JSONLACKS";
|
|
15
|
-
import "./SetProcessVariables";
|
|
16
|
-
import cborx from "cbor-x";
|
|
17
|
-
import { setFlag } from "./require/compileFlags";
|
|
18
|
-
import { isNode } from "./src/misc";
|
|
19
|
-
import { getPendingCallCount, harvestCallTimes, harvestFailedCallCount } from "./src/CallFactory";
|
|
20
|
-
|
|
21
|
-
setFlag(require, "cbor-x", "allowclient", true);
|
|
22
|
-
let cborxInstance = new cborx.Encoder({ structuredClone: true });
|
|
23
|
-
if (isNode()) {
|
|
24
|
-
// Do not crash on unhandled errors. SocketFunction is made to run a webserver,
|
|
25
|
-
// which will run perfectly after 99.9% of errors. Crashing the process is
|
|
26
|
-
// not a good alternative to proper error log and notifications. Do you guys
|
|
27
|
-
// not get automated emails when unexpected errors are logged? I do.
|
|
28
|
-
process.on("unhandledRejection", (e) => {
|
|
29
|
-
console.error("Unhandled rejection", e);
|
|
30
|
-
});
|
|
31
|
-
process.on("uncaughtException", (e) => {
|
|
32
|
-
console.error("Uncaught exception", e);
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
module.allowclient = true;
|
|
37
|
-
|
|
38
|
-
type ExtractShape<ClassType, Shape> = {
|
|
39
|
-
[key in keyof Shape]: (
|
|
40
|
-
key extends keyof ClassType
|
|
41
|
-
? ClassType[key] extends SocketExposedInterface[""]
|
|
42
|
-
? ClassType[key]
|
|
43
|
-
: ClassType[key] extends Function ? "All exposed function must be async (or return a Promise)" : never
|
|
44
|
-
: "Function is in shape, but not in class"
|
|
45
|
-
);
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
export class SocketFunction {
|
|
49
|
-
public static logMessages = false;
|
|
50
|
-
public static trackMessageSizes = {
|
|
51
|
-
upload: [] as ((size: number) => void)[],
|
|
52
|
-
download: [] as ((size: number) => void)[],
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
public static
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
public static
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
public static
|
|
88
|
-
public static
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
()
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
noAutoExpose
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
let
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (!value
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
let
|
|
211
|
-
let
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
let
|
|
233
|
-
let
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
//
|
|
246
|
-
// if
|
|
247
|
-
//
|
|
248
|
-
// }
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
*
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
url.
|
|
321
|
-
url.searchParams.set("
|
|
322
|
-
url.searchParams.set("
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
*
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if (
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
public static
|
|
361
|
-
public static
|
|
362
|
-
|
|
363
|
-
private static
|
|
364
|
-
|
|
365
|
-
public static
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
await
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
1
|
+
/// <reference path="./require/RequireController.ts" />
|
|
2
|
+
|
|
3
|
+
import { SocketExposedInterface, SocketFunctionHook, SocketFunctionClientHook, SocketExposedShape, SocketRegistered, CallerContext, FullCallType, CallType, FncType, SocketRegisterType } from "./SocketFunctionTypes";
|
|
4
|
+
import { exposeClass, registerClass, registerGlobalClientHook, registerGlobalHook, runClientHooks } from "./src/callManager";
|
|
5
|
+
import { SocketServerConfig, startSocketServer } from "./src/webSocketServer";
|
|
6
|
+
import { getCallFactory, getCreateCallFactory, getNodeId, getNodeIdLocation } from "./src/nodeCache";
|
|
7
|
+
import { getCallProxy } from "./src/nodeProxy";
|
|
8
|
+
import { Args, MaybePromise } from "./src/types";
|
|
9
|
+
import { setDefaultHTTPCall } from "./src/callHTTPHandler";
|
|
10
|
+
import debugbreak from "debugbreak";
|
|
11
|
+
import { lazy } from "./src/caching";
|
|
12
|
+
import { delay } from "./src/batching";
|
|
13
|
+
import { blue, magenta } from "./src/formatting/logColors";
|
|
14
|
+
import { JSONLACKS } from "./src/JSONLACKS/JSONLACKS";
|
|
15
|
+
import "./SetProcessVariables";
|
|
16
|
+
import cborx from "cbor-x";
|
|
17
|
+
import { setFlag } from "./require/compileFlags";
|
|
18
|
+
import { isNode } from "./src/misc";
|
|
19
|
+
import { getPendingCallCount, harvestCallTimes, harvestFailedCallCount } from "./src/CallFactory";
|
|
20
|
+
|
|
21
|
+
setFlag(require, "cbor-x", "allowclient", true);
|
|
22
|
+
let cborxInstance = new cborx.Encoder({ structuredClone: true });
|
|
23
|
+
if (isNode()) {
|
|
24
|
+
// Do not crash on unhandled errors. SocketFunction is made to run a webserver,
|
|
25
|
+
// which will run perfectly after 99.9% of errors. Crashing the process is
|
|
26
|
+
// not a good alternative to proper error log and notifications. Do you guys
|
|
27
|
+
// not get automated emails when unexpected errors are logged? I do.
|
|
28
|
+
process.on("unhandledRejection", (e) => {
|
|
29
|
+
console.error("Unhandled rejection", e);
|
|
30
|
+
});
|
|
31
|
+
process.on("uncaughtException", (e) => {
|
|
32
|
+
console.error("Uncaught exception", e);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.allowclient = true;
|
|
37
|
+
|
|
38
|
+
type ExtractShape<ClassType, Shape> = {
|
|
39
|
+
[key in keyof Shape]: (
|
|
40
|
+
key extends keyof ClassType
|
|
41
|
+
? ClassType[key] extends SocketExposedInterface[""]
|
|
42
|
+
? ClassType[key]
|
|
43
|
+
: ClassType[key] extends Function ? "All exposed function must be async (or return a Promise)" : never
|
|
44
|
+
: "Function is in shape, but not in class"
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export class SocketFunction {
|
|
49
|
+
public static logMessages = false;
|
|
50
|
+
public static trackMessageSizes = {
|
|
51
|
+
upload: [] as ((size: number) => void)[],
|
|
52
|
+
download: [] as ((size: number) => void)[],
|
|
53
|
+
callTimes: [] as ((obj: { start: number; end: number; }) => void)[],
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
public static MAX_MESSAGE_SIZE = 1024 * 1024 * 32;
|
|
57
|
+
|
|
58
|
+
public static HTTP_ETAG_CACHE = false;
|
|
59
|
+
public static silent = true;
|
|
60
|
+
|
|
61
|
+
public static HTTP_COMPRESS = false;
|
|
62
|
+
|
|
63
|
+
// If you have HTTP resources that require cookies you might to set `SocketFunction.COEP = "require-corp"`
|
|
64
|
+
// - Cross-origin-resource-policy.
|
|
65
|
+
public static COEP = "credentialless";
|
|
66
|
+
|
|
67
|
+
// In retrospect... dynamically changing the wire serializer is a BAD idea. If any calls happen
|
|
68
|
+
// before it is changed, things just break. Also, it needs to be changed on both sides,
|
|
69
|
+
// or else things break. Also, it is very hard to detect when the issue is different serializers
|
|
70
|
+
public static readonly WIRE_SERIALIZER = {
|
|
71
|
+
serialize: (obj: unknown): MaybePromise<Buffer[]> => [cborxInstance.encode(obj)],
|
|
72
|
+
deserialize: (buffers: Buffer[]): MaybePromise<unknown> => cborxInstance.decode(buffers[0]),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
public static WIRE_WARN_TIME = 100;
|
|
76
|
+
|
|
77
|
+
private static onMountCallbacks = new Map<string, (() => MaybePromise<void>)[]>();
|
|
78
|
+
public static exposedClasses = new Set<string>();
|
|
79
|
+
|
|
80
|
+
public static callerContext: CallerContext | undefined;
|
|
81
|
+
public static getCaller(): CallerContext {
|
|
82
|
+
const caller = SocketFunction.callerContext;
|
|
83
|
+
if (!caller) throw new Error(`Tried to access caller when not in the synchronous phase of a function call`);
|
|
84
|
+
return caller;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public static harvestFailedCallCount = () => harvestFailedCallCount();
|
|
88
|
+
public static getPendingCallCount = () => getPendingCallCount();
|
|
89
|
+
public static harvestCallTimes = () => harvestCallTimes();
|
|
90
|
+
|
|
91
|
+
// NOTE: We use callbacks we don't run into issues with cyclic dependencies
|
|
92
|
+
// (ex, using a hook in a controller where the hook also calls the controller).
|
|
93
|
+
/*
|
|
94
|
+
export const DiskLoggerController = SocketFunction.register(
|
|
95
|
+
// Can be anything, but should be unique amongst other controllers on your server.
|
|
96
|
+
"DiskLoggerController-f76a6fdf-3bd5-4bd4-a183-55a8be0a5a32",
|
|
97
|
+
// Contains the functions that can be exposed, which must all be async.
|
|
98
|
+
// Only those listed below will be exposed.
|
|
99
|
+
new DiskLoggerControllerBase(),
|
|
100
|
+
() => ({
|
|
101
|
+
// Only functions listed here will be exposed
|
|
102
|
+
getRemoteLogFiles: {},
|
|
103
|
+
getRemoteLogBuffer: {
|
|
104
|
+
compress: true,
|
|
105
|
+
// SocketFunctionClientHook[]
|
|
106
|
+
clientHooks: [
|
|
107
|
+
(x) => {
|
|
108
|
+
// If overrideResult is set, it skips the call and returns overrideResult
|
|
109
|
+
x.overrideResult = Buffer.from(...);
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
},
|
|
113
|
+
}),
|
|
114
|
+
() => ({
|
|
115
|
+
// Default hooks for all functions
|
|
116
|
+
// SocketFunctionHook[]
|
|
117
|
+
hooks: [assertIsManagementUser],
|
|
118
|
+
}),
|
|
119
|
+
{
|
|
120
|
+
// Additionaly flags
|
|
121
|
+
}
|
|
122
|
+
);
|
|
123
|
+
*/
|
|
124
|
+
public static register<
|
|
125
|
+
ClassInstance extends object,
|
|
126
|
+
Shape extends SocketExposedShape<{
|
|
127
|
+
[key in keyof ClassInstance]: (...args: any[]) => Promise<unknown>;
|
|
128
|
+
}>,
|
|
129
|
+
Statics
|
|
130
|
+
>(
|
|
131
|
+
classGuid: string,
|
|
132
|
+
instance: ClassInstance,
|
|
133
|
+
shapeFnc: () => Shape,
|
|
134
|
+
defaultHooksFnc?: () => SocketExposedShape[""] & {
|
|
135
|
+
onMount?: () => MaybePromise<void>;
|
|
136
|
+
},
|
|
137
|
+
config?: {
|
|
138
|
+
/** @noAutoExpose If true SocketFunction.expose(Controller) must be called explicitly. */
|
|
139
|
+
noAutoExpose?: boolean;
|
|
140
|
+
statics?: Statics;
|
|
141
|
+
/** Skip timing functions calls. Useful if a lot of functions have wait time that
|
|
142
|
+
is unrelated to processing, and therefore their timings won't be useful.
|
|
143
|
+
- Also useful if our auto function wrapping code is breaking functionality,
|
|
144
|
+
such as if you have a singleton function which you compare with ===,
|
|
145
|
+
which will breaks because we replaced it with a wrapped measure function.
|
|
146
|
+
*/
|
|
147
|
+
noFunctionMeasure?: boolean;
|
|
148
|
+
}
|
|
149
|
+
): SocketRegistered<ExtractShape<ClassInstance, Shape>> & Statics {
|
|
150
|
+
void Promise.resolve().then(() => {
|
|
151
|
+
let onMount = getDefaultHooks?.().onMount;
|
|
152
|
+
if (onMount) {
|
|
153
|
+
let callbacks = SocketFunction.onMountCallbacks.get(classGuid);
|
|
154
|
+
if (!callbacks) {
|
|
155
|
+
callbacks = [];
|
|
156
|
+
SocketFunction.onMountCallbacks.set(classGuid, callbacks);
|
|
157
|
+
}
|
|
158
|
+
callbacks.push(onMount);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
let getDefaultHooks = defaultHooksFnc && lazy(defaultHooksFnc);
|
|
163
|
+
const getShape = lazy(() => {
|
|
164
|
+
let shape = shapeFnc() as SocketExposedShape;
|
|
165
|
+
let defaultHooks = getDefaultHooks?.();
|
|
166
|
+
|
|
167
|
+
for (let value of Object.values(shape)) {
|
|
168
|
+
if (!value) continue;
|
|
169
|
+
if (!value.noClientHooks) {
|
|
170
|
+
value.clientHooks = [...(defaultHooks?.clientHooks || []), ...(value.clientHooks || [])];
|
|
171
|
+
}
|
|
172
|
+
if (value.noDefaultHooks) {
|
|
173
|
+
value.hooks = [...(value.hooks || [])];
|
|
174
|
+
} else {
|
|
175
|
+
value.hooks = [...(defaultHooks?.hooks || []), ...(value.hooks || [])];
|
|
176
|
+
}
|
|
177
|
+
value.dataImmutable = defaultHooks?.dataImmutable ?? value.dataImmutable;
|
|
178
|
+
}
|
|
179
|
+
return shape as any as SocketExposedShape;
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Wait, so any constants referenced by the base shapeFnc will be fully resolved
|
|
183
|
+
// by now. This is IMPORTANT, as it allows permissions functions to be moved
|
|
184
|
+
// to a common module, instead of all being inline.
|
|
185
|
+
void Promise.resolve().then(() => {
|
|
186
|
+
registerClass(classGuid, instance as SocketExposedInterface, getShape(), {
|
|
187
|
+
noFunctionMeasure: config?.noFunctionMeasure,
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
let socketCaller = SocketFunction.rehydrateSocketCaller({
|
|
192
|
+
_classGuid: classGuid,
|
|
193
|
+
_internalType: null as any,
|
|
194
|
+
}, getShape);
|
|
195
|
+
|
|
196
|
+
if (!config?.noAutoExpose) {
|
|
197
|
+
this.expose(socketCaller);
|
|
198
|
+
}
|
|
199
|
+
return Object.assign(socketCaller, config?.statics) as any;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private static socketCache = new Map<string, SocketRegistered>();
|
|
203
|
+
public static rehydrateSocketCaller<Controller>(
|
|
204
|
+
socketRegistered: SocketRegisterType<Controller>,
|
|
205
|
+
// Shape is required for client hooks.
|
|
206
|
+
shapeFnc?: () => SocketExposedShape,
|
|
207
|
+
): SocketRegistered<Controller> {
|
|
208
|
+
let cached = this.socketCache.get(socketRegistered._classGuid);
|
|
209
|
+
if (!cached) {
|
|
210
|
+
let getShape = lazy(() => shapeFnc?.());
|
|
211
|
+
let classGuid = socketRegistered._classGuid;
|
|
212
|
+
let nodeProxy = getCallProxy(classGuid, async (call) => {
|
|
213
|
+
return await SocketFunction.callFromGuid(call, classGuid, getShape());
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
cached = {
|
|
217
|
+
nodes: nodeProxy,
|
|
218
|
+
_classGuid: classGuid,
|
|
219
|
+
_internalType: null as any,
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
this.socketCache.set(classGuid, cached);
|
|
223
|
+
}
|
|
224
|
+
return cached;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private static async callFromGuid<FncT extends FncType>(
|
|
228
|
+
call: FullCallType<FncT>,
|
|
229
|
+
classGuid: string,
|
|
230
|
+
shape?: SocketExposedShape,
|
|
231
|
+
): Promise<ReturnType<FncType>> {
|
|
232
|
+
let nodeId = call.nodeId;
|
|
233
|
+
let functionName = call.functionName;
|
|
234
|
+
let time = Date.now();
|
|
235
|
+
if (SocketFunction.logMessages) {
|
|
236
|
+
console.log(`START\t\t\t${classGuid}.${functionName} at ${Date.now()}`);
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
let callFactory = await getCreateCallFactory(nodeId);
|
|
240
|
+
|
|
241
|
+
let shapeObj = shape?.[functionName];
|
|
242
|
+
if (!shapeObj) {
|
|
243
|
+
shapeObj = {};
|
|
244
|
+
}
|
|
245
|
+
// NOTE: Actually... this just means the client doesn't have a definition for it. The server
|
|
246
|
+
// might, so call it, and let them throw if it is unrecognized.
|
|
247
|
+
// if (!shapeObj) {
|
|
248
|
+
// throw new Error(`Function ${functionName} is not in shape`);
|
|
249
|
+
// }
|
|
250
|
+
|
|
251
|
+
let hookResult = await runClientHooks(call, shapeObj as Exclude<SocketExposedShape[""], undefined>, callFactory.connectionId);
|
|
252
|
+
|
|
253
|
+
if ("overrideResult" in hookResult) {
|
|
254
|
+
for (let callback of hookResult.onResult) {
|
|
255
|
+
await callback(hookResult.overrideResult);
|
|
256
|
+
}
|
|
257
|
+
if ("overrideResult" in hookResult) {
|
|
258
|
+
return hookResult.overrideResult;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
let result = await callFactory.performCall(call);
|
|
263
|
+
for (let callback of hookResult.onResult) {
|
|
264
|
+
await callback(result);
|
|
265
|
+
}
|
|
266
|
+
if ("overrideResult" in hookResult) {
|
|
267
|
+
return hookResult.overrideResult;
|
|
268
|
+
}
|
|
269
|
+
return result;
|
|
270
|
+
} finally {
|
|
271
|
+
time = Date.now() - time;
|
|
272
|
+
if (SocketFunction.logMessages) {
|
|
273
|
+
console.log(`FINISHED\t${time}ms\t${classGuid}.${functionName} at ${Date.now()}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
public static onNextDisconnect(nodeId: string, callback: () => void) {
|
|
279
|
+
(async () => {
|
|
280
|
+
let factory = await getCallFactory(nodeId);
|
|
281
|
+
if (!factory) {
|
|
282
|
+
callback();
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
factory.onNextDisconnect(callback);
|
|
287
|
+
})().catch(() => {
|
|
288
|
+
callback();
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
public static getLastDisconnectTime(nodeId: string): number | undefined {
|
|
292
|
+
let factory = getCallFactory(nodeId);
|
|
293
|
+
if (!factory) {
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
if (factory instanceof Promise) {
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
return factory.lastClosed;
|
|
300
|
+
}
|
|
301
|
+
public static isNodeConnected(nodeId: string): boolean {
|
|
302
|
+
let factory = getCallFactory(nodeId);
|
|
303
|
+
if (!factory) {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
if (factory instanceof Promise) {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
return !!factory.isConnected;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** NOTE: Only works if the nodeIs used is from SocketFunction.connect (we can't convert arbitrary nodeIds into urls,
|
|
313
|
+
* as we have no way of knowing how to contain a nodeId).
|
|
314
|
+
* */
|
|
315
|
+
public static getHTTPCallLink(call: FullCallType): string {
|
|
316
|
+
let location = getNodeIdLocation(call.nodeId);
|
|
317
|
+
if (!location) {
|
|
318
|
+
throw new Error(`Cannot find call location for nodeId, and so do not know where call location is. NodeId ${call.nodeId}`);
|
|
319
|
+
}
|
|
320
|
+
let url = new URL(`https://${location.address}:${location.port}`);
|
|
321
|
+
url.searchParams.set("classGuid", call.classGuid);
|
|
322
|
+
url.searchParams.set("functionName", call.functionName);
|
|
323
|
+
url.searchParams.set("args", JSON.stringify(call.args));
|
|
324
|
+
return url.toString();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private static ignoreExposeCount = 0;
|
|
328
|
+
public static async ignoreExposeCalls<T>(code: () => Promise<T>) {
|
|
329
|
+
this.ignoreExposeCount++;
|
|
330
|
+
try {
|
|
331
|
+
return await code();
|
|
332
|
+
} finally {
|
|
333
|
+
this.ignoreExposeCount--;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** Expose should be called before your mounting occurs. It mostly just exists to ensure you include the class type,
|
|
338
|
+
* so the class type's module construction runs, which should trigger register. Otherwise you would have
|
|
339
|
+
* to add additional imports to ensure the register call runs.
|
|
340
|
+
*/
|
|
341
|
+
public static expose(socketRegistered: SocketRegistered) {
|
|
342
|
+
if (this.ignoreExposeCount > 0) return;
|
|
343
|
+
if (!socketRegistered._classGuid) {
|
|
344
|
+
throw new Error("SocketFunction.expose must be called with a classGuid");
|
|
345
|
+
}
|
|
346
|
+
console.log(`Exposing Controller ${blue(socketRegistered._classGuid)}`);
|
|
347
|
+
exposeClass(socketRegistered);
|
|
348
|
+
this.exposedClasses.add(socketRegistered._classGuid);
|
|
349
|
+
|
|
350
|
+
if (this.hasMounted) {
|
|
351
|
+
let mountCallbacks = SocketFunction.onMountCallbacks.get(socketRegistered._classGuid);
|
|
352
|
+
for (let onMount of mountCallbacks || []) {
|
|
353
|
+
Promise.resolve(onMount()).catch(e => {
|
|
354
|
+
console.error("Error in onMount callback exposed after mount", e);
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
public static mountedNodeId: string = "";
|
|
361
|
+
public static isMounted() { return !!this.mountedNodeId; }
|
|
362
|
+
public static mountedIP: string = "";
|
|
363
|
+
private static hasMounted = false;
|
|
364
|
+
private static onMountCallback: () => void = () => { };
|
|
365
|
+
public static mountPromise: Promise<void> = new Promise(r => this.onMountCallback = r);
|
|
366
|
+
public static async mount(config: SocketServerConfig) {
|
|
367
|
+
if (this.mountedNodeId) {
|
|
368
|
+
throw new Error("SocketFunction already mounted, mounting twice in one thread is not allowed.");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
this.mountedIP = config.public ? "0.0.0.0" : "127.0.0.1";
|
|
372
|
+
if (config.ip) {
|
|
373
|
+
this.mountedIP = config.ip;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const { waitForFirstTimeSync, shimDateNow } = await import("./time/trueTimeShim");
|
|
377
|
+
shimDateNow();
|
|
378
|
+
await waitForFirstTimeSync();
|
|
379
|
+
|
|
380
|
+
// Wait for any additionals functions to expose themselves
|
|
381
|
+
await delay("immediate");
|
|
382
|
+
|
|
383
|
+
this.mountedNodeId = await startSocketServer(config);
|
|
384
|
+
this.hasMounted = true;
|
|
385
|
+
for (let classGuid of SocketFunction.exposedClasses) {
|
|
386
|
+
let callbacks = SocketFunction.onMountCallbacks.get(classGuid);
|
|
387
|
+
if (!callbacks) continue;
|
|
388
|
+
for (let callback of callbacks) {
|
|
389
|
+
await callback();
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
this.onMountCallback();
|
|
393
|
+
return this.mountedNodeId;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** Sets the default call when an http request is made, but no classGuid is set.
|
|
397
|
+
* NOTE: All other calls should be endpoint calls, even if those endpoints return a static file with an HTML content type.
|
|
398
|
+
* - However, to load new content, you should probably just use `require("./example.ts")`, which works on any files
|
|
399
|
+
* clientside that have also been required serverside (and whitelisted with module.allowclient = true,
|
|
400
|
+
* or with an `allowclient.flag` file in the directory or parent directory).
|
|
401
|
+
*/
|
|
402
|
+
public static setDefaultHTTPCall<
|
|
403
|
+
Registered extends SocketRegistered,
|
|
404
|
+
FunctionName extends keyof Registered["nodes"][""] & string,
|
|
405
|
+
>(
|
|
406
|
+
registered: Registered,
|
|
407
|
+
functionName: FunctionName,
|
|
408
|
+
...args: Args<Registered["nodes"][""][FunctionName]>
|
|
409
|
+
) {
|
|
410
|
+
setDefaultHTTPCall({
|
|
411
|
+
classGuid: registered._classGuid,
|
|
412
|
+
functionName,
|
|
413
|
+
args,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
public static connect(location: { address: string, port: number }): string {
|
|
418
|
+
return getNodeId(location.address, location.port);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
public static browserNodeId() {
|
|
422
|
+
if (isNode()) {
|
|
423
|
+
throw new Error("Cannot get browser nodeId on server");
|
|
424
|
+
}
|
|
425
|
+
let edgeNode = getBootedEdgeNode();
|
|
426
|
+
if (edgeNode) {
|
|
427
|
+
return edgeNode.host;
|
|
428
|
+
}
|
|
429
|
+
return SocketFunction.connect({ address: location.hostname, port: +location.port || 443 });
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
public static addGlobalHook(hook: SocketFunctionHook) {
|
|
433
|
+
registerGlobalHook(hook as SocketFunctionHook);
|
|
434
|
+
}
|
|
435
|
+
public static addGlobalClientHook(hook: SocketFunctionClientHook) {
|
|
436
|
+
registerGlobalClientHook(hook as SocketFunctionClientHook);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
declare global {
|
|
441
|
+
var BOOTED_EDGE_NODE: { host: string } | undefined;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function getBootedEdgeNode() {
|
|
445
|
+
return BOOTED_EDGE_NODE as { host: string } | undefined;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
let socketContextSeqNum = 1;
|
|
450
|
+
|
|
451
|
+
export function _setSocketContext<T>(
|
|
452
|
+
caller: CallerContext,
|
|
453
|
+
code: () => T,
|
|
454
|
+
) {
|
|
455
|
+
socketContextSeqNum++;
|
|
456
|
+
let seqNum = socketContextSeqNum;
|
|
457
|
+
SocketFunction.callerContext = caller;
|
|
458
|
+
try {
|
|
459
|
+
return code();
|
|
460
|
+
} finally {
|
|
461
|
+
if (seqNum === socketContextSeqNum) {
|
|
462
|
+
SocketFunction.callerContext = undefined;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
463
465
|
}
|