socket-function 0.9.5 → 0.9.7
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/SetProcessVariables.ts +6 -0
- package/SocketFunction.ts +32 -12
- package/SocketFunctionTypes.ts +3 -3
- package/package.json +3 -2
- package/require/RequireController.ts +6 -2
- package/require/require.js +4 -0
- package/src/CallFactory.ts +66 -48
- package/src/JSONLACKS/JSONLACKS.ts +44 -19
- package/src/batching.ts +9 -0
- package/src/caching.ts +3 -0
- package/src/callHTTPHandler.ts +3 -10
- package/src/callManager.ts +3 -3
- package/src/formatting/format.ts +6 -1
- package/src/misc.ts +49 -0
- package/src/nodeCache.ts +14 -1
- package/src/profiling/getOwnTime.ts +1 -0
- package/src/profiling/measure.ts +9 -23
- package/src/profiling/statsFormat.ts +41 -0
- package/src/webSocketServer.ts +1 -3
package/SocketFunction.ts
CHANGED
|
@@ -10,6 +10,13 @@ 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 "./SetProcessVariables";
|
|
16
|
+
import cborx from "cbor-x";
|
|
17
|
+
import { setFlag } from "./require/compileFlags";
|
|
18
|
+
setFlag(require, "cbor-x", "allowclient", true);
|
|
19
|
+
let cborxInstance = new cborx.Encoder({ structuredClone: true });
|
|
13
20
|
|
|
14
21
|
module.allowclient = true;
|
|
15
22
|
|
|
@@ -22,22 +29,23 @@ type ExtractShape<ClassType, Shape> = {
|
|
|
22
29
|
: "Function is in shape, but not in class"
|
|
23
30
|
);
|
|
24
31
|
};
|
|
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
32
|
|
|
30
33
|
export class SocketFunction {
|
|
31
34
|
public static logMessages = false;
|
|
32
35
|
|
|
33
36
|
public static MAX_MESSAGE_SIZE = 1024 * 1024 * 32;
|
|
34
|
-
public static compression: undefined | {
|
|
35
|
-
type: "gzip";
|
|
36
|
-
};
|
|
37
37
|
|
|
38
38
|
public static httpETagCache = false;
|
|
39
39
|
public static silent = true;
|
|
40
40
|
|
|
41
|
+
// In retrospect... dynamically changing the wire serializer is a BAD idea. If any calls happen
|
|
42
|
+
// before it is changed, things just break. Also, it needs to be changed on both sides,
|
|
43
|
+
// or else things break. Also, it is very hard to detect when the issue is different serializers
|
|
44
|
+
public static readonly WIRE_SERIALIZER = {
|
|
45
|
+
serialize: (obj: unknown): MaybePromise<Buffer[]> => [cborxInstance.encode(obj)],
|
|
46
|
+
deserialize: (buffers: Buffer[]): MaybePromise<unknown> => cborxInstance.decode(buffers[0]),
|
|
47
|
+
};
|
|
48
|
+
|
|
41
49
|
public static WIRE_WARN_TIME = 100;
|
|
42
50
|
|
|
43
51
|
private static onMountCallbacks = new Map<string, (() => MaybePromise<void>)[]>();
|
|
@@ -54,18 +62,24 @@ export class SocketFunction {
|
|
|
54
62
|
// (ex, using a hook in a controller where the hook also calls the controller).
|
|
55
63
|
public static register<
|
|
56
64
|
ClassInstance extends object,
|
|
57
|
-
Shape extends SocketExposedShape<
|
|
65
|
+
Shape extends SocketExposedShape<{
|
|
66
|
+
[key in keyof ClassInstance]: (...args: any[]) => Promise<unknown>;
|
|
67
|
+
}>,
|
|
58
68
|
>(
|
|
59
69
|
classGuid: string,
|
|
60
70
|
instance: ClassInstance,
|
|
61
71
|
shapeFnc: () => Shape,
|
|
62
72
|
defaultHooksFnc?: () => SocketExposedShape[""] & {
|
|
63
73
|
onMount?: () => MaybePromise<void>;
|
|
74
|
+
},
|
|
75
|
+
config?: {
|
|
76
|
+
/** @noAutoExpose If true SocketFunction.expose(Controller) must be called explicitly. */
|
|
77
|
+
noAutoExpose?: boolean;
|
|
64
78
|
}
|
|
65
79
|
): SocketRegistered<ExtractShape<ClassInstance, Shape>> {
|
|
66
80
|
let getDefaultHooks = defaultHooksFnc && lazy(defaultHooksFnc);
|
|
67
81
|
const getShape = lazy(() => {
|
|
68
|
-
let shape = shapeFnc();
|
|
82
|
+
let shape = shapeFnc() as SocketExposedShape;
|
|
69
83
|
let defaultHooks = getDefaultHooks?.();
|
|
70
84
|
|
|
71
85
|
for (let value of Object.values(shape)) {
|
|
@@ -96,7 +110,7 @@ export class SocketFunction {
|
|
|
96
110
|
throw new Error(`Function ${functionName} is not in shape`);
|
|
97
111
|
}
|
|
98
112
|
|
|
99
|
-
let hookResult = await runClientHooks(call, shapeObj as SocketExposedShape[""], callFactory.connectionId);
|
|
113
|
+
let hookResult = await runClientHooks(call, shapeObj as Exclude<SocketExposedShape[""], undefined>, callFactory.connectionId);
|
|
100
114
|
|
|
101
115
|
if ("overrideResult" in hookResult) {
|
|
102
116
|
return hookResult.overrideResult;
|
|
@@ -128,7 +142,11 @@ export class SocketFunction {
|
|
|
128
142
|
}
|
|
129
143
|
});
|
|
130
144
|
|
|
131
|
-
|
|
145
|
+
let result = output as any as SocketRegistered;
|
|
146
|
+
if (!config?.noAutoExpose) {
|
|
147
|
+
this.expose(result);
|
|
148
|
+
}
|
|
149
|
+
return result;
|
|
132
150
|
}
|
|
133
151
|
|
|
134
152
|
public static onNextDisconnect(nodeId: string, callback: () => void) {
|
|
@@ -185,8 +203,9 @@ export class SocketFunction {
|
|
|
185
203
|
* to add additional imports to ensure the register call runs.
|
|
186
204
|
*/
|
|
187
205
|
public static expose(socketRegistered: SocketRegistered) {
|
|
206
|
+
console.log(blue(`Exposing Controller ${socketRegistered._classGuid}`));
|
|
188
207
|
exposeClass(socketRegistered);
|
|
189
|
-
|
|
208
|
+
this.exposedClasses.add(socketRegistered._classGuid);
|
|
190
209
|
|
|
191
210
|
if (this.hasMounted) {
|
|
192
211
|
let mountCallbacks = SocketFunction.onMountCallbacks.get(socketRegistered._classGuid);
|
|
@@ -199,6 +218,7 @@ export class SocketFunction {
|
|
|
199
218
|
}
|
|
200
219
|
|
|
201
220
|
public static mountedNodeId: string = "";
|
|
221
|
+
public static isMounted() { return !!this.mountedNodeId; }
|
|
202
222
|
public static mountedIP: string = "";
|
|
203
223
|
private static hasMounted = false;
|
|
204
224
|
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.7",
|
|
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": "
|
|
13
|
+
"typenode": "5.3.3",
|
|
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
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
// Mirror the tnode.js setting
|
|
10
10
|
NODE_ENV: "production"
|
|
11
11
|
},
|
|
12
|
+
versions: {
|
|
13
|
+
|
|
14
|
+
},
|
|
12
15
|
},
|
|
13
16
|
setImmediate(callback) {
|
|
14
17
|
setTimeout(callback, 0);
|
|
@@ -35,6 +38,7 @@
|
|
|
35
38
|
stream: {
|
|
36
39
|
// HACK: Needed to get SAX JS to work correctly.
|
|
37
40
|
Stream: function () { },
|
|
41
|
+
Transform: function () { },
|
|
38
42
|
},
|
|
39
43
|
timers: {
|
|
40
44
|
// 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
|
|
@@ -94,7 +94,7 @@ export class JSONLACKS {
|
|
|
94
94
|
obj = measureBlock(function JSONparse() { return JSON.parse(text); });
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
return JSONLACKS.hydrateSpecialObjects(obj, hydrateState) as T;
|
|
97
|
+
return JSONLACKS.hydrateSpecialObjects(obj, hydrateState, config) as T;
|
|
98
98
|
}
|
|
99
99
|
@measureFnc
|
|
100
100
|
public static async parseLines<T>(buffer: Buffer, config?: JSONLACKS_ParseConfig): Promise<T[]> {
|
|
@@ -131,9 +131,32 @@ export class JSONLACKS {
|
|
|
131
131
|
linesJSON += lines[i];
|
|
132
132
|
}
|
|
133
133
|
linesJSON += "]";
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
+
}
|
|
137
160
|
}
|
|
138
161
|
}
|
|
139
162
|
while (pos < buffer.length) {
|
|
@@ -237,7 +260,7 @@ export class JSONLACKS {
|
|
|
237
260
|
}
|
|
238
261
|
|
|
239
262
|
@measureFnc
|
|
240
|
-
private static hydrateSpecialObjects(obj: unknown, hydrateState?: HydrateState): unknown {
|
|
263
|
+
private static hydrateSpecialObjects(obj: unknown, hydrateState?: HydrateState, config?: JSONLACKS_ParseConfig): unknown {
|
|
241
264
|
let references = hydrateState?.references || new Map<string, unknown>();
|
|
242
265
|
let visited = hydrateState?.visited || new Set<unknown>();
|
|
243
266
|
return iterate(obj);
|
|
@@ -271,8 +294,10 @@ export class JSONLACKS {
|
|
|
271
294
|
if (type === "ref") {
|
|
272
295
|
let id = obj.id as string;
|
|
273
296
|
if (!JSONLACKS.IGNORE_MISSING_REFERENCES && !references.has(id)) {
|
|
274
|
-
|
|
275
|
-
|
|
297
|
+
if (!config?.discardMissingReferences) {
|
|
298
|
+
debugbreak(2);
|
|
299
|
+
debugger;
|
|
300
|
+
}
|
|
276
301
|
throw new Error(`Reference to undefined id "${id}"`);
|
|
277
302
|
}
|
|
278
303
|
return references.get(id);
|
package/src/batching.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isNode } from "./misc";
|
|
2
2
|
import { measureWrap } from "./profiling/measure";
|
|
3
|
+
import { MaybePromise } from "./types";
|
|
3
4
|
|
|
4
5
|
/*
|
|
5
6
|
"numbers" use setTimeout
|
|
@@ -35,6 +36,14 @@ export function delay(delayTime: DelayType): Promise<void> {
|
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
// NOTE: This is an easy way to turn off batching, without having to strip the extra batch handling code
|
|
40
|
+
export function batchFunctionNone<Arg, Result = void>(
|
|
41
|
+
config: unknown,
|
|
42
|
+
fnc: (arg: Arg[]) => (Promise<Result> | Result)
|
|
43
|
+
): (arg: Arg) => Promise<Result> {
|
|
44
|
+
return async arg => fnc([arg]);
|
|
45
|
+
}
|
|
46
|
+
|
|
38
47
|
export function batchFunction<Arg, Result = void>(
|
|
39
48
|
config: {
|
|
40
49
|
delay: DelayType;
|
package/src/caching.ts
CHANGED
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
|
}
|
|
@@ -50,6 +50,8 @@ export function getNodeIdsFromRequest(request: http.IncomingMessage) {
|
|
|
50
50
|
|
|
51
51
|
export async function httpCallHandler(request: http.IncomingMessage, response: http.ServerResponse) {
|
|
52
52
|
try {
|
|
53
|
+
// Always set x-frame-options, to prevent iframe embedding click hijacking
|
|
54
|
+
response.setHeader("X-Frame-Options", "SAMEORIGIN");
|
|
53
55
|
|
|
54
56
|
let urlBase = request.url;
|
|
55
57
|
if (!urlBase) {
|
|
@@ -147,15 +149,6 @@ export async function httpCallHandler(request: http.IncomingMessage, response: h
|
|
|
147
149
|
}
|
|
148
150
|
|
|
149
151
|
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
152
|
|
|
160
153
|
// NOTE: Our ETag caching is only to reduce data sent on the wire, we evaluate the calls
|
|
161
154
|
// 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
|
@@ -313,4 +313,53 @@ export function last<T>(arr: T[]): T | undefined {
|
|
|
313
313
|
export type ObjectValues<T> = T[keyof T];
|
|
314
314
|
export function entries<Obj extends { [key: string]: unknown }>(obj: Obj): [keyof Obj, ObjectValues<Obj>][] {
|
|
315
315
|
return Object.entries(obj) as any;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function sort<T>(arr: T[], sortKey: (obj: T) => unknown) {
|
|
319
|
+
if (arr.length <= 1) return arr;
|
|
320
|
+
arr.sort((a, b) => compare(sortKey(a), sortKey(b)));
|
|
321
|
+
return arr;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// NOTE: If there are duplicates, returns the first match.
|
|
325
|
+
export function binarySearchIndex(listCount: number, compare: (lhsIndex: number) => number): number {
|
|
326
|
+
if (listCount === 0) {
|
|
327
|
+
return ~0;
|
|
328
|
+
}
|
|
329
|
+
let min = 0;
|
|
330
|
+
let max = listCount - 1;
|
|
331
|
+
while (min < max) {
|
|
332
|
+
let fingerIndex = Math.floor((max + min) / 2);
|
|
333
|
+
let comparisonValue = compare(fingerIndex);
|
|
334
|
+
if (comparisonValue < 0) {
|
|
335
|
+
min = fingerIndex + 1;
|
|
336
|
+
} else {
|
|
337
|
+
max = fingerIndex;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
let comparison = compare(min);
|
|
341
|
+
if (comparison === 0) return min;
|
|
342
|
+
if (comparison > 0) return ~min;
|
|
343
|
+
return ~(min + 1);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export function compare(lhs: unknown, rhs: unknown): number {
|
|
347
|
+
if (typeof lhs !== typeof rhs) {
|
|
348
|
+
return compare(typeof lhs, typeof rhs);
|
|
349
|
+
}
|
|
350
|
+
if (lhs === rhs) return 0;
|
|
351
|
+
if (lhs as any < (rhs as any)) return -1;
|
|
352
|
+
return 1;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function insertIntoSortedList<T>(list: T[], map: (val: T) => string | number, element: T) {
|
|
356
|
+
let searchValue = map(element);
|
|
357
|
+
let index = binarySearchIndex(list.length, i => compare(map(list[i]), searchValue));
|
|
358
|
+
if (index < 0) index = ~index;
|
|
359
|
+
list.splice(index, 0, element);
|
|
360
|
+
}
|
|
361
|
+
export function removeFromSortedList<T>(list: T[], map: (val: T) => string | number, searchValue: string | number) {
|
|
362
|
+
let index = binarySearchIndex(list.length, i => compare(map(list[i]), searchValue));
|
|
363
|
+
if (index < 0) return;
|
|
364
|
+
list.splice(index, 1);
|
|
316
365
|
}
|
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()}`;
|
|
@@ -44,9 +50,16 @@ export function getNodeIdLocation(nodeId: string): { address: string, port: numb
|
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
export function getNodeIdDomain(nodeId: string): string {
|
|
53
|
+
let result = getNodeIdDomainMaybeUndefined(nodeId);
|
|
54
|
+
if (result === undefined) {
|
|
55
|
+
throw new Error(`Cannot get domain from nodeId, which is only usable as a client. NodeId: ${JSON.stringify(nodeId)}`);
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
export function getNodeIdDomainMaybeUndefined(nodeId: string): string | undefined {
|
|
47
60
|
let location = getNodeIdLocation(nodeId);
|
|
48
61
|
if (!location) {
|
|
49
|
-
|
|
62
|
+
return undefined;
|
|
50
63
|
}
|
|
51
64
|
return new URL(location.address).hostname.split(".").slice(-2).join(".");
|
|
52
65
|
}
|
package/src/profiling/measure.ts
CHANGED
|
@@ -6,7 +6,9 @@ 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
|
|
|
11
|
+
let measurementsDisabled = false;
|
|
10
12
|
/** NOTE: Must be called BEFORE anything else is imported!
|
|
11
13
|
* NOTE: Measurements on on by default now, so this doesn't really need to be called...
|
|
12
14
|
*/
|
|
@@ -19,6 +21,7 @@ export function enableMeasurements() {
|
|
|
19
21
|
/** NOTE: Must be called BEFORE anything else is imported! */
|
|
20
22
|
export function disableMeasurements() {
|
|
21
23
|
measurementsEnabled = false;
|
|
24
|
+
measurementsDisabled = true;
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
let functionsSkipped = 0;
|
|
@@ -30,6 +33,7 @@ const AsyncFunction = (async () => { }).constructor;
|
|
|
30
33
|
// TIMING: 1-5us. I have seen timing values greatly vary, but it does seem to be quite high, despite
|
|
31
34
|
// microbenchmarks saying it is slow. Perhaps it is because getOwnTime breaks the cpu pipeline,
|
|
32
35
|
// which causes slowness for code around us, but not if we are running in isolation?
|
|
36
|
+
// NOTE: Handles promises correctly
|
|
33
37
|
export function measureFnc(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
|
34
38
|
let name = propertyKey;
|
|
35
39
|
if (target.name) {
|
|
@@ -51,6 +55,7 @@ export function nameFunction<T extends Function>(name: string, fnc: T) {
|
|
|
51
55
|
Object.defineProperty(fnc, "name", { value: name });
|
|
52
56
|
return fnc;
|
|
53
57
|
}
|
|
58
|
+
// NOTE: Handles promises correctly
|
|
54
59
|
export function measureWrap<T extends (...args: any[]) => any>(fnc: T, name?: string): T {
|
|
55
60
|
if (!measurementsEnabled) {
|
|
56
61
|
functionsSkipped++;
|
|
@@ -71,7 +76,7 @@ export function measureBlock<T extends (...args: any[]) => any>(fnc: T, name?: s
|
|
|
71
76
|
export function startMeasure(): {
|
|
72
77
|
finish: () => MeasureProfile;
|
|
73
78
|
} {
|
|
74
|
-
if (!measurementsEnabled) {
|
|
79
|
+
if (!measurementsEnabled && !measurementsDisabled) {
|
|
75
80
|
console.warn(red(`To capture measurements enableMeasurements() must be called before any other imports in your entry point`));
|
|
76
81
|
}
|
|
77
82
|
let profile: MeasureProfile = {
|
|
@@ -154,27 +159,8 @@ export function logMeasureTable(
|
|
|
154
159
|
return String(text).padStart(count, " ");
|
|
155
160
|
}
|
|
156
161
|
let fractionText = percent(getTime(entry).sum / totalTime);
|
|
157
|
-
|
|
158
|
-
let
|
|
159
|
-
let sumText = formatTime(getTime(entry).sum);
|
|
160
|
-
|
|
161
|
-
let equation = `${p(6, sumText)} = ${p(6, countText)} * ${p(6, perText)}`;
|
|
162
|
-
|
|
163
|
-
let ownTimeTop = getStatsTop(getTime(entry));
|
|
164
|
-
if (ownTimeTop.topHeavy) {
|
|
165
|
-
let topText = formatTime(ownTimeTop.value / ownTimeTop.count);
|
|
166
|
-
let topCountText = formatNumber(ownTimeTop.count);
|
|
167
|
-
let bottomText = formatTime((getTime(entry).sum - ownTimeTop.value) / getTime(entry).count, ownTimeTop.value);
|
|
168
|
-
let bottomCountText = formatNumber(getTime(entry).count - ownTimeTop.count);
|
|
169
|
-
let topPart = `${p(6, topText)} per * ${topCountText}`;
|
|
170
|
-
let bottomPart = `${bottomText} * ${bottomCountText}`;
|
|
171
|
-
if (isNode()) {
|
|
172
|
-
topPart = red(topPart);
|
|
173
|
-
} else {
|
|
174
|
-
bottomPart = white(bottomPart);
|
|
175
|
-
}
|
|
176
|
-
equation = `${p(6, sumText)} = ${p(6, topPart)} + ${bottomPart}`;
|
|
177
|
-
}
|
|
162
|
+
|
|
163
|
+
let equation = formatStats(getTime(entry));
|
|
178
164
|
|
|
179
165
|
let text = `${p(6, fractionText)} ( ${equation} )`;
|
|
180
166
|
let overhead = measureOverhead * getTime(entry).count;
|
|
@@ -204,7 +190,7 @@ export async function measureCode<T>(code: () => Promise<T>, config?: LogMeasure
|
|
|
204
190
|
try {
|
|
205
191
|
return await measureBlock(code, code.name || "untracked");
|
|
206
192
|
} finally {
|
|
207
|
-
finishProfile(measure, config);
|
|
193
|
+
finishProfile(measure, config || { name: code.name, minTimeToLog: 0 });
|
|
208
194
|
}
|
|
209
195
|
}
|
|
210
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
|
@@ -239,9 +239,7 @@ export async function startSocketServer(
|
|
|
239
239
|
|
|
240
240
|
port = (realServer.address() as net.AddressInfo).port;
|
|
241
241
|
let nodeId = getNodeId(getCommonName(config.cert), port);
|
|
242
|
-
|
|
243
|
-
console.log(green(`Started Listening on ${nodeId}`));
|
|
244
|
-
}
|
|
242
|
+
console.log(green(`Started Listening on ${nodeId}`));
|
|
245
243
|
|
|
246
244
|
return nodeId;
|
|
247
245
|
}
|