socket-function 0.8.37 → 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 +16 -4
- package/SocketFunctionTypes.ts +2 -0
- package/package.json +4 -2
- package/require/require.js +8 -2
- package/src/CallFactory.ts +46 -13
- 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 +8 -5
- 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 +7 -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
|
@@ -28,10 +28,16 @@ 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>();
|
|
@@ -82,14 +88,14 @@ export class SocketFunction {
|
|
|
82
88
|
console.log(`START\t\t\t${classGuid}.${functionName}`);
|
|
83
89
|
}
|
|
84
90
|
try {
|
|
85
|
-
let callFactory = await getCreateCallFactory(nodeId
|
|
91
|
+
let callFactory = await getCreateCallFactory(nodeId);
|
|
86
92
|
|
|
87
93
|
let shapeObj = getShape()[functionName];
|
|
88
94
|
if (!shapeObj) {
|
|
89
95
|
throw new Error(`Function ${functionName} is not in shape`);
|
|
90
96
|
}
|
|
91
97
|
|
|
92
|
-
let hookResult = await runClientHooks(call, shapeObj as SocketExposedShape[""]);
|
|
98
|
+
let hookResult = await runClientHooks(call, shapeObj as SocketExposedShape[""], callFactory.connectionId);
|
|
93
99
|
|
|
94
100
|
if ("overrideResult" in hookResult) {
|
|
95
101
|
return hookResult.overrideResult;
|
|
@@ -191,12 +197,18 @@ export class SocketFunction {
|
|
|
191
197
|
}
|
|
192
198
|
}
|
|
193
199
|
|
|
194
|
-
public static mountedNodeId: string = "
|
|
200
|
+
public static mountedNodeId: string = "";
|
|
201
|
+
public static mountedIP: string = "";
|
|
195
202
|
private static hasMounted = false;
|
|
196
203
|
public static async mount(config: SocketServerConfig) {
|
|
197
|
-
if (this.mountedNodeId
|
|
204
|
+
if (this.mountedNodeId) {
|
|
198
205
|
throw new Error("SocketFunction already mounted, mounting twice in one thread is not allowed.");
|
|
199
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
|
+
}
|
|
200
212
|
this.mountedNodeId = await startSocketServer(config);
|
|
201
213
|
this.hasMounted = true;
|
|
202
214
|
for (let classGuid of SocketFunction.exposedClasses) {
|
package/SocketFunctionTypes.ts
CHANGED
|
@@ -57,6 +57,7 @@ export type ClientHookContext<ExposedType extends SocketExposedInterface = Socke
|
|
|
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
62
|
export interface SocketFunctionClientHook<ExposedType extends SocketExposedInterface = SocketExposedInterface> {
|
|
62
63
|
(config: ClientHookContext<ExposedType>): MaybePromise<void>;
|
|
@@ -85,5 +86,6 @@ export type CallerContextBase = {
|
|
|
85
86
|
// The nodeId they contacted. This is useful to determine their intention (otherwise
|
|
86
87
|
// requests can be redirected to us and would accept them, even though they are being
|
|
87
88
|
// blatantly MITMed).
|
|
89
|
+
// IF they are the server, calling us back, then this will just be ""
|
|
88
90
|
localNodeId: string;
|
|
89
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,8 @@ 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";
|
|
12
14
|
|
|
13
15
|
const MIN_RETRY_DELAY = 1000;
|
|
14
16
|
|
|
@@ -37,6 +39,7 @@ export interface CallFactory {
|
|
|
37
39
|
// Trigger performLocalCall on the other side of the connection
|
|
38
40
|
performCall(call: CallType): Promise<unknown>;
|
|
39
41
|
onNextDisconnect(callback: () => void): void;
|
|
42
|
+
connectionId: { nodeId: string };
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
export interface SenderInterface {
|
|
@@ -56,8 +59,10 @@ export interface SenderInterface {
|
|
|
56
59
|
|
|
57
60
|
export async function createCallFactory(
|
|
58
61
|
webSocketBase: SenderInterface | undefined,
|
|
62
|
+
// The node id we are connecting to (or that connected to us)
|
|
59
63
|
nodeId: string,
|
|
60
|
-
|
|
64
|
+
// The node id that we were contacted on
|
|
65
|
+
localNodeId = "",
|
|
61
66
|
): Promise<CallFactory> {
|
|
62
67
|
let niceConnectionName = nodeId;
|
|
63
68
|
|
|
@@ -66,8 +71,6 @@ export async function createCallFactory(
|
|
|
66
71
|
|
|
67
72
|
const canReconnect = !!getNodeIdLocation(nodeId);
|
|
68
73
|
|
|
69
|
-
let lastReceivedSeqNum = 0;
|
|
70
|
-
|
|
71
74
|
let pendingCalls: Map<number, {
|
|
72
75
|
data: Buffer;
|
|
73
76
|
call: InternalCallType;
|
|
@@ -76,7 +79,7 @@ export async function createCallFactory(
|
|
|
76
79
|
// NOTE: It is important to make this as random as possible, to prevent
|
|
77
80
|
// reconnections dues to a process being reset causing seqNum collisions
|
|
78
81
|
// in return calls.
|
|
79
|
-
let nextSeqNum = Math.random();
|
|
82
|
+
let nextSeqNum = Date.now() + Math.random();
|
|
80
83
|
|
|
81
84
|
let lastConnectionAttempt = 0;
|
|
82
85
|
|
|
@@ -93,6 +96,7 @@ export async function createCallFactory(
|
|
|
93
96
|
let callFactory: CallFactory = {
|
|
94
97
|
nodeId,
|
|
95
98
|
lastClosed: 0,
|
|
99
|
+
connectionId: { nodeId },
|
|
96
100
|
onNextDisconnect,
|
|
97
101
|
async performCall(call: CallType) {
|
|
98
102
|
let seqNum = nextSeqNum++;
|
|
@@ -105,7 +109,12 @@ export async function createCallFactory(
|
|
|
105
109
|
seqNum,
|
|
106
110
|
compress: !!SocketFunction.compression,
|
|
107
111
|
};
|
|
108
|
-
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
|
+
}
|
|
109
118
|
let resultPromise = new Promise((resolve, reject) => {
|
|
110
119
|
let callback = (result: InternalReturnType) => {
|
|
111
120
|
if (SocketFunction.logMessages) {
|
|
@@ -121,6 +130,10 @@ export async function createCallFactory(
|
|
|
121
130
|
pendingCalls.set(seqNum, { callback, data, call: fullCall });
|
|
122
131
|
});
|
|
123
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
|
+
|
|
124
137
|
await send(data);
|
|
125
138
|
|
|
126
139
|
return await resultPromise;
|
|
@@ -137,6 +150,7 @@ export async function createCallFactory(
|
|
|
137
150
|
registerOnce();
|
|
138
151
|
|
|
139
152
|
function onClose(error: string) {
|
|
153
|
+
callFactory.connectionId = { nodeId };
|
|
140
154
|
callFactory.lastClosed = Date.now();
|
|
141
155
|
webSocketPromise = undefined;
|
|
142
156
|
if (!canReconnect) {
|
|
@@ -181,7 +195,9 @@ export async function createCallFactory(
|
|
|
181
195
|
if (newWebSocket.readyState === 0 /* CONNECTING */) {
|
|
182
196
|
await new Promise<void>(resolve => {
|
|
183
197
|
newWebSocket.addEventListener("open", () => {
|
|
184
|
-
|
|
198
|
+
if (!SocketFunction.silent) {
|
|
199
|
+
console.log(`Connection established to ${niceConnectionName}`);
|
|
200
|
+
}
|
|
185
201
|
callFactory.isConnected = true;
|
|
186
202
|
resolve();
|
|
187
203
|
});
|
|
@@ -248,9 +264,15 @@ export async function createCallFactory(
|
|
|
248
264
|
(message as any) = Buffer.from(arrayBuffer);
|
|
249
265
|
}
|
|
250
266
|
|
|
251
|
-
let
|
|
267
|
+
let time = Date.now();
|
|
268
|
+
let call = JSONLACKS.parse(message.toString(), { extended: false }) as InternalCallType | InternalReturnType;
|
|
269
|
+
time = Date.now() - time;
|
|
270
|
+
|
|
252
271
|
if (call.isReturn) {
|
|
253
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
|
+
}
|
|
254
276
|
if (!callbackObj) {
|
|
255
277
|
console.log(`Got return for unknown call ${call.seqNum}`);
|
|
256
278
|
return;
|
|
@@ -258,11 +280,9 @@ export async function createCallFactory(
|
|
|
258
280
|
call.resultSize = resultSize;
|
|
259
281
|
callbackObj.callback(call);
|
|
260
282
|
} else {
|
|
261
|
-
if (
|
|
262
|
-
console.log(`
|
|
263
|
-
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}`));
|
|
264
285
|
}
|
|
265
|
-
lastReceivedSeqNum = call.seqNum;
|
|
266
286
|
|
|
267
287
|
let response: InternalReturnType;
|
|
268
288
|
try {
|
|
@@ -288,13 +308,24 @@ export async function createCallFactory(
|
|
|
288
308
|
let result: Buffer;
|
|
289
309
|
if (isNode() && call.compress && SocketFunction.compression?.type === "gzip") {
|
|
290
310
|
response.compressed = true;
|
|
291
|
-
result = Buffer.from(
|
|
311
|
+
result = Buffer.from(JSONLACKS.stringify(response));
|
|
292
312
|
result = await new Promise<Buffer>((resolve, reject) =>
|
|
293
313
|
gzip(result, (err, result) => err ? reject(err) : resolve(result))
|
|
294
314
|
);
|
|
295
315
|
result = Buffer.concat([new Uint8Array([0]), result]);
|
|
296
316
|
} else {
|
|
297
|
-
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));
|
|
298
329
|
}
|
|
299
330
|
await send(result);
|
|
300
331
|
}
|
|
@@ -302,6 +333,8 @@ export async function createCallFactory(
|
|
|
302
333
|
}
|
|
303
334
|
throw new Error(`Unhandled data type ${typeof message}`);
|
|
304
335
|
} catch (e: any) {
|
|
336
|
+
debugbreak(1);
|
|
337
|
+
debugger;
|
|
305
338
|
console.error(e.stack);
|
|
306
339
|
}
|
|
307
340
|
}
|