socket-function 0.8.36 → 0.8.38
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 +65 -26
- package/SocketFunctionTypes.ts +14 -22
- package/package.json +4 -2
- package/require/require.js +8 -2
- package/src/CallFactory.ts +90 -19
- package/src/JSONLACKS/JSONLACKS.generated.js +2202 -0
- package/src/JSONLACKS/JSONLACKS.generated.js.d.ts +1 -0
- package/src/JSONLACKS/JSONLACKS.pegjs +248 -0
- package/src/JSONLACKS/JSONLACKS.ts +376 -0
- package/src/batching.ts +94 -0
- package/src/caching.ts +7 -1
- package/src/callManager.ts +14 -12
- package/src/certStore.ts +2 -0
- package/src/formatting/colors.ts +79 -0
- package/src/formatting/format.ts +157 -0
- package/src/formatting/logColors.ts +18 -0
- package/src/misc.ts +74 -2
- package/src/nodeCache.ts +10 -4
- package/src/profiling/getOwnTime.ts +143 -0
- package/src/profiling/measure.ts +241 -0
- package/src/profiling/stats.ts +213 -0
- package/src/profiling/tcpLagProxy.ts +64 -0
- package/src/types.ts +1 -1
- package/src/webSocketServer.ts +24 -11
- package/src/websocketFactory.ts +7 -2
package/SocketFunction.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/// <reference path="./require/RequireController.ts" />
|
|
2
2
|
|
|
3
|
-
import { SocketExposedInterface,
|
|
3
|
+
import { SocketExposedInterface, SocketFunctionHook, SocketFunctionClientHook, SocketExposedShape, SocketRegistered, CallerContext, FullCallType } from "./SocketFunctionTypes";
|
|
4
4
|
import { exposeClass, registerClass, registerGlobalClientHook, registerGlobalHook, runClientHooks } from "./src/callManager";
|
|
5
5
|
import { SocketServerConfig, startSocketServer } from "./src/webSocketServer";
|
|
6
|
-
import {
|
|
6
|
+
import { getCallFactory, getCreateCallFactory, getNodeId, getNodeIdLocation } from "./src/nodeCache";
|
|
7
7
|
import { getCallProxy } from "./src/nodeProxy";
|
|
8
8
|
import { Args, MaybePromise } from "./src/types";
|
|
9
9
|
import { setDefaultHTTPCall } from "./src/callHTTPHandler";
|
|
@@ -28,20 +28,32 @@ type PickByType<T, Value> = {
|
|
|
28
28
|
|
|
29
29
|
export class SocketFunction {
|
|
30
30
|
public static logMessages = false;
|
|
31
|
+
|
|
32
|
+
public static MAX_MESSAGE_SIZE = 1024 * 1024 * 32;
|
|
31
33
|
public static compression: undefined | {
|
|
32
34
|
type: "gzip";
|
|
33
35
|
};
|
|
36
|
+
|
|
34
37
|
public static httpETagCache = false;
|
|
38
|
+
public static silent = true;
|
|
39
|
+
|
|
40
|
+
public static WIRE_WARN_TIME = 100;
|
|
35
41
|
|
|
36
42
|
private static onMountCallbacks = new Map<string, (() => MaybePromise<void>)[]>();
|
|
37
43
|
public static exposedClasses = new Set<string>();
|
|
38
44
|
|
|
45
|
+
public static callerContext: CallerContext | undefined;
|
|
46
|
+
public static getCaller(): CallerContext {
|
|
47
|
+
const caller = SocketFunction.callerContext;
|
|
48
|
+
if (!caller) throw new Error(`Tried to access caller when not in the synchronous phase of a function call`);
|
|
49
|
+
return caller;
|
|
50
|
+
}
|
|
51
|
+
|
|
39
52
|
// NOTE: We use callbacks we don't run into issues with cyclic dependencies
|
|
40
53
|
// (ex, using a hook in a controller where the hook also calls the controller).
|
|
41
54
|
public static register<
|
|
42
55
|
ClassInstance extends object,
|
|
43
|
-
Shape extends SocketExposedShape<SocketExposedInterface
|
|
44
|
-
CallContext extends CallContextType
|
|
56
|
+
Shape extends SocketExposedShape<SocketExposedInterface>,
|
|
45
57
|
>(
|
|
46
58
|
classGuid: string,
|
|
47
59
|
instance: ClassInstance,
|
|
@@ -49,7 +61,7 @@ export class SocketFunction {
|
|
|
49
61
|
defaultHooksFnc?: () => SocketExposedShape[""] & {
|
|
50
62
|
onMount?: () => MaybePromise<void>;
|
|
51
63
|
}
|
|
52
|
-
): SocketRegistered<ExtractShape<ClassInstance, Shape
|
|
64
|
+
): SocketRegistered<ExtractShape<ClassInstance, Shape>> {
|
|
53
65
|
let getDefaultHooks = defaultHooksFnc && lazy(defaultHooksFnc);
|
|
54
66
|
const getShape = lazy(() => {
|
|
55
67
|
let shape = shapeFnc();
|
|
@@ -76,14 +88,14 @@ export class SocketFunction {
|
|
|
76
88
|
console.log(`START\t\t\t${classGuid}.${functionName}`);
|
|
77
89
|
}
|
|
78
90
|
try {
|
|
79
|
-
let callFactory = await
|
|
91
|
+
let callFactory = await getCreateCallFactory(nodeId);
|
|
80
92
|
|
|
81
93
|
let shapeObj = getShape()[functionName];
|
|
82
94
|
if (!shapeObj) {
|
|
83
95
|
throw new Error(`Function ${functionName} is not in shape`);
|
|
84
96
|
}
|
|
85
97
|
|
|
86
|
-
let hookResult = await runClientHooks(call, shapeObj as SocketExposedShape[""]);
|
|
98
|
+
let hookResult = await runClientHooks(call, shapeObj as SocketExposedShape[""], callFactory.connectionId);
|
|
87
99
|
|
|
88
100
|
if ("overrideResult" in hookResult) {
|
|
89
101
|
return hookResult.overrideResult;
|
|
@@ -99,7 +111,6 @@ export class SocketFunction {
|
|
|
99
111
|
});
|
|
100
112
|
|
|
101
113
|
let output: SocketRegistered = {
|
|
102
|
-
context: curSocketContext,
|
|
103
114
|
nodes: nodeProxy,
|
|
104
115
|
_classGuid: classGuid,
|
|
105
116
|
};
|
|
@@ -119,6 +130,40 @@ export class SocketFunction {
|
|
|
119
130
|
return output as any;
|
|
120
131
|
}
|
|
121
132
|
|
|
133
|
+
public static onNextDisconnect(nodeId: string, callback: () => void) {
|
|
134
|
+
(async () => {
|
|
135
|
+
let factory = await getCallFactory(nodeId);
|
|
136
|
+
if (!factory) {
|
|
137
|
+
callback();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
factory.onNextDisconnect(callback);
|
|
142
|
+
})().catch(() => {
|
|
143
|
+
callback();
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
public static getLastDisconnectTime(nodeId: string): number | undefined {
|
|
147
|
+
let factory = getCallFactory(nodeId);
|
|
148
|
+
if (!factory) {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
if (factory instanceof Promise) {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
return factory.lastClosed;
|
|
155
|
+
}
|
|
156
|
+
public static isNodeConnected(nodeId: string): boolean {
|
|
157
|
+
let factory = getCallFactory(nodeId);
|
|
158
|
+
if (!factory) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
if (factory instanceof Promise) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
return !!factory.isConnected;
|
|
165
|
+
}
|
|
166
|
+
|
|
122
167
|
/** NOTE: Only works if the nodeIs used is from SocketFunction.connect (we can't convert arbitrary nodeIds into urls,
|
|
123
168
|
* as we have no way of knowing how to contain a nodeId).
|
|
124
169
|
* */
|
|
@@ -152,12 +197,18 @@ export class SocketFunction {
|
|
|
152
197
|
}
|
|
153
198
|
}
|
|
154
199
|
|
|
155
|
-
public static mountedNodeId: string = "
|
|
200
|
+
public static mountedNodeId: string = "";
|
|
201
|
+
public static mountedIP: string = "";
|
|
156
202
|
private static hasMounted = false;
|
|
157
203
|
public static async mount(config: SocketServerConfig) {
|
|
158
|
-
if (this.mountedNodeId
|
|
204
|
+
if (this.mountedNodeId) {
|
|
159
205
|
throw new Error("SocketFunction already mounted, mounting twice in one thread is not allowed.");
|
|
160
206
|
}
|
|
207
|
+
|
|
208
|
+
this.mountedIP = config.public ? "0.0.0.0" : "127.0.0.1";
|
|
209
|
+
if (config.ip) {
|
|
210
|
+
this.mountedIP = config.ip;
|
|
211
|
+
}
|
|
161
212
|
this.mountedNodeId = await startSocketServer(config);
|
|
162
213
|
this.hasMounted = true;
|
|
163
214
|
for (let classGuid of SocketFunction.exposedClasses) {
|
|
@@ -190,41 +241,29 @@ export class SocketFunction {
|
|
|
190
241
|
return getNodeId(location.address, location.port);
|
|
191
242
|
}
|
|
192
243
|
|
|
193
|
-
public static addGlobalHook
|
|
244
|
+
public static addGlobalHook(hook: SocketFunctionHook<SocketExposedInterface>) {
|
|
194
245
|
registerGlobalHook(hook as SocketFunctionHook);
|
|
195
246
|
}
|
|
196
|
-
public static addGlobalClientHook
|
|
247
|
+
public static addGlobalClientHook(hook: SocketFunctionClientHook<SocketExposedInterface>) {
|
|
197
248
|
registerGlobalClientHook(hook as SocketFunctionClientHook);
|
|
198
249
|
}
|
|
199
250
|
}
|
|
200
251
|
|
|
201
252
|
|
|
202
|
-
const curSocketContext: SocketRegistered["context"] = {
|
|
203
|
-
curContext: undefined,
|
|
204
|
-
caller: undefined,
|
|
205
|
-
getCaller() {
|
|
206
|
-
const caller = curSocketContext.caller;
|
|
207
|
-
if (!caller) throw new Error(`Tried to access caller when not in the synchronous phase of a function call`);
|
|
208
|
-
return caller;
|
|
209
|
-
}
|
|
210
|
-
};
|
|
211
253
|
let socketContextSeqNum = 1;
|
|
212
254
|
|
|
213
255
|
export function _setSocketContext<T>(
|
|
214
|
-
callContext: CallContextType,
|
|
215
256
|
caller: CallerContext,
|
|
216
257
|
code: () => T,
|
|
217
258
|
) {
|
|
218
259
|
socketContextSeqNum++;
|
|
219
260
|
let seqNum = socketContextSeqNum;
|
|
220
|
-
|
|
221
|
-
curSocketContext.caller = caller;
|
|
261
|
+
SocketFunction.callerContext = caller;
|
|
222
262
|
try {
|
|
223
263
|
return code();
|
|
224
264
|
} finally {
|
|
225
265
|
if (seqNum === socketContextSeqNum) {
|
|
226
|
-
|
|
227
|
-
curSocketContext.caller = undefined;
|
|
266
|
+
SocketFunction.callerContext = undefined;
|
|
228
267
|
}
|
|
229
268
|
}
|
|
230
269
|
}
|
package/SocketFunctionTypes.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module.allowclient = true;
|
|
4
4
|
|
|
5
|
+
import { SocketFunction } from "./SocketFunction";
|
|
5
6
|
import { getCallObj } from "./src/nodeProxy";
|
|
6
7
|
import { Args, MaybePromise } from "./src/types";
|
|
7
8
|
|
|
@@ -21,14 +22,14 @@ export type SocketExposedInterfaceClass = {
|
|
|
21
22
|
new(): unknown;
|
|
22
23
|
prototype: unknown;
|
|
23
24
|
};
|
|
24
|
-
export interface SocketExposedShape<ExposedType extends SocketExposedInterface = SocketExposedInterface
|
|
25
|
+
export interface SocketExposedShape<ExposedType extends SocketExposedInterface = SocketExposedInterface> {
|
|
25
26
|
[functionName: string]: {
|
|
26
27
|
/** Indicates with the same input, we give the same output, forever,
|
|
27
28
|
* independent of code changes. This only works for data storage.
|
|
28
29
|
*/
|
|
29
30
|
dataImmutable?: boolean;
|
|
30
|
-
hooks?: SocketFunctionHook<ExposedType
|
|
31
|
-
clientHooks?: SocketFunctionClientHook<ExposedType
|
|
31
|
+
hooks?: SocketFunctionHook<ExposedType>[];
|
|
32
|
+
clientHooks?: SocketFunctionClientHook<ExposedType>[];
|
|
32
33
|
};
|
|
33
34
|
}
|
|
34
35
|
|
|
@@ -41,32 +42,28 @@ export interface FullCallType extends CallType {
|
|
|
41
42
|
nodeId: string;
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
export interface SocketFunctionHook<ExposedType extends SocketExposedInterface = SocketExposedInterface
|
|
45
|
-
(config: HookContext<ExposedType
|
|
45
|
+
export interface SocketFunctionHook<ExposedType extends SocketExposedInterface = SocketExposedInterface> {
|
|
46
|
+
(config: HookContext<ExposedType>): MaybePromise<void>;
|
|
46
47
|
/** NOTE: This is useful when we need a clientside hook to set up state specifically for our serverside hook. */
|
|
47
|
-
clientHook?: SocketFunctionClientHook<ExposedType
|
|
48
|
+
clientHook?: SocketFunctionClientHook<ExposedType>;
|
|
48
49
|
}
|
|
49
|
-
export type HookContext<ExposedType extends SocketExposedInterface = SocketExposedInterface
|
|
50
|
+
export type HookContext<ExposedType extends SocketExposedInterface = SocketExposedInterface> = {
|
|
50
51
|
call: FullCallType;
|
|
51
|
-
context: SocketRegistered<ExposedType, CallContext>["context"];
|
|
52
52
|
// If the result is overriden, we continue evaluating hooks BUT DO NOT perform the final call
|
|
53
53
|
overrideResult?: unknown;
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
-
export type ClientHookContext<ExposedType extends SocketExposedInterface = SocketExposedInterface
|
|
56
|
+
export type ClientHookContext<ExposedType extends SocketExposedInterface = SocketExposedInterface> = {
|
|
57
57
|
call: FullCallType;
|
|
58
58
|
// If the result is overriden, we continue evaluating hooks BUT DO NOT perform the final call
|
|
59
59
|
overrideResult?: unknown;
|
|
60
|
+
connectionId: { nodeId: string };
|
|
60
61
|
};
|
|
61
|
-
export interface SocketFunctionClientHook<ExposedType extends SocketExposedInterface = SocketExposedInterface
|
|
62
|
-
(config: ClientHookContext<ExposedType
|
|
62
|
+
export interface SocketFunctionClientHook<ExposedType extends SocketExposedInterface = SocketExposedInterface> {
|
|
63
|
+
(config: ClientHookContext<ExposedType>): MaybePromise<void>;
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
export
|
|
66
|
-
[key: string]: unknown;
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
export interface SocketRegistered<ExposedType = any, DynamicCallContext extends CallContextType = CallContextType> {
|
|
66
|
+
export interface SocketRegistered<ExposedType = any> {
|
|
70
67
|
nodes: {
|
|
71
68
|
// NOTE: Don't pass around nodeId to other nodes, instead pass around NetworkLocation (which they
|
|
72
69
|
// then turn into a nodeId, which they can then check permissions on themself).
|
|
@@ -76,12 +73,6 @@ export interface SocketRegistered<ExposedType = any, DynamicCallContext extends
|
|
|
76
73
|
}
|
|
77
74
|
};
|
|
78
75
|
};
|
|
79
|
-
context: {
|
|
80
|
-
// If undefined we are not synchronously in a call
|
|
81
|
-
curContext: DynamicCallContext | undefined;
|
|
82
|
-
caller: CallerContext | undefined;
|
|
83
|
-
getCaller(): CallerContext;
|
|
84
|
-
};
|
|
85
76
|
_classGuid: string;
|
|
86
77
|
}
|
|
87
78
|
export type CallerContext = Readonly<CallerContextBase>;
|
|
@@ -95,5 +86,6 @@ export type CallerContextBase = {
|
|
|
95
86
|
// The nodeId they contacted. This is useful to determine their intention (otherwise
|
|
96
87
|
// requests can be redirected to us and would accept them, even though they are being
|
|
97
88
|
// blatantly MITMed).
|
|
89
|
+
// IF they are the server, calling us back, then this will just be ""
|
|
98
90
|
localNodeId: string;
|
|
99
91
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "socket-function",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.38",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"mobx": "^6.6.2",
|
|
10
10
|
"node-forge": "https://github.com/sliftist/forge#name",
|
|
11
11
|
"preact": "^10.10.6",
|
|
12
|
-
"
|
|
12
|
+
"rdtsc-now": "^0.3.0",
|
|
13
|
+
"typenode": "^4.9.4-b",
|
|
13
14
|
"ws": "^8.8.0"
|
|
14
15
|
},
|
|
15
16
|
"scripts": {
|
|
@@ -21,6 +22,7 @@
|
|
|
21
22
|
"@types/node-forge": "^1.3.1",
|
|
22
23
|
"@types/ws": "^8.5.3",
|
|
23
24
|
"debugbreak": "^0.6.5",
|
|
25
|
+
"pegjs": "^0.10.0",
|
|
24
26
|
"typedev": "^0.1.1"
|
|
25
27
|
}
|
|
26
28
|
}
|
package/require/require.js
CHANGED
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
global: window,
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
|
|
20
19
|
// Not real modules, as we just define their exports
|
|
21
20
|
const builtInModuleExports = {
|
|
22
21
|
worker_threads: {
|
|
@@ -253,6 +252,7 @@
|
|
|
253
252
|
if (property === "__esModule") return undefined;
|
|
254
253
|
// NOTE: Return a toString that evaluates to "" so we can EXPLICITLY detect non-loaded modules
|
|
255
254
|
if (property === unloadedModule) return true;
|
|
255
|
+
if (property === "default") return exportsOverride;
|
|
256
256
|
|
|
257
257
|
throw new Error(`Module ${childId} is serverside only. Tried to access ${property} from ${module.id}`);
|
|
258
258
|
}
|
|
@@ -263,6 +263,7 @@
|
|
|
263
263
|
if (property === "__esModule") return undefined;
|
|
264
264
|
// NOTE: Return a toString that evaluates to "" so we can EXPLICITLY detect non-loaded modules
|
|
265
265
|
if (property === unloadedModule) return true;
|
|
266
|
+
if (property === "default") return exportsOverride;
|
|
266
267
|
|
|
267
268
|
serializedModule;
|
|
268
269
|
|
|
@@ -415,7 +416,12 @@
|
|
|
415
416
|
}
|
|
416
417
|
__setModuleDefault(result, mod);
|
|
417
418
|
return result;
|
|
418
|
-
}
|
|
419
|
+
},
|
|
420
|
+
__importDefault(mod) {
|
|
421
|
+
// If typescript isn't going to complain about importing from a module with no default export,
|
|
422
|
+
// then we'll just change our implementation to work the same way as typescript types...
|
|
423
|
+
return mod.default ? mod : { default: mod };
|
|
424
|
+
},
|
|
419
425
|
},
|
|
420
426
|
module.exports,
|
|
421
427
|
module.require,
|
package/src/CallFactory.ts
CHANGED
|
@@ -9,6 +9,10 @@ import * as tls from "tls";
|
|
|
9
9
|
import { getClientNodeId, getNodeIdLocation, registerNodeClient } from "./nodeCache";
|
|
10
10
|
import debugbreak from "debugbreak";
|
|
11
11
|
import { lazy } from "./caching";
|
|
12
|
+
import { JSONLACKS } from "./JSONLACKS/JSONLACKS";
|
|
13
|
+
import { red } from "./formatting/logColors";
|
|
14
|
+
|
|
15
|
+
const MIN_RETRY_DELAY = 1000;
|
|
12
16
|
|
|
13
17
|
type InternalCallType = FullCallType & {
|
|
14
18
|
seqNum: number;
|
|
@@ -28,10 +32,14 @@ type InternalReturnType = {
|
|
|
28
32
|
|
|
29
33
|
export interface CallFactory {
|
|
30
34
|
nodeId: string;
|
|
35
|
+
lastClosed: number;
|
|
36
|
+
closedForever?: boolean;
|
|
37
|
+
isConnected?: boolean;
|
|
31
38
|
// NOTE: May or may not have reconnection or retry logic inside of performCall.
|
|
32
39
|
// Trigger performLocalCall on the other side of the connection
|
|
33
40
|
performCall(call: CallType): Promise<unknown>;
|
|
34
|
-
|
|
41
|
+
onNextDisconnect(callback: () => void): void;
|
|
42
|
+
connectionId: { nodeId: string };
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
export interface SenderInterface {
|
|
@@ -51,15 +59,17 @@ export interface SenderInterface {
|
|
|
51
59
|
|
|
52
60
|
export async function createCallFactory(
|
|
53
61
|
webSocketBase: SenderInterface | undefined,
|
|
62
|
+
// The node id we are connecting to (or that connected to us)
|
|
54
63
|
nodeId: string,
|
|
55
|
-
|
|
64
|
+
// The node id that we were contacted on
|
|
65
|
+
localNodeId = "",
|
|
56
66
|
): Promise<CallFactory> {
|
|
57
67
|
let niceConnectionName = nodeId;
|
|
58
68
|
|
|
59
69
|
const createWebsocket = createWebsocketFactory();
|
|
60
70
|
const registerOnce = lazy(() => registerNodeClient(callFactory));
|
|
61
71
|
|
|
62
|
-
|
|
72
|
+
const canReconnect = !!getNodeIdLocation(nodeId);
|
|
63
73
|
|
|
64
74
|
let pendingCalls: Map<number, {
|
|
65
75
|
data: Buffer;
|
|
@@ -69,21 +79,26 @@ export async function createCallFactory(
|
|
|
69
79
|
// NOTE: It is important to make this as random as possible, to prevent
|
|
70
80
|
// reconnections dues to a process being reset causing seqNum collisions
|
|
71
81
|
// in return calls.
|
|
72
|
-
let nextSeqNum = Math.random();
|
|
82
|
+
let nextSeqNum = Date.now() + Math.random();
|
|
83
|
+
|
|
84
|
+
let lastConnectionAttempt = 0;
|
|
73
85
|
|
|
74
86
|
let callerContext: CallerContextBase = {
|
|
75
87
|
nodeId,
|
|
76
88
|
localNodeId
|
|
77
89
|
};
|
|
78
90
|
|
|
91
|
+
let disconnectCallbacks: (() => void)[] = [];
|
|
92
|
+
function onNextDisconnect(callback: () => void): void {
|
|
93
|
+
disconnectCallbacks.push(callback);
|
|
94
|
+
}
|
|
95
|
+
|
|
79
96
|
let callFactory: CallFactory = {
|
|
80
97
|
nodeId,
|
|
81
|
-
|
|
98
|
+
lastClosed: 0,
|
|
99
|
+
connectionId: { nodeId },
|
|
100
|
+
onNextDisconnect,
|
|
82
101
|
async performCall(call: CallType) {
|
|
83
|
-
if (callFactory.closedForever) {
|
|
84
|
-
throw new Error(`Connection lost to ${niceConnectionName}`);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
102
|
let seqNum = nextSeqNum++;
|
|
88
103
|
let fullCall: InternalCallType = {
|
|
89
104
|
nodeId,
|
|
@@ -94,7 +109,12 @@ export async function createCallFactory(
|
|
|
94
109
|
seqNum,
|
|
95
110
|
compress: !!SocketFunction.compression,
|
|
96
111
|
};
|
|
97
|
-
let
|
|
112
|
+
let time = Date.now();
|
|
113
|
+
let data = Buffer.from(JSONLACKS.stringify(fullCall));
|
|
114
|
+
time = Date.now() - time;
|
|
115
|
+
if (time > SocketFunction.WIRE_WARN_TIME) {
|
|
116
|
+
console.log(red(`Slow serialize, took ${time}ms to serialize ${data.byteLength} bytes. For ${call.classGuid}.${call.functionName}`));
|
|
117
|
+
}
|
|
98
118
|
let resultPromise = new Promise((resolve, reject) => {
|
|
99
119
|
let callback = (result: InternalReturnType) => {
|
|
100
120
|
if (SocketFunction.logMessages) {
|
|
@@ -110,6 +130,10 @@ export async function createCallFactory(
|
|
|
110
130
|
pendingCalls.set(seqNum, { callback, data, call: fullCall });
|
|
111
131
|
});
|
|
112
132
|
|
|
133
|
+
if (data.byteLength > SocketFunction.MAX_MESSAGE_SIZE * 1.5) {
|
|
134
|
+
throw new Error(`Call too large to send (${call.classGuid}.${call.functionName}, size: ${data.byteLength} > ${SocketFunction.MAX_MESSAGE_SIZE}). If you need to handle very large static data use some external service, such as Backblaze B2 or AWS S3. Or consider fragmenting data at an application level, because sending large data will cause large lag spikes for other clients using this server. Or, if absolutely required, set SocketFunction.MAX_MESSAGE_SIZE to a higher value.`);
|
|
135
|
+
}
|
|
136
|
+
|
|
113
137
|
await send(data);
|
|
114
138
|
|
|
115
139
|
return await resultPromise;
|
|
@@ -126,7 +150,12 @@ export async function createCallFactory(
|
|
|
126
150
|
registerOnce();
|
|
127
151
|
|
|
128
152
|
function onClose(error: string) {
|
|
153
|
+
callFactory.connectionId = { nodeId };
|
|
154
|
+
callFactory.lastClosed = Date.now();
|
|
129
155
|
webSocketPromise = undefined;
|
|
156
|
+
if (!canReconnect) {
|
|
157
|
+
callFactory.closedForever = true;
|
|
158
|
+
}
|
|
130
159
|
for (let [key, call] of pendingCalls) {
|
|
131
160
|
pendingCalls.delete(key);
|
|
132
161
|
call.callback({
|
|
@@ -138,6 +167,14 @@ export async function createCallFactory(
|
|
|
138
167
|
compressed: false,
|
|
139
168
|
});
|
|
140
169
|
}
|
|
170
|
+
|
|
171
|
+
let callbacks = disconnectCallbacks;
|
|
172
|
+
disconnectCallbacks = [];
|
|
173
|
+
for (let callback of callbacks) {
|
|
174
|
+
try {
|
|
175
|
+
callback();
|
|
176
|
+
} catch { }
|
|
177
|
+
}
|
|
141
178
|
}
|
|
142
179
|
|
|
143
180
|
newWebSocket.addEventListener("error", e => {
|
|
@@ -158,7 +195,10 @@ export async function createCallFactory(
|
|
|
158
195
|
if (newWebSocket.readyState === 0 /* CONNECTING */) {
|
|
159
196
|
await new Promise<void>(resolve => {
|
|
160
197
|
newWebSocket.addEventListener("open", () => {
|
|
161
|
-
|
|
198
|
+
if (!SocketFunction.silent) {
|
|
199
|
+
console.log(`Connection established to ${niceConnectionName}`);
|
|
200
|
+
}
|
|
201
|
+
callFactory.isConnected = true;
|
|
162
202
|
resolve();
|
|
163
203
|
});
|
|
164
204
|
newWebSocket.addEventListener("close", () => resolve());
|
|
@@ -166,15 +206,29 @@ export async function createCallFactory(
|
|
|
166
206
|
});
|
|
167
207
|
} else if (newWebSocket.readyState !== 1 /* OPEN */) {
|
|
168
208
|
onClose(`Websocket received in closed state`);
|
|
209
|
+
callFactory.isConnected = true;
|
|
169
210
|
}
|
|
170
211
|
}
|
|
171
212
|
|
|
172
213
|
async function send(data: Buffer) {
|
|
173
|
-
|
|
214
|
+
if (!webSocketPromise) {
|
|
215
|
+
if (canReconnect) {
|
|
216
|
+
webSocketPromise = tryToReconnect();
|
|
217
|
+
} else {
|
|
218
|
+
throw new Error(`Cannot send data to ${niceConnectionName} as the connection has closed`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
174
221
|
let webSocket = await webSocketPromise;
|
|
175
222
|
webSocket.send(data);
|
|
176
223
|
}
|
|
177
224
|
async function tryToReconnect(): Promise<SenderInterface> {
|
|
225
|
+
// Don't try to reconnect too often!
|
|
226
|
+
let timeSinceLastAttempt = Date.now() - lastConnectionAttempt;
|
|
227
|
+
if (timeSinceLastAttempt < MIN_RETRY_DELAY) {
|
|
228
|
+
await new Promise(r => setTimeout(r, MIN_RETRY_DELAY - timeSinceLastAttempt));
|
|
229
|
+
}
|
|
230
|
+
lastConnectionAttempt = Date.now();
|
|
231
|
+
|
|
178
232
|
let newWebSocket = createWebsocket(nodeId);
|
|
179
233
|
await initializeWebsocket(newWebSocket);
|
|
180
234
|
|
|
@@ -210,9 +264,15 @@ export async function createCallFactory(
|
|
|
210
264
|
(message as any) = Buffer.from(arrayBuffer);
|
|
211
265
|
}
|
|
212
266
|
|
|
213
|
-
let
|
|
267
|
+
let time = Date.now();
|
|
268
|
+
let call = JSONLACKS.parse(message.toString(), { extended: false }) as InternalCallType | InternalReturnType;
|
|
269
|
+
time = Date.now() - time;
|
|
270
|
+
|
|
214
271
|
if (call.isReturn) {
|
|
215
272
|
let callbackObj = pendingCalls.get(call.seqNum);
|
|
273
|
+
if (time > SocketFunction.WIRE_WARN_TIME) {
|
|
274
|
+
console.log(red(`Slow parse, took ${time}ms to parse ${message.length} bytes, for receieving result of call to ${callbackObj?.call.classGuid}.${callbackObj?.call.functionName}`));
|
|
275
|
+
}
|
|
216
276
|
if (!callbackObj) {
|
|
217
277
|
console.log(`Got return for unknown call ${call.seqNum}`);
|
|
218
278
|
return;
|
|
@@ -220,11 +280,9 @@ export async function createCallFactory(
|
|
|
220
280
|
call.resultSize = resultSize;
|
|
221
281
|
callbackObj.callback(call);
|
|
222
282
|
} else {
|
|
223
|
-
if (
|
|
224
|
-
console.log(`
|
|
225
|
-
return;
|
|
283
|
+
if (time > SocketFunction.WIRE_WARN_TIME) {
|
|
284
|
+
console.log(red(`Slow parse, took ${time}ms to parse ${message.length} bytes, for call to ${call.classGuid}.${call.functionName}`));
|
|
226
285
|
}
|
|
227
|
-
lastReceivedSeqNum = call.seqNum;
|
|
228
286
|
|
|
229
287
|
let response: InternalReturnType;
|
|
230
288
|
try {
|
|
@@ -250,13 +308,24 @@ export async function createCallFactory(
|
|
|
250
308
|
let result: Buffer;
|
|
251
309
|
if (isNode() && call.compress && SocketFunction.compression?.type === "gzip") {
|
|
252
310
|
response.compressed = true;
|
|
253
|
-
result = Buffer.from(
|
|
311
|
+
result = Buffer.from(JSONLACKS.stringify(response));
|
|
254
312
|
result = await new Promise<Buffer>((resolve, reject) =>
|
|
255
313
|
gzip(result, (err, result) => err ? reject(err) : resolve(result))
|
|
256
314
|
);
|
|
257
315
|
result = Buffer.concat([new Uint8Array([0]), result]);
|
|
258
316
|
} else {
|
|
259
|
-
result = Buffer.from(
|
|
317
|
+
result = Buffer.from(JSONLACKS.stringify(response));
|
|
318
|
+
}
|
|
319
|
+
if (result.byteLength > SocketFunction.MAX_MESSAGE_SIZE * 1.5) {
|
|
320
|
+
response = {
|
|
321
|
+
isReturn: true,
|
|
322
|
+
result: undefined,
|
|
323
|
+
seqNum: call.seqNum,
|
|
324
|
+
error: new Error(`Response too large to send (${call.classGuid}.${call.functionName}, size: ${result.byteLength} > ${SocketFunction.MAX_MESSAGE_SIZE}). If you need to handle very large static data use some external service, such as Backblaze B2 or AWS S3. Or consider fragmenting data at an application level, because sending large data will cause large lag spikes for other clients using this server. Or, if absolutely required, set SocketFunction.MAX_MESSAGE_SIZE to a higher value.`).stack,
|
|
325
|
+
resultSize: resultSize,
|
|
326
|
+
compressed: false,
|
|
327
|
+
};
|
|
328
|
+
result = Buffer.from(JSONLACKS.stringify(response));
|
|
260
329
|
}
|
|
261
330
|
await send(result);
|
|
262
331
|
}
|
|
@@ -264,6 +333,8 @@ export async function createCallFactory(
|
|
|
264
333
|
}
|
|
265
334
|
throw new Error(`Unhandled data type ${typeof message}`);
|
|
266
335
|
} catch (e: any) {
|
|
336
|
+
debugbreak(1);
|
|
337
|
+
debugger;
|
|
267
338
|
console.error(e.stack);
|
|
268
339
|
}
|
|
269
340
|
}
|