socket-function 0.9.4 → 0.9.6
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 +31 -12
- package/SocketFunctionTypes.ts +3 -3
- package/package.json +3 -2
- package/require/RequireController.ts +6 -2
- package/require/require.js +4 -1
- package/src/CallFactory.ts +66 -48
- package/src/JSONLACKS/JSONLACKS.ts +56 -19
- package/src/batching.ts +13 -6
- package/src/caching.ts +44 -6
- package/src/callHTTPHandler.ts +1 -10
- package/src/callManager.ts +3 -3
- package/src/formatting/format.ts +6 -1
- package/src/misc.ts +12 -0
- package/src/nodeCache.ts +6 -0
- package/src/profiling/getOwnTime.ts +18 -52
- package/src/profiling/measure.ts +31 -29
- package/src/profiling/statsFormat.ts +41 -0
- package/src/webSocketServer.ts +6 -4
package/SocketFunction.ts
CHANGED
|
@@ -10,6 +10,12 @@ import { setDefaultHTTPCall } from "./src/callHTTPHandler";
|
|
|
10
10
|
import debugbreak from "debugbreak";
|
|
11
11
|
import { lazy } from "./src/caching";
|
|
12
12
|
import { delay } from "./src/batching";
|
|
13
|
+
import { blue, magenta } from "./src/formatting/logColors";
|
|
14
|
+
import { JSONLACKS } from "./src/JSONLACKS/JSONLACKS";
|
|
15
|
+
import cborx from "cbor-x";
|
|
16
|
+
import { setFlag } from "./require/compileFlags";
|
|
17
|
+
setFlag(require, "cbor-x", "allowclient", true);
|
|
18
|
+
let cborxInstance = new cborx.Encoder({ structuredClone: true });
|
|
13
19
|
|
|
14
20
|
module.allowclient = true;
|
|
15
21
|
|
|
@@ -22,22 +28,23 @@ type ExtractShape<ClassType, Shape> = {
|
|
|
22
28
|
: "Function is in shape, but not in class"
|
|
23
29
|
);
|
|
24
30
|
};
|
|
25
|
-
// https://stackoverflow.com/a/69756175/1117119
|
|
26
|
-
type PickByType<T, Value> = {
|
|
27
|
-
[P in keyof T as T[P] extends Value | undefined ? P : never]: T[P]
|
|
28
|
-
};
|
|
29
31
|
|
|
30
32
|
export class SocketFunction {
|
|
31
33
|
public static logMessages = false;
|
|
32
34
|
|
|
33
35
|
public static MAX_MESSAGE_SIZE = 1024 * 1024 * 32;
|
|
34
|
-
public static compression: undefined | {
|
|
35
|
-
type: "gzip";
|
|
36
|
-
};
|
|
37
36
|
|
|
38
37
|
public static httpETagCache = false;
|
|
39
38
|
public static silent = true;
|
|
40
39
|
|
|
40
|
+
// In retrospect... dynamically changing the wire serializer is a BAD idea. If any calls happen
|
|
41
|
+
// before it is changed, things just break. Also, it needs to be changed on both sides,
|
|
42
|
+
// or else things break. Also, it is very hard to detect when the issue is different serializers
|
|
43
|
+
public static readonly WIRE_SERIALIZER = {
|
|
44
|
+
serialize: (obj: unknown): MaybePromise<Buffer[]> => [cborxInstance.encode(obj)],
|
|
45
|
+
deserialize: (buffers: Buffer[]): MaybePromise<unknown> => cborxInstance.decode(buffers[0]),
|
|
46
|
+
};
|
|
47
|
+
|
|
41
48
|
public static WIRE_WARN_TIME = 100;
|
|
42
49
|
|
|
43
50
|
private static onMountCallbacks = new Map<string, (() => MaybePromise<void>)[]>();
|
|
@@ -54,18 +61,24 @@ export class SocketFunction {
|
|
|
54
61
|
// (ex, using a hook in a controller where the hook also calls the controller).
|
|
55
62
|
public static register<
|
|
56
63
|
ClassInstance extends object,
|
|
57
|
-
Shape extends SocketExposedShape<
|
|
64
|
+
Shape extends SocketExposedShape<{
|
|
65
|
+
[key in keyof ClassInstance]: (...args: any[]) => Promise<unknown>;
|
|
66
|
+
}>,
|
|
58
67
|
>(
|
|
59
68
|
classGuid: string,
|
|
60
69
|
instance: ClassInstance,
|
|
61
70
|
shapeFnc: () => Shape,
|
|
62
71
|
defaultHooksFnc?: () => SocketExposedShape[""] & {
|
|
63
72
|
onMount?: () => MaybePromise<void>;
|
|
73
|
+
},
|
|
74
|
+
config?: {
|
|
75
|
+
/** @noAutoExpose If true SocketFunction.expose(Controller) must be called explicitly. */
|
|
76
|
+
noAutoExpose?: boolean;
|
|
64
77
|
}
|
|
65
78
|
): SocketRegistered<ExtractShape<ClassInstance, Shape>> {
|
|
66
79
|
let getDefaultHooks = defaultHooksFnc && lazy(defaultHooksFnc);
|
|
67
80
|
const getShape = lazy(() => {
|
|
68
|
-
let shape = shapeFnc();
|
|
81
|
+
let shape = shapeFnc() as SocketExposedShape;
|
|
69
82
|
let defaultHooks = getDefaultHooks?.();
|
|
70
83
|
|
|
71
84
|
for (let value of Object.values(shape)) {
|
|
@@ -96,7 +109,7 @@ export class SocketFunction {
|
|
|
96
109
|
throw new Error(`Function ${functionName} is not in shape`);
|
|
97
110
|
}
|
|
98
111
|
|
|
99
|
-
let hookResult = await runClientHooks(call, shapeObj as SocketExposedShape[""], callFactory.connectionId);
|
|
112
|
+
let hookResult = await runClientHooks(call, shapeObj as Exclude<SocketExposedShape[""], undefined>, callFactory.connectionId);
|
|
100
113
|
|
|
101
114
|
if ("overrideResult" in hookResult) {
|
|
102
115
|
return hookResult.overrideResult;
|
|
@@ -128,7 +141,11 @@ export class SocketFunction {
|
|
|
128
141
|
}
|
|
129
142
|
});
|
|
130
143
|
|
|
131
|
-
|
|
144
|
+
let result = output as any as SocketRegistered;
|
|
145
|
+
if (!config?.noAutoExpose) {
|
|
146
|
+
this.expose(result);
|
|
147
|
+
}
|
|
148
|
+
return result;
|
|
132
149
|
}
|
|
133
150
|
|
|
134
151
|
public static onNextDisconnect(nodeId: string, callback: () => void) {
|
|
@@ -185,8 +202,9 @@ export class SocketFunction {
|
|
|
185
202
|
* to add additional imports to ensure the register call runs.
|
|
186
203
|
*/
|
|
187
204
|
public static expose(socketRegistered: SocketRegistered) {
|
|
205
|
+
console.log(blue(`Exposing Controller ${socketRegistered._classGuid}`));
|
|
188
206
|
exposeClass(socketRegistered);
|
|
189
|
-
|
|
207
|
+
this.exposedClasses.add(socketRegistered._classGuid);
|
|
190
208
|
|
|
191
209
|
if (this.hasMounted) {
|
|
192
210
|
let mountCallbacks = SocketFunction.onMountCallbacks.get(socketRegistered._classGuid);
|
|
@@ -199,6 +217,7 @@ export class SocketFunction {
|
|
|
199
217
|
}
|
|
200
218
|
|
|
201
219
|
public static mountedNodeId: string = "";
|
|
220
|
+
public static isMounted() { return !!this.mountedNodeId; }
|
|
202
221
|
public static mountedIP: string = "";
|
|
203
222
|
private static hasMounted = false;
|
|
204
223
|
private static onMountCallback: () => void = () => { };
|
package/SocketFunctionTypes.ts
CHANGED
|
@@ -22,8 +22,8 @@ export type SocketExposedInterfaceClass = {
|
|
|
22
22
|
new(): unknown;
|
|
23
23
|
prototype: unknown;
|
|
24
24
|
};
|
|
25
|
-
export
|
|
26
|
-
[functionName
|
|
25
|
+
export type SocketExposedShape<ExposedType extends SocketExposedInterface = SocketExposedInterface> = {
|
|
26
|
+
[functionName in keyof ExposedType]?: {
|
|
27
27
|
/** Indicates with the same input, we give the same output, forever,
|
|
28
28
|
* independent of code changes. This only works for data storage.
|
|
29
29
|
*/
|
|
@@ -31,7 +31,7 @@ export interface SocketExposedShape<ExposedType extends SocketExposedInterface =
|
|
|
31
31
|
hooks?: SocketFunctionHook<ExposedType>[];
|
|
32
32
|
clientHooks?: SocketFunctionClientHook<ExposedType>[];
|
|
33
33
|
};
|
|
34
|
-
}
|
|
34
|
+
};
|
|
35
35
|
|
|
36
36
|
export interface CallType {
|
|
37
37
|
classGuid: string;
|
package/package.json
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "socket-function",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.6",
|
|
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",
|
|
7
7
|
"dependencies": {
|
|
8
|
+
"cbor-x": "^1.5.6",
|
|
8
9
|
"cookie": "^0.5.0",
|
|
9
10
|
"mobx": "^6.6.2",
|
|
10
11
|
"node-forge": "https://github.com/sliftist/forge#name",
|
|
11
12
|
"preact": "^10.10.6",
|
|
12
|
-
"typenode": "^4.9.4-
|
|
13
|
+
"typenode": "^4.9.4-j",
|
|
13
14
|
"ws": "^8.8.0"
|
|
14
15
|
},
|
|
15
16
|
"optionalDependencies": {
|
|
@@ -223,7 +223,7 @@ class RequireControllerBase {
|
|
|
223
223
|
//require(rootPath);
|
|
224
224
|
let clientModule = require.cache[resolvedPath];
|
|
225
225
|
if (!clientModule) {
|
|
226
|
-
clientModule = createNotFoundModule(`Module ${pathRequest} (resolved to ${JSON.stringify(resolvedPath)}) was not included serverside.
|
|
226
|
+
clientModule = createNotFoundModule(`Module ${pathRequest} (resolved to ${JSON.stringify(resolvedPath)}) was not included serverside. Resolved from root dir ${JSON.stringify(this.rootResolvePath)} (set by call to setRequireBootRequire), resolve search paths: ${JSON.stringify(searchPaths)})}`);
|
|
227
227
|
}
|
|
228
228
|
if (!clientModule.allowclient) {
|
|
229
229
|
clientModule = createNotFoundModule(`Module ${pathRequest} (resolved to ${resolvedPath}) is not allowed clientside (set module.allowclient in it, or call setFlag when it is imported).`);
|
|
@@ -249,5 +249,9 @@ export const RequireController = SocketFunction.register(
|
|
|
249
249
|
requireHTML: {},
|
|
250
250
|
bufferJS: {},
|
|
251
251
|
requireJS: {},
|
|
252
|
-
})
|
|
252
|
+
}),
|
|
253
|
+
undefined,
|
|
254
|
+
{
|
|
255
|
+
noAutoExpose: true
|
|
256
|
+
}
|
|
253
257
|
);
|
package/require/require.js
CHANGED
|
@@ -27,12 +27,15 @@
|
|
|
27
27
|
// https://nodejs.org/api/util.html#util_util_inherits_constructor_superconstructor
|
|
28
28
|
inherits(constructor, superConstructor) {
|
|
29
29
|
Object.setPrototypeOf(constructor.prototype, superConstructor.prototype);
|
|
30
|
-
}
|
|
30
|
+
},
|
|
31
|
+
TextDecoder: TextDecoder,
|
|
32
|
+
TextEncoder: TextEncoder,
|
|
31
33
|
},
|
|
32
34
|
buffer: { Buffer },
|
|
33
35
|
stream: {
|
|
34
36
|
// HACK: Needed to get SAX JS to work correctly.
|
|
35
37
|
Stream: function () { },
|
|
38
|
+
Transform: function () { },
|
|
36
39
|
},
|
|
37
40
|
timers: {
|
|
38
41
|
// TODO: Add all members of timers
|
package/src/CallFactory.ts
CHANGED
|
@@ -9,17 +9,16 @@ 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
12
|
import { red, yellow } from "./formatting/logColors";
|
|
14
13
|
import { isSplitableArray, markArrayAsSplitable } from "./fixLargeNetworkCalls";
|
|
15
|
-
import { delay } from "./batching";
|
|
14
|
+
import { delay, runInSerial } from "./batching";
|
|
15
|
+
import { formatNumber, formatTime } from "./formatting/format";
|
|
16
16
|
|
|
17
17
|
const MIN_RETRY_DELAY = 1000;
|
|
18
18
|
|
|
19
19
|
type InternalCallType = FullCallType & {
|
|
20
20
|
seqNum: number;
|
|
21
21
|
isReturn: false;
|
|
22
|
-
compress: boolean;
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
type InternalReturnType = {
|
|
@@ -76,7 +75,7 @@ export async function createCallFactory(
|
|
|
76
75
|
const canReconnect = !!getNodeIdLocation(nodeId);
|
|
77
76
|
|
|
78
77
|
let pendingCalls: Map<number, {
|
|
79
|
-
data: Buffer;
|
|
78
|
+
data: Buffer[];
|
|
80
79
|
call: InternalCallType;
|
|
81
80
|
callback: (resultJSON: InternalReturnType) => void;
|
|
82
81
|
}> = new Map();
|
|
@@ -123,16 +122,22 @@ export async function createCallFactory(
|
|
|
123
122
|
classGuid: call.classGuid,
|
|
124
123
|
functionName: call.functionName,
|
|
125
124
|
seqNum,
|
|
126
|
-
compress: !!SocketFunction.compression,
|
|
127
125
|
};
|
|
128
126
|
let time = Date.now();
|
|
129
|
-
let data
|
|
127
|
+
let data: Buffer[];
|
|
128
|
+
let dataMaybePromise = SocketFunction.WIRE_SERIALIZER.serialize(fullCall);
|
|
129
|
+
if (dataMaybePromise instanceof Promise) {
|
|
130
|
+
data = await dataMaybePromise;
|
|
131
|
+
} else {
|
|
132
|
+
data = dataMaybePromise;
|
|
133
|
+
}
|
|
130
134
|
time = Date.now() - time;
|
|
135
|
+
let size = data.map(x => x.length).reduce((a, b) => a + b, 0);
|
|
131
136
|
if (time > SocketFunction.WIRE_WARN_TIME) {
|
|
132
|
-
console.log(red(`Slow serialize, took ${time}
|
|
137
|
+
console.log(red(`Slow serialize, took ${formatTime(time)} to serialize ${formatNumber(size)} bytes. For ${call.classGuid}.${call.functionName}`));
|
|
133
138
|
}
|
|
134
139
|
|
|
135
|
-
if (
|
|
140
|
+
if (size > SocketFunction.MAX_MESSAGE_SIZE * 1.5) {
|
|
136
141
|
let splitArgIndex = call.args.findIndex(isSplitableArray);
|
|
137
142
|
if (splitArgIndex >= 0) {
|
|
138
143
|
console.log(yellow(`Splitting large call due to large args: ${call.classGuid}.${call.functionName}`));
|
|
@@ -157,7 +162,7 @@ export async function createCallFactory(
|
|
|
157
162
|
return "CALLS_SPLIT_DUE_TO_LARGE_ARGS";
|
|
158
163
|
}
|
|
159
164
|
|
|
160
|
-
throw new Error(`Call too large to send (${call.classGuid}.${call.functionName}, size: ${
|
|
165
|
+
throw new Error(`Call too large to send (${call.classGuid}.${call.functionName}, size: ${formatNumber(size)} > ${formatNumber(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.`);
|
|
161
166
|
}
|
|
162
167
|
|
|
163
168
|
let resultPromise = new Promise((resolve, reject) => {
|
|
@@ -251,7 +256,8 @@ export async function createCallFactory(
|
|
|
251
256
|
}
|
|
252
257
|
}
|
|
253
258
|
|
|
254
|
-
|
|
259
|
+
const BASE_LENGTH_OFFSET = 324_432_461_592_612;
|
|
260
|
+
async function send(data: Buffer[]) {
|
|
255
261
|
if (!webSocketPromise) {
|
|
256
262
|
if (canReconnect) {
|
|
257
263
|
webSocketPromise = tryToReconnect();
|
|
@@ -260,7 +266,10 @@ export async function createCallFactory(
|
|
|
260
266
|
}
|
|
261
267
|
}
|
|
262
268
|
let webSocket = await webSocketPromise;
|
|
263
|
-
webSocket.send(data);
|
|
269
|
+
webSocket.send((data.length + BASE_LENGTH_OFFSET).toString());
|
|
270
|
+
for (let d of data) {
|
|
271
|
+
webSocket.send(d);
|
|
272
|
+
}
|
|
264
273
|
}
|
|
265
274
|
async function tryToReconnect(): Promise<SenderInterface> {
|
|
266
275
|
// Don't try to reconnect too often!
|
|
@@ -276,7 +285,8 @@ export async function createCallFactory(
|
|
|
276
285
|
return newWebSocket;
|
|
277
286
|
}
|
|
278
287
|
|
|
279
|
-
|
|
288
|
+
let pendingCall: { buffers: Buffer[]; targetCount: number; } | undefined;
|
|
289
|
+
let clientsideSerial = runInSerial(async <T>(val: Promise<T>) => val);
|
|
280
290
|
async function onMessage(message: ws.RawData | ws.MessageEvent | string) {
|
|
281
291
|
try {
|
|
282
292
|
if (typeof message === "object" && "data" in message) {
|
|
@@ -284,35 +294,54 @@ export async function createCallFactory(
|
|
|
284
294
|
}
|
|
285
295
|
if (!isNode()) {
|
|
286
296
|
if (message instanceof Blob) {
|
|
287
|
-
|
|
297
|
+
// We need to force the results to be in serial, otherwise strings leapfrog
|
|
298
|
+
// ahead of buffers, which breaks things.
|
|
299
|
+
message = Buffer.from(await clientsideSerial(message.arrayBuffer()));
|
|
300
|
+
} else {
|
|
301
|
+
await clientsideSerial(Promise.resolve());
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (typeof message === "string") {
|
|
305
|
+
let size = parseInt(message);
|
|
306
|
+
if (isNaN(size)) {
|
|
307
|
+
throw new Error(`Invalid message size ${message}`);
|
|
308
|
+
}
|
|
309
|
+
if (size < BASE_LENGTH_OFFSET) {
|
|
310
|
+
throw new Error(`Invalid message size ${message}`);
|
|
288
311
|
}
|
|
312
|
+
size -= BASE_LENGTH_OFFSET;
|
|
313
|
+
if (size > 1000 * 1000) {
|
|
314
|
+
throw new Error(`Invalid message size ${size}`);
|
|
315
|
+
}
|
|
316
|
+
pendingCall = {
|
|
317
|
+
buffers: [],
|
|
318
|
+
targetCount: size,
|
|
319
|
+
};
|
|
320
|
+
return;
|
|
289
321
|
}
|
|
290
|
-
if (message instanceof Buffer
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
// TODO: Add typings for DecompressionStream
|
|
299
|
-
let DecompressionStream = (window as any).DecompressionStream;
|
|
300
|
-
// https://stackoverflow.com/a/68829631/1117119
|
|
301
|
-
let stream = new DecompressionStream("gzip");
|
|
302
|
-
let blob = new Blob([message]);
|
|
303
|
-
let decompressedStream = (await (blob.stream() as any).pipeThrough(stream));
|
|
304
|
-
let arrayBuffer = await new Response(decompressedStream).arrayBuffer();
|
|
305
|
-
(message as any) = Buffer.from(arrayBuffer);
|
|
322
|
+
if (message instanceof Buffer) {
|
|
323
|
+
if (!pendingCall) {
|
|
324
|
+
throw new Error(`Received data without size`);
|
|
325
|
+
}
|
|
326
|
+
pendingCall.buffers.push(message);
|
|
327
|
+
let currentBuffers: Buffer[];
|
|
328
|
+
if (pendingCall.buffers.length !== pendingCall.targetCount) {
|
|
329
|
+
return;
|
|
306
330
|
}
|
|
307
331
|
|
|
332
|
+
currentBuffers = pendingCall.buffers;
|
|
333
|
+
pendingCall = undefined;
|
|
334
|
+
|
|
335
|
+
let resultSize = currentBuffers.map(x => x.length).reduce((a, b) => a + b, 0);
|
|
336
|
+
|
|
308
337
|
let time = Date.now();
|
|
309
|
-
let call =
|
|
338
|
+
let call = await SocketFunction.WIRE_SERIALIZER.deserialize(currentBuffers) as InternalCallType | InternalReturnType;
|
|
310
339
|
time = Date.now() - time;
|
|
311
340
|
|
|
312
341
|
if (call.isReturn) {
|
|
313
342
|
let callbackObj = pendingCalls.get(call.seqNum);
|
|
314
343
|
if (time > SocketFunction.WIRE_WARN_TIME) {
|
|
315
|
-
console.log(red(`Slow parse, took ${time}ms to parse ${
|
|
344
|
+
console.log(red(`Slow parse, took ${time}ms to parse ${resultSize} bytes, for receieving result of call to ${callbackObj?.call.classGuid}.${callbackObj?.call.functionName}`));
|
|
316
345
|
}
|
|
317
346
|
if (!callbackObj) {
|
|
318
347
|
console.log(`Got return for unknown call ${call.seqNum}`);
|
|
@@ -322,7 +351,7 @@ export async function createCallFactory(
|
|
|
322
351
|
callbackObj.callback(call);
|
|
323
352
|
} else {
|
|
324
353
|
if (time > SocketFunction.WIRE_WARN_TIME) {
|
|
325
|
-
console.log(red(`Slow parse, took ${time}ms to parse ${
|
|
354
|
+
console.log(red(`Slow parse, took ${time}ms to parse ${resultSize} bytes, for call to ${call.classGuid}.${call.functionName}`));
|
|
326
355
|
}
|
|
327
356
|
|
|
328
357
|
let response: InternalReturnType;
|
|
@@ -346,27 +375,18 @@ export async function createCallFactory(
|
|
|
346
375
|
};
|
|
347
376
|
}
|
|
348
377
|
|
|
349
|
-
let result: Buffer;
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
result = Buffer.from(JSONLACKS.stringify(response));
|
|
353
|
-
result = await new Promise<Buffer>((resolve, reject) =>
|
|
354
|
-
gzip(result, (err, result) => err ? reject(err) : resolve(result))
|
|
355
|
-
);
|
|
356
|
-
result = Buffer.concat([new Uint8Array([0]), result]);
|
|
357
|
-
} else {
|
|
358
|
-
result = Buffer.from(JSONLACKS.stringify(response));
|
|
359
|
-
}
|
|
360
|
-
if (result.byteLength > SocketFunction.MAX_MESSAGE_SIZE * 1.5) {
|
|
378
|
+
let result: Buffer[] = await SocketFunction.WIRE_SERIALIZER.serialize(response);
|
|
379
|
+
let totalResultSize = result.map(x => x.length).reduce((a, b) => a + b, 0);
|
|
380
|
+
if (totalResultSize > SocketFunction.MAX_MESSAGE_SIZE * 1.5) {
|
|
361
381
|
response = {
|
|
362
382
|
isReturn: true,
|
|
363
383
|
result: undefined,
|
|
364
384
|
seqNum: call.seqNum,
|
|
365
|
-
error: new Error(`Response too large to send (${call.classGuid}.${call.functionName}, size: ${
|
|
385
|
+
error: new Error(`Response too large to send (${call.classGuid}.${call.functionName}, size: ${formatNumber(totalResultSize)} > ${formatNumber(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,
|
|
366
386
|
resultSize: resultSize,
|
|
367
387
|
compressed: false,
|
|
368
388
|
};
|
|
369
|
-
result =
|
|
389
|
+
result = await SocketFunction.WIRE_SERIALIZER.serialize(response);
|
|
370
390
|
}
|
|
371
391
|
await send(result);
|
|
372
392
|
}
|
|
@@ -379,8 +399,6 @@ export async function createCallFactory(
|
|
|
379
399
|
if (e.stack.startsWith("Error: Cannot send data to") && e.stack.includes("as the connection has closed")) {
|
|
380
400
|
// This is fine, just ignore it
|
|
381
401
|
} else {
|
|
382
|
-
debugbreak(2);
|
|
383
|
-
debugger;
|
|
384
402
|
console.error(e.stack);
|
|
385
403
|
}
|
|
386
404
|
}
|
|
@@ -13,23 +13,13 @@ import { delay } from "../batching";
|
|
|
13
13
|
const SERIALIZE_OBJECT_BATCH_COUNT = 1000;
|
|
14
14
|
const PARSE_BYTE_CHUNK_SIZE = 1024 * 1024 * 10;
|
|
15
15
|
|
|
16
|
-
// Supports json and also:
|
|
17
|
-
// - Non quoted field names "{ x: 1 }"?
|
|
18
|
-
// - Trailing commas
|
|
19
|
-
// NOTE: Comma only syntax is not supported, ex, "[,,]", which is an array of length 2 in javascript
|
|
20
|
-
// - Comments on input, but not on output
|
|
21
|
-
// - References
|
|
22
|
-
// - Buffers (just Buffers, not typed arrays)
|
|
23
|
-
// The stringify function always creates valid json, as the syntax for references and buffers
|
|
24
|
-
// will just be special property names and values.
|
|
25
|
-
// NOTE: We don't support Date serialization. Never store Dates, store "number".
|
|
26
|
-
|
|
27
16
|
export interface JSONLACKS_ParseConfig {
|
|
28
17
|
// Defaults to true. Enables parsing of:
|
|
29
18
|
// - Trailing commas
|
|
30
19
|
// - Non-quoted field names (ex, "{ x: 1 }")
|
|
31
20
|
// - Comments (strips them, but doesn't throw)
|
|
32
21
|
extended?: boolean;
|
|
22
|
+
discardMissingReferences?: boolean;
|
|
33
23
|
}
|
|
34
24
|
export interface JSONLACKS_StringifyConfig {
|
|
35
25
|
// If specified, we are allowed to mutate the provided object. Speeds up serialization.
|
|
@@ -41,11 +31,21 @@ interface HydrateState {
|
|
|
41
31
|
visited: Set<unknown>,
|
|
42
32
|
}
|
|
43
33
|
|
|
34
|
+
// Supports json and also:
|
|
35
|
+
// - Non quoted field names "{ x: 1 }"?
|
|
36
|
+
// - Trailing commas
|
|
37
|
+
// NOTE: Comma only syntax is not supported, ex, "[,,]", which is an array of length 2 in javascript
|
|
38
|
+
// - Comments on input, but not on output
|
|
39
|
+
// - References
|
|
40
|
+
// - Buffers (just Buffers, not typed arrays)
|
|
41
|
+
// The stringify function always creates valid json, as the syntax for references and buffers
|
|
42
|
+
// will just be special property names and values.
|
|
43
|
+
// NOTE: We don't support Date serialization. Never store Dates, store "number".
|
|
44
44
|
export class JSONLACKS {
|
|
45
45
|
public static readonly LACKS_KEY = "__JSONLACKS__98cfb4a05fa34d828661cae15b8779ce__";
|
|
46
46
|
|
|
47
47
|
/** If set to true parses non-quoted field names, comments, trailing commas, etc */
|
|
48
|
-
public static EXTENDED_PARSER =
|
|
48
|
+
public static EXTENDED_PARSER = false;
|
|
49
49
|
public static IGNORE_MISSING_REFERENCES = false;
|
|
50
50
|
|
|
51
51
|
@measureFnc
|
|
@@ -68,6 +68,18 @@ export class JSONLACKS {
|
|
|
68
68
|
return Buffer.concat(buffers);
|
|
69
69
|
});
|
|
70
70
|
}
|
|
71
|
+
public static stringifyFileSync(obj: unknown[], config?: JSONLACKS_StringifyConfig): Buffer {
|
|
72
|
+
let serialized = JSONLACKS.escapeSpecialObjects(obj, config) as unknown[];
|
|
73
|
+
return measureBlock(function JSONstringifyAndJoin() {
|
|
74
|
+
let buffers: Buffer[] = [];
|
|
75
|
+
for (let i = 0; i < serialized.length; i += SERIALIZE_OBJECT_BATCH_COUNT) {
|
|
76
|
+
let str = serialized.slice(i, i + SERIALIZE_OBJECT_BATCH_COUNT).map(x => JSON.stringify(x) + "\n").join("");
|
|
77
|
+
buffers.push(Buffer.from(str));
|
|
78
|
+
}
|
|
79
|
+
// Break up into chunks, as string => Buffer i
|
|
80
|
+
return Buffer.concat(buffers);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
71
83
|
// TIMING: Seems to be about 40X slower than JSON.parse unless extended is set to false,
|
|
72
84
|
// then it is about 2X slower (although it depends on the size and complexity of the objects!)
|
|
73
85
|
@measureFnc
|
|
@@ -82,7 +94,7 @@ export class JSONLACKS {
|
|
|
82
94
|
obj = measureBlock(function JSONparse() { return JSON.parse(text); });
|
|
83
95
|
}
|
|
84
96
|
|
|
85
|
-
return JSONLACKS.hydrateSpecialObjects(obj, hydrateState) as T;
|
|
97
|
+
return JSONLACKS.hydrateSpecialObjects(obj, hydrateState, config) as T;
|
|
86
98
|
}
|
|
87
99
|
@measureFnc
|
|
88
100
|
public static async parseLines<T>(buffer: Buffer, config?: JSONLACKS_ParseConfig): Promise<T[]> {
|
|
@@ -119,9 +131,32 @@ export class JSONLACKS {
|
|
|
119
131
|
linesJSON += lines[i];
|
|
120
132
|
}
|
|
121
133
|
linesJSON += "]";
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
134
|
+
if (config?.discardMissingReferences) {
|
|
135
|
+
try {
|
|
136
|
+
let parts = JSONLACKS.parse(linesJSON, config, hydrateState) as T[];
|
|
137
|
+
for (let part of parts) {
|
|
138
|
+
output.push(part);
|
|
139
|
+
}
|
|
140
|
+
} catch (e: any) {
|
|
141
|
+
if (!e.message.includes("Reference to undefined id")) {
|
|
142
|
+
throw e;
|
|
143
|
+
}
|
|
144
|
+
for (let line of lines) {
|
|
145
|
+
try {
|
|
146
|
+
let part = JSONLACKS.parse(line, config, hydrateState) as T;
|
|
147
|
+
output.push(part);
|
|
148
|
+
} catch (e: any) {
|
|
149
|
+
if (!e.message.includes("Reference to undefined id")) {
|
|
150
|
+
throw e;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
let parts = JSONLACKS.parse(linesJSON, config, hydrateState) as T[];
|
|
157
|
+
for (let part of parts) {
|
|
158
|
+
output.push(part);
|
|
159
|
+
}
|
|
125
160
|
}
|
|
126
161
|
}
|
|
127
162
|
while (pos < buffer.length) {
|
|
@@ -225,7 +260,7 @@ export class JSONLACKS {
|
|
|
225
260
|
}
|
|
226
261
|
|
|
227
262
|
@measureFnc
|
|
228
|
-
private static hydrateSpecialObjects(obj: unknown, hydrateState?: HydrateState): unknown {
|
|
263
|
+
private static hydrateSpecialObjects(obj: unknown, hydrateState?: HydrateState, config?: JSONLACKS_ParseConfig): unknown {
|
|
229
264
|
let references = hydrateState?.references || new Map<string, unknown>();
|
|
230
265
|
let visited = hydrateState?.visited || new Set<unknown>();
|
|
231
266
|
return iterate(obj);
|
|
@@ -259,8 +294,10 @@ export class JSONLACKS {
|
|
|
259
294
|
if (type === "ref") {
|
|
260
295
|
let id = obj.id as string;
|
|
261
296
|
if (!JSONLACKS.IGNORE_MISSING_REFERENCES && !references.has(id)) {
|
|
262
|
-
|
|
263
|
-
|
|
297
|
+
if (!config?.discardMissingReferences) {
|
|
298
|
+
debugbreak(2);
|
|
299
|
+
debugger;
|
|
300
|
+
}
|
|
264
301
|
throw new Error(`Reference to undefined id "${id}"`);
|
|
265
302
|
}
|
|
266
303
|
return references.get(id);
|
package/src/batching.ts
CHANGED
|
@@ -113,15 +113,22 @@ export function batchFunction<Arg, Result = void>(
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
export function runInSerial<T extends (...args: any[]) => Promise<any>>(fnc: T): T {
|
|
116
|
-
let updateQueue: (
|
|
116
|
+
let updateQueue: { promise: Promise<void>; resolve: () => void; }[] = [];
|
|
117
117
|
|
|
118
118
|
return (async (...args: any[]) => {
|
|
119
|
+
let promise = {
|
|
120
|
+
promise: null as any as Promise<void>,
|
|
121
|
+
resolve: () => { },
|
|
122
|
+
};
|
|
123
|
+
promise.promise = new Promise<void>(resolve => {
|
|
124
|
+
promise.resolve = resolve;
|
|
125
|
+
});
|
|
119
126
|
const queueWasEmpty = updateQueue.length === 0;
|
|
120
|
-
if (
|
|
121
|
-
|
|
122
|
-
await new Promise<void>(resolve => updateQueue.push(resolve));
|
|
127
|
+
if (queueWasEmpty) {
|
|
128
|
+
promise.resolve();
|
|
123
129
|
}
|
|
124
|
-
updateQueue.push(
|
|
130
|
+
updateQueue.push(promise);
|
|
131
|
+
await promise.promise;
|
|
125
132
|
|
|
126
133
|
try {
|
|
127
134
|
return await fnc(...args);
|
|
@@ -129,7 +136,7 @@ export function runInSerial<T extends (...args: any[]) => Promise<any>>(fnc: T):
|
|
|
129
136
|
// Pop ourself off
|
|
130
137
|
updateQueue.shift();
|
|
131
138
|
// Resolve the next promise
|
|
132
|
-
updateQueue[0]?.();
|
|
139
|
+
updateQueue[0]?.resolve();
|
|
133
140
|
}
|
|
134
141
|
}) as T;
|
|
135
142
|
}
|
package/src/caching.ts
CHANGED
|
@@ -12,6 +12,9 @@ export function lazy<T>(factory: () => T) {
|
|
|
12
12
|
get.reset = () => {
|
|
13
13
|
value = undefined;
|
|
14
14
|
};
|
|
15
|
+
get.set = (newValue: T) => {
|
|
16
|
+
value = { value: newValue };
|
|
17
|
+
};
|
|
15
18
|
return get;
|
|
16
19
|
}
|
|
17
20
|
|
|
@@ -38,8 +41,10 @@ export function cache<Output, Key>(getValue: (key: Key) => Output): {
|
|
|
38
41
|
(key: Key): Output;
|
|
39
42
|
// NOTE: If you want to clear all, just make a new cache!
|
|
40
43
|
clear(key: Key): void;
|
|
44
|
+
clearAll(): void;
|
|
41
45
|
forceSet(key: Key, value: Output): void;
|
|
42
46
|
getAllKeys(): Key[];
|
|
47
|
+
get(key: Key): Output | undefined;
|
|
43
48
|
} {
|
|
44
49
|
let startingCalculating = new Set<Key>();
|
|
45
50
|
let values = new Map<Key, Output>();
|
|
@@ -69,6 +74,13 @@ export function cache<Output, Key>(getValue: (key: Key) => Output): {
|
|
|
69
74
|
cache.getAllKeys = () => {
|
|
70
75
|
return [...values.keys()];
|
|
71
76
|
};
|
|
77
|
+
cache.get = (key: Key) => {
|
|
78
|
+
return values.get(key);
|
|
79
|
+
};
|
|
80
|
+
cache.clearAll = () => {
|
|
81
|
+
values.clear();
|
|
82
|
+
startingCalculating.clear();
|
|
83
|
+
};
|
|
72
84
|
return cache;
|
|
73
85
|
}
|
|
74
86
|
|
|
@@ -114,6 +126,14 @@ export function cacheLimited<Output, Key>(
|
|
|
114
126
|
values.set(key, value);
|
|
115
127
|
startingCalculating.add(key);
|
|
116
128
|
};
|
|
129
|
+
get["clearKey"] = (key: Key) => {
|
|
130
|
+
values.delete(key);
|
|
131
|
+
startingCalculating.delete(key);
|
|
132
|
+
};
|
|
133
|
+
get["clear"] = () => {
|
|
134
|
+
values.clear();
|
|
135
|
+
startingCalculating.clear();
|
|
136
|
+
};
|
|
117
137
|
|
|
118
138
|
return get;
|
|
119
139
|
}
|
|
@@ -186,6 +206,7 @@ export function cacheArrayEqual<Input extends unknown[] | undefined, Output>(
|
|
|
186
206
|
): {
|
|
187
207
|
(array: Input): Output;
|
|
188
208
|
clear(array: Input): void;
|
|
209
|
+
clearAll(): void;
|
|
189
210
|
} {
|
|
190
211
|
let state: {
|
|
191
212
|
cache: {
|
|
@@ -227,7 +248,10 @@ export function cacheArrayEqual<Input extends unknown[] | undefined, Output>(
|
|
|
227
248
|
state.cache.splice(i, 1);
|
|
228
249
|
}
|
|
229
250
|
}
|
|
230
|
-
}
|
|
251
|
+
},
|
|
252
|
+
clearAll() {
|
|
253
|
+
state.cache = [];
|
|
254
|
+
},
|
|
231
255
|
}
|
|
232
256
|
);
|
|
233
257
|
}
|
|
@@ -247,7 +271,7 @@ export function cacheArgsEqual<Fnc extends AnyFunction>(
|
|
|
247
271
|
{
|
|
248
272
|
clear(...args: unknown[]) {
|
|
249
273
|
cache.clear(args);
|
|
250
|
-
}
|
|
274
|
+
},
|
|
251
275
|
}
|
|
252
276
|
);
|
|
253
277
|
}
|
|
@@ -255,13 +279,23 @@ export function cacheArgsEqual<Fnc extends AnyFunction>(
|
|
|
255
279
|
export function cacheJSONArgsEqual<Fnc extends AnyFunction>(
|
|
256
280
|
fnc: Fnc,
|
|
257
281
|
limit = 10
|
|
258
|
-
)
|
|
282
|
+
) {
|
|
259
283
|
let cache = cacheLimited(limit, (argsJSON: string) => {
|
|
260
284
|
return fnc(...JSON.parse(argsJSON));
|
|
261
285
|
});
|
|
262
|
-
return (
|
|
263
|
-
|
|
264
|
-
|
|
286
|
+
return Object.assign(
|
|
287
|
+
((...args: unknown[]) => {
|
|
288
|
+
return cache(JSON.stringify(args));
|
|
289
|
+
}) as Fnc,
|
|
290
|
+
{
|
|
291
|
+
clear(...args: unknown[]) {
|
|
292
|
+
cache.clearKey(JSON.stringify(args));
|
|
293
|
+
},
|
|
294
|
+
clearAll() {
|
|
295
|
+
cache.clear();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
);
|
|
265
299
|
}
|
|
266
300
|
|
|
267
301
|
export function cacheShallowConfigArgEqual<Fnc extends AnyFunction>(
|
|
@@ -269,6 +303,7 @@ export function cacheShallowConfigArgEqual<Fnc extends AnyFunction>(
|
|
|
269
303
|
limit = 10
|
|
270
304
|
): Fnc & {
|
|
271
305
|
clear(...args: Args<Fnc>): void;
|
|
306
|
+
clearAll(): void;
|
|
272
307
|
} {
|
|
273
308
|
let cache = cacheArrayEqual((kvpsFlat: unknown[]) => {
|
|
274
309
|
output.missCount++;
|
|
@@ -301,6 +336,9 @@ export function cacheShallowConfigArgEqual<Fnc extends AnyFunction>(
|
|
|
301
336
|
clear(configArg: object) {
|
|
302
337
|
cache.clear(getKVPs(configArg));
|
|
303
338
|
},
|
|
339
|
+
clearAll() {
|
|
340
|
+
cache.clearAll();
|
|
341
|
+
},
|
|
304
342
|
callCount: 0,
|
|
305
343
|
missCount: 0,
|
|
306
344
|
}
|
package/src/callHTTPHandler.ts
CHANGED
|
@@ -36,7 +36,7 @@ export function getNodeIdsFromRequest(request: http.IncomingMessage) {
|
|
|
36
36
|
// THAT WAY HTTP can have consistent nodeIds, instead of making them randomly every time!
|
|
37
37
|
// (This isn't needed or possible for websockets, but they stay open, so calling functions
|
|
38
38
|
// after they open to set the nodeId is possible, and preferred).
|
|
39
|
-
let remoteAddress = request.socket.remoteAddress;
|
|
39
|
+
let remoteAddress = request.socket.remoteAddress?.split(":").pop();
|
|
40
40
|
if (!remoteAddress) {
|
|
41
41
|
throw new Error(`Missing remoteAddress`);
|
|
42
42
|
}
|
|
@@ -147,15 +147,6 @@ export async function httpCallHandler(request: http.IncomingMessage, response: h
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
let headers = (resultBuffer as HTTPResultType)[resultHeaders];
|
|
150
|
-
if (SocketFunction.compression?.type === "gzip" && !headers?.["Content-Encoding"]) {
|
|
151
|
-
if (request.headers["accept-encoding"]?.includes("gzip")) {
|
|
152
|
-
resultBuffer = await new Promise<Buffer>((resolve, reject) =>
|
|
153
|
-
gzip(resultBuffer, (err, result) => err ? reject(err) : resolve(result))
|
|
154
|
-
);
|
|
155
|
-
response.setHeader("Content-Encoding", "gzip");
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
150
|
|
|
160
151
|
// NOTE: Our ETag caching is only to reduce data sent on the wire, we evaluate the calls
|
|
161
152
|
// every time (so it is strictly a wire cache for HTTP, not a computation cache)
|
package/src/callManager.ts
CHANGED
|
@@ -29,7 +29,7 @@ export async function performLocalCall(
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
if (!exposedClasses.has(call.classGuid)) {
|
|
32
|
-
throw new Error(`Class ${call.classGuid} not exposed`);
|
|
32
|
+
throw new Error(`Class ${call.classGuid} not exposed, exposed classes: ${Array.from(exposedClasses).join(", ")}`);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
let controller = classDef.controller;
|
|
@@ -95,7 +95,7 @@ export function unregisterGlobalClientHook(hook: SocketFunctionClientHook) {
|
|
|
95
95
|
|
|
96
96
|
export const runClientHooks = measureWrap(async function runClientHooks(
|
|
97
97
|
callType: FullCallType,
|
|
98
|
-
hooks: SocketExposedShape[""],
|
|
98
|
+
hooks: Exclude<SocketExposedShape[""], undefined>,
|
|
99
99
|
connectionId: { nodeId: string },
|
|
100
100
|
): Promise<ClientHookContext> {
|
|
101
101
|
let context: ClientHookContext = { call: callType, connectionId };
|
|
@@ -122,7 +122,7 @@ export const runClientHooks = measureWrap(async function runClientHooks(
|
|
|
122
122
|
export const runServerHooks = measureWrap(async function runServerHooks(
|
|
123
123
|
callType: FullCallType,
|
|
124
124
|
caller: CallerContext,
|
|
125
|
-
hooks: SocketExposedShape[""],
|
|
125
|
+
hooks: Exclude<SocketExposedShape[""], undefined>,
|
|
126
126
|
): Promise<HookContext> {
|
|
127
127
|
let hookContext: HookContext = { call: callType };
|
|
128
128
|
for (let hook of globalHooks.concat(hooks.hooks || [])) {
|
package/src/formatting/format.ts
CHANGED
|
@@ -134,7 +134,12 @@ export function formatNumber(count: number | undefined, maxAbsoluteValue?: numbe
|
|
|
134
134
|
// NOTE: We don't switch units as soon as we possible can, because...
|
|
135
135
|
// 3.594 vs 3.584 is harder to quickly distinguish compared to 3594 and 3584,
|
|
136
136
|
// the decimal simply makes it harder to read, and larger.
|
|
137
|
-
|
|
137
|
+
// NOTE: This number should prevent us from ever using 5 digits, so that we never need commas
|
|
138
|
+
// For example, if the factor is 10 then, 9999.5 is kept with a divisor of 1, and then rounds up to
|
|
139
|
+
// "10,000". So we want any value which rounds up at 5 digits to be put in the next group (and having
|
|
140
|
+
// extra values put in the next group is fine, as we won't show the decimal value anyways, so it only
|
|
141
|
+
// means 9999 wraps around to 10K a bit faster).
|
|
142
|
+
const extraFactor = 9.99949999;
|
|
138
143
|
let divisor = 1;
|
|
139
144
|
let suffix = "";
|
|
140
145
|
let currencyDecimalsNeeded = false;
|
package/src/misc.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import * as crypto from "crypto";
|
|
2
2
|
import { canHaveChildren, MaybePromise } from "./types";
|
|
3
3
|
|
|
4
|
+
export const timeInSecond = 1000;
|
|
5
|
+
export const timeInMinute = timeInSecond * 60;
|
|
6
|
+
export const timeInHour = timeInMinute * 60;
|
|
7
|
+
export const timeInDay = timeInHour * 24;
|
|
8
|
+
export const timeInWeek = timeInDay * 7;
|
|
9
|
+
export const timeInYear = timeInDay * 365;
|
|
10
|
+
|
|
4
11
|
export type Watchable<T> = (callback: (value: T) => void) => MaybePromise<void>;
|
|
5
12
|
|
|
6
13
|
export function convertErrorStackToError(error: string): Error {
|
|
@@ -301,4 +308,9 @@ export function arrayFromOrderObject<T>(obj: { [order: number]: T }): T[] {
|
|
|
301
308
|
|
|
302
309
|
export function last<T>(arr: T[]): T | undefined {
|
|
303
310
|
return arr[arr.length - 1];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export type ObjectValues<T> = T[keyof T];
|
|
314
|
+
export function entries<Obj extends { [key: string]: unknown }>(obj: Obj): [keyof Obj, ObjectValues<Obj>][] {
|
|
315
|
+
return Object.entries(obj) as any;
|
|
304
316
|
}
|
package/src/nodeCache.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { CallFactory, createCallFactory } from "./CallFactory";
|
|
|
2
2
|
import { MaybePromise } from "./types";
|
|
3
3
|
import { lazy } from "./caching";
|
|
4
4
|
import { SocketFunction } from "../SocketFunction";
|
|
5
|
+
import { isNode } from "./misc";
|
|
5
6
|
|
|
6
7
|
// TODO: Add CallInstanceFactory.isClosed, so nodeCache can clean up old entries.
|
|
7
8
|
// This is only needed for memory management, and not for correctness. Entries never
|
|
@@ -17,6 +18,11 @@ export function getNodeId(domain: string, port: number): string {
|
|
|
17
18
|
return `${domain}:${port}`;
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
export function getNodeIdFromLocation() {
|
|
22
|
+
if (isNode()) throw new Error(`Cannot get nodeId from location, as we are running in NodeJS`);
|
|
23
|
+
return getNodeId(location.hostname, location.port ? parseInt(location.port) : 443);
|
|
24
|
+
}
|
|
25
|
+
|
|
20
26
|
/** A nodeId not available for reconnecting. */
|
|
21
27
|
export function getClientNodeId(address: string): string {
|
|
22
28
|
return `client:${address}:${Date.now()}:${Math.random()}`;
|
|
@@ -8,39 +8,18 @@ export type OwnTimeObj = {
|
|
|
8
8
|
time: number;
|
|
9
9
|
ownTime: number;
|
|
10
10
|
};
|
|
11
|
-
type OwnTimeObjInternal = OwnTimeObj & {
|
|
11
|
+
export type OwnTimeObjInternal = OwnTimeObj & {
|
|
12
12
|
lastStartTime: number;
|
|
13
13
|
firstStartTime: number;
|
|
14
|
-
parent: OwnTimeObjInternal | undefined;
|
|
15
|
-
child: OwnTimeObjInternal | undefined;
|
|
16
14
|
};
|
|
17
15
|
|
|
18
|
-
let
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (!instances) return undefined;
|
|
23
|
-
if (!pendingCallTime) return undefined;
|
|
24
|
-
let results = instances.map((instance) => ({
|
|
25
|
-
name: instance.name,
|
|
26
|
-
ownTime: instance.ownTime,
|
|
27
|
-
time: time - instance.firstStartTime,
|
|
28
|
-
source: instance
|
|
29
|
-
}));
|
|
30
|
-
results[0].ownTime += time - pendingCallTime.lastStartTime;
|
|
31
|
-
return results;
|
|
32
|
-
}
|
|
33
|
-
export function getPendingOwnTimeInstances(): OwnTimeObjInternal[] | undefined {
|
|
34
|
-
if (!pendingCallTime) return undefined;
|
|
35
|
-
let results: OwnTimeObjInternal[] = [];
|
|
36
|
-
let current: OwnTimeObjInternal | undefined = pendingCallTime;
|
|
37
|
-
while (current) {
|
|
38
|
-
results.push(current);
|
|
39
|
-
current = current.parent;
|
|
40
|
-
}
|
|
41
|
-
return results;
|
|
16
|
+
let openTimes: OwnTimeObjInternal[] = [];
|
|
17
|
+
|
|
18
|
+
export function getOpenTimesBase(): OwnTimeObjInternal[] {
|
|
19
|
+
return openTimes;
|
|
42
20
|
}
|
|
43
|
-
|
|
21
|
+
|
|
22
|
+
(global as any).pendingOwnCallTime = openTimes;
|
|
44
23
|
|
|
45
24
|
// NOTE: This overhead time is actually mostly for aggregate time, but it is needed,
|
|
46
25
|
// otherwise we consistently underestimate the time spent.
|
|
@@ -73,6 +52,7 @@ let addMeasureOverheadTime = 0;
|
|
|
73
52
|
|
|
74
53
|
// TIMING: About 60ns, of which 40ns is just now() calls.
|
|
75
54
|
// If async is closer to 300ns.
|
|
55
|
+
// NOTE: Handles promises correctly
|
|
76
56
|
export function getOwnTime<T>(
|
|
77
57
|
name: string,
|
|
78
58
|
code: () => T,
|
|
@@ -85,39 +65,25 @@ export function getOwnTime<T>(
|
|
|
85
65
|
ownTime: 0,
|
|
86
66
|
firstStartTime: time,
|
|
87
67
|
lastStartTime: time,
|
|
88
|
-
parent: pendingCallTime,
|
|
89
|
-
child: undefined,
|
|
90
68
|
};
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
pendingCallTime = obj;
|
|
95
|
-
if (obj.parent) {
|
|
96
|
-
obj.parent.ownTime += obj.lastStartTime - obj.parent.lastStartTime;
|
|
69
|
+
let prevOwnTime = openTimes[openTimes.length - 1];
|
|
70
|
+
if (prevOwnTime) {
|
|
71
|
+
prevOwnTime.ownTime += time - prevOwnTime.lastStartTime;
|
|
97
72
|
}
|
|
73
|
+
openTimes.push(obj);
|
|
98
74
|
|
|
99
75
|
function finish() {
|
|
100
76
|
let time = now();
|
|
101
77
|
obj.time = time - obj.firstStartTime;
|
|
102
|
-
if (
|
|
103
|
-
// Good case, all of our children call ended before us.
|
|
104
|
-
|
|
105
|
-
// End our own time calculation
|
|
78
|
+
if (obj === openTimes[openTimes.length - 1]) {
|
|
106
79
|
obj.ownTime += time - obj.lastStartTime;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (pendingCallTime) {
|
|
111
|
-
// Resume our parent ownTime counting
|
|
112
|
-
pendingCallTime.lastStartTime = time;
|
|
80
|
+
let newOwnTime = openTimes[openTimes.length - 2];
|
|
81
|
+
if (newOwnTime) {
|
|
82
|
+
newOwnTime.lastStartTime = time;
|
|
113
83
|
}
|
|
114
84
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
obj.parent.child = obj.child;
|
|
118
|
-
}
|
|
119
|
-
obj.parent = undefined;
|
|
120
|
-
obj.child = undefined;
|
|
85
|
+
let index = openTimes.indexOf(obj);
|
|
86
|
+
openTimes.splice(index, 1);
|
|
121
87
|
|
|
122
88
|
obj.time += addMeasureOverheadTime;
|
|
123
89
|
obj.ownTime += addMeasureOverheadTime;
|
package/src/profiling/measure.ts
CHANGED
|
@@ -2,12 +2,16 @@ import debugbreak from "debugbreak";
|
|
|
2
2
|
import { formatTime, formatNumber } from "../formatting/format";
|
|
3
3
|
import { red, yellow, blue, magenta } from "../formatting/logColors";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { getOpenTimesBase, getOwnTime, OwnTimeObj } from "./getOwnTime";
|
|
6
6
|
import { addToStats, addToStatsValue, createStatsValue, getStatsTop, StatsValue } from "./stats";
|
|
7
7
|
import { white } from "../formatting/logColors";
|
|
8
8
|
import { isNode } from "../misc";
|
|
9
|
+
import { formatStats } from "./statsFormat";
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
let measurementsDisabled = false;
|
|
12
|
+
/** NOTE: Must be called BEFORE anything else is imported!
|
|
13
|
+
* NOTE: Measurements on on by default now, so this doesn't really need to be called...
|
|
14
|
+
*/
|
|
11
15
|
export function enableMeasurements() {
|
|
12
16
|
if (functionsSkipped) {
|
|
13
17
|
console.warn(red(`Skipped measure shimming ${functionsSkipped} functions. Fix this by calling enableMeasurements before any other imports.`));
|
|
@@ -17,6 +21,7 @@ export function enableMeasurements() {
|
|
|
17
21
|
/** NOTE: Must be called BEFORE anything else is imported! */
|
|
18
22
|
export function disableMeasurements() {
|
|
19
23
|
measurementsEnabled = false;
|
|
24
|
+
measurementsDisabled = true;
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
let functionsSkipped = 0;
|
|
@@ -28,6 +33,7 @@ const AsyncFunction = (async () => { }).constructor;
|
|
|
28
33
|
// TIMING: 1-5us. I have seen timing values greatly vary, but it does seem to be quite high, despite
|
|
29
34
|
// microbenchmarks saying it is slow. Perhaps it is because getOwnTime breaks the cpu pipeline,
|
|
30
35
|
// which causes slowness for code around us, but not if we are running in isolation?
|
|
36
|
+
// NOTE: Handles promises correctly
|
|
31
37
|
export function measureFnc(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
|
32
38
|
let name = propertyKey;
|
|
33
39
|
if (target.name) {
|
|
@@ -49,6 +55,7 @@ export function nameFunction<T extends Function>(name: string, fnc: T) {
|
|
|
49
55
|
Object.defineProperty(fnc, "name", { value: name });
|
|
50
56
|
return fnc;
|
|
51
57
|
}
|
|
58
|
+
// NOTE: Handles promises correctly
|
|
52
59
|
export function measureWrap<T extends (...args: any[]) => any>(fnc: T, name?: string): T {
|
|
53
60
|
if (!measurementsEnabled) {
|
|
54
61
|
functionsSkipped++;
|
|
@@ -69,21 +76,31 @@ export function measureBlock<T extends (...args: any[]) => any>(fnc: T, name?: s
|
|
|
69
76
|
export function startMeasure(): {
|
|
70
77
|
finish: () => MeasureProfile;
|
|
71
78
|
} {
|
|
72
|
-
if (!measurementsEnabled) {
|
|
79
|
+
if (!measurementsEnabled && !measurementsDisabled) {
|
|
73
80
|
console.warn(red(`To capture measurements enableMeasurements() must be called before any other imports in your entry point`));
|
|
74
81
|
}
|
|
75
82
|
let profile: MeasureProfile = {
|
|
76
83
|
entries: Object.create(null),
|
|
77
84
|
};
|
|
78
|
-
let openAtStart = new Set(
|
|
85
|
+
let openAtStart = new Set(getOpenTimesBase());
|
|
79
86
|
|
|
80
87
|
outstandingProfiles.push(profile);
|
|
81
88
|
return {
|
|
82
89
|
finish() {
|
|
83
|
-
let pending =
|
|
90
|
+
let pending = getOpenTimesBase();
|
|
91
|
+
let last = pending[pending.length - 1];
|
|
92
|
+
let time = Date.now();
|
|
84
93
|
for (let timeObj of pending) {
|
|
85
|
-
|
|
86
|
-
|
|
94
|
+
// Ignore any values that were already open, as they are clearly not
|
|
95
|
+
// caused by our code.
|
|
96
|
+
if (openAtStart.has(timeObj)) continue;
|
|
97
|
+
timeObj = { ...timeObj };
|
|
98
|
+
|
|
99
|
+
if (timeObj === last) {
|
|
100
|
+
timeObj.ownTime += time - timeObj.lastStartTime;
|
|
101
|
+
}
|
|
102
|
+
timeObj.time = time - timeObj.firstStartTime;
|
|
103
|
+
addToProfile(profile, timeObj);
|
|
87
104
|
}
|
|
88
105
|
outstandingProfiles.splice(outstandingProfiles.indexOf(profile), 1);
|
|
89
106
|
return profile;
|
|
@@ -96,6 +113,8 @@ export interface LogMeasureTableConfig {
|
|
|
96
113
|
name?: string;
|
|
97
114
|
// Defaults to 0.05
|
|
98
115
|
thresholdInTable?: number;
|
|
116
|
+
// Details to 50
|
|
117
|
+
minTimeToLog?: number;
|
|
99
118
|
}
|
|
100
119
|
|
|
101
120
|
export function logMeasureTable(
|
|
@@ -106,6 +125,7 @@ export function logMeasureTable(
|
|
|
106
125
|
if (thresholdInTable === undefined) {
|
|
107
126
|
thresholdInTable = 0.05;
|
|
108
127
|
}
|
|
128
|
+
let minTimeToLog = config?.minTimeToLog ?? 50;
|
|
109
129
|
|
|
110
130
|
function getTime(entry: ProfileEntry) {
|
|
111
131
|
return useTotalTime ? entry.totalTime : entry.ownTime;
|
|
@@ -114,6 +134,7 @@ export function logMeasureTable(
|
|
|
114
134
|
entries.sort((a, b) => getTime(b).sum - getTime(a).sum);
|
|
115
135
|
|
|
116
136
|
let totalTime = entries.map(x => getTime(x).sum).reduce((a, b) => a + b, 0);
|
|
137
|
+
if (totalTime < minTimeToLog) return;
|
|
117
138
|
|
|
118
139
|
console.log();
|
|
119
140
|
let title = yellow(`Profiled ${formatTime(totalTime)} (logged at ${new Date().toISOString()})`);
|
|
@@ -138,27 +159,8 @@ export function logMeasureTable(
|
|
|
138
159
|
return String(text).padStart(count, " ");
|
|
139
160
|
}
|
|
140
161
|
let fractionText = percent(getTime(entry).sum / totalTime);
|
|
141
|
-
|
|
142
|
-
let
|
|
143
|
-
let sumText = formatTime(getTime(entry).sum);
|
|
144
|
-
|
|
145
|
-
let equation = `${p(6, sumText)} = ${p(6, countText)} * ${p(6, perText)}`;
|
|
146
|
-
|
|
147
|
-
let ownTimeTop = getStatsTop(getTime(entry));
|
|
148
|
-
if (ownTimeTop.topHeavy) {
|
|
149
|
-
let topText = formatTime(ownTimeTop.value / ownTimeTop.count);
|
|
150
|
-
let topCountText = formatNumber(ownTimeTop.count);
|
|
151
|
-
let bottomText = formatTime((getTime(entry).sum - ownTimeTop.value) / getTime(entry).count, ownTimeTop.value);
|
|
152
|
-
let bottomCountText = formatNumber(getTime(entry).count - ownTimeTop.count);
|
|
153
|
-
let topPart = `${p(6, topText)} per * ${topCountText}`;
|
|
154
|
-
let bottomPart = `${bottomText} * ${bottomCountText}`;
|
|
155
|
-
if (isNode()) {
|
|
156
|
-
topPart = red(topPart);
|
|
157
|
-
} else {
|
|
158
|
-
bottomPart = white(bottomPart);
|
|
159
|
-
}
|
|
160
|
-
equation = `${p(6, sumText)} = ${p(6, topPart)} + ${bottomPart}`;
|
|
161
|
-
}
|
|
162
|
+
|
|
163
|
+
let equation = formatStats(getTime(entry));
|
|
162
164
|
|
|
163
165
|
let text = `${p(6, fractionText)} ( ${equation} )`;
|
|
164
166
|
let overhead = measureOverhead * getTime(entry).count;
|
|
@@ -188,7 +190,7 @@ export async function measureCode<T>(code: () => Promise<T>, config?: LogMeasure
|
|
|
188
190
|
try {
|
|
189
191
|
return await measureBlock(code, code.name || "untracked");
|
|
190
192
|
} finally {
|
|
191
|
-
finishProfile(measure, config);
|
|
193
|
+
finishProfile(measure, config || { name: code.name, minTimeToLog: 0 });
|
|
192
194
|
}
|
|
193
195
|
}
|
|
194
196
|
export function measureCodeSync<T>(code: () => T, config?: LogMeasureTableConfig): T {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { formatTime, formatNumber } from "../formatting/format";
|
|
2
|
+
import { red, white } from "../formatting/logColors";
|
|
3
|
+
import { isNode } from "../misc";
|
|
4
|
+
import { StatsValue, getStatsTop } from "./stats";
|
|
5
|
+
|
|
6
|
+
export function percent(value: number) {
|
|
7
|
+
return `${(value * 100).toFixed(2)}%`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function formatStats(stats: StatsValue, config?: {
|
|
11
|
+
noColor?: boolean;
|
|
12
|
+
noSum?: boolean;
|
|
13
|
+
}) {
|
|
14
|
+
function p(count: number, text: string | number) {
|
|
15
|
+
return String(text).padStart(count, " ");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let perText = formatTime(stats.sum / stats.count);
|
|
19
|
+
let countText = formatNumber(stats.count);
|
|
20
|
+
let sumText = formatTime(stats.sum);
|
|
21
|
+
let equation = (!config?.noSum && `${p(6, sumText)} = ` || "") + `${p(6, countText)} * ${p(6, perText)}`;
|
|
22
|
+
|
|
23
|
+
let top = getStatsTop(stats);
|
|
24
|
+
if (top.topHeavy) {
|
|
25
|
+
let topText = formatTime(top.value / top.count);
|
|
26
|
+
let topCountText = formatNumber(top.count);
|
|
27
|
+
let bottomText = formatTime((stats.sum - top.value) / (stats.count - top.count) || 0);
|
|
28
|
+
let bottomCountText = formatNumber(stats.count - top.count);
|
|
29
|
+
let topPart = `${topCountText} * ${p(6, topText)}`;
|
|
30
|
+
let bottomPart = `${bottomCountText} * ${bottomText}`;
|
|
31
|
+
if (!config?.noColor) {
|
|
32
|
+
if (isNode()) {
|
|
33
|
+
topPart = red(topPart);
|
|
34
|
+
} else {
|
|
35
|
+
bottomPart = white(bottomPart);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
equation = (!config?.noSum && `${p(6, sumText)} = ` || "") + `${p(6, topPart)} + ${bottomPart}`;
|
|
39
|
+
}
|
|
40
|
+
return equation;
|
|
41
|
+
}
|
package/src/webSocketServer.ts
CHANGED
|
@@ -96,7 +96,11 @@ export async function startSocketServer(
|
|
|
96
96
|
console.error(`Connection attempt error ${e.message}`);
|
|
97
97
|
});
|
|
98
98
|
httpsServer.on("tlsClientError", e => {
|
|
99
|
-
|
|
99
|
+
// NOTE: This happens a lot when we have tabs open that connected to an old
|
|
100
|
+
// server (with old certs, that the browser will reject?)
|
|
101
|
+
if (!SocketFunction.silent) {
|
|
102
|
+
console.error(`TLS client error ${e.message}`);
|
|
103
|
+
}
|
|
100
104
|
});
|
|
101
105
|
|
|
102
106
|
httpsServer.on("request", httpCallHandler);
|
|
@@ -235,9 +239,7 @@ export async function startSocketServer(
|
|
|
235
239
|
|
|
236
240
|
port = (realServer.address() as net.AddressInfo).port;
|
|
237
241
|
let nodeId = getNodeId(getCommonName(config.cert), port);
|
|
238
|
-
|
|
239
|
-
console.log(green(`Started Listening on ${nodeId}`));
|
|
240
|
-
}
|
|
242
|
+
console.log(green(`Started Listening on ${nodeId}`));
|
|
241
243
|
|
|
242
244
|
return nodeId;
|
|
243
245
|
}
|