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 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<SocketExposedInterface>,
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
- return output as any;
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
- SocketFunction.exposedClasses.add(socketRegistered._classGuid);
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 = () => { };
@@ -22,8 +22,8 @@ export type SocketExposedInterfaceClass = {
22
22
  new(): unknown;
23
23
  prototype: unknown;
24
24
  };
25
- export interface SocketExposedShape<ExposedType extends SocketExposedInterface = SocketExposedInterface> {
26
- [functionName: string]: {
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.4",
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-g",
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. Resolve root ${JSON.stringify(this.rootResolvePath)} (set by call to setRequireBootRequire), resolve search paths: ${JSON.stringify(searchPaths)})}`);
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
  );
@@ -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
@@ -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 = Buffer.from(JSONLACKS.stringify(fullCall));
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}ms to serialize ${data.byteLength} bytes. For ${call.classGuid}.${call.functionName}`));
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 (data.byteLength > SocketFunction.MAX_MESSAGE_SIZE * 1.5) {
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: ${data.byteLength} > ${SocketFunction.MAX_MESSAGE_SIZE}). If you need to handle very large static data use some external service, such as Backblaze B2 or AWS S3. Or consider fragmenting data at an application level, because sending large data will cause large lag spikes for other clients using this server. Or, if absolutely required, set SocketFunction.MAX_MESSAGE_SIZE to a higher value.`);
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
- async function send(data: Buffer) {
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
- message = Buffer.from(await message.arrayBuffer());
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 || typeof message === "string") {
291
-
292
- let resultSize = message.length;
293
-
294
- if (message instanceof Buffer && message[0] === 0) {
295
- // First byte of 0 means it is decompressed (as JSON can't have a first byte of 0).
296
- (message as any) = message.slice(1);
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 = JSONLACKS.parse(message.toString(), { extended: false }) as InternalCallType | InternalReturnType;
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 ${message.length} bytes, for receieving result of call to ${callbackObj?.call.classGuid}.${callbackObj?.call.functionName}`));
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 ${message.length} bytes, for call to ${call.classGuid}.${call.functionName}`));
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
- if (isNode() && call.compress && SocketFunction.compression?.type === "gzip") {
351
- response.compressed = true;
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: ${result.byteLength} > ${SocketFunction.MAX_MESSAGE_SIZE}). If you need to handle very large static data use some external service, such as Backblaze B2 or AWS S3. Or consider fragmenting data at an application level, because sending large data will cause large lag spikes for other clients using this server. Or, if absolutely required, set SocketFunction.MAX_MESSAGE_SIZE to a higher value.`).stack,
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 = Buffer.from(JSONLACKS.stringify(response));
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 = true;
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
- let parts = JSONLACKS.parse(linesJSON, config, hydrateState) as T[];
123
- for (let part of parts) {
124
- output.push(part);
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
- debugbreak(2);
263
- debugger;
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: (() => void)[] = [];
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 (!queueWasEmpty) {
121
- // Wait for the previous promise to resolve
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
- ): Fnc {
282
+ ) {
259
283
  let cache = cacheLimited(limit, (argsJSON: string) => {
260
284
  return fnc(...JSON.parse(argsJSON));
261
285
  });
262
- return ((...args: unknown[]) => {
263
- return cache(JSON.stringify(args));
264
- }) as Fnc;
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
  }
@@ -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)
@@ -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 || [])) {
@@ -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
- const extraFactor = 10;
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 pendingCallTime: OwnTimeObjInternal | undefined;
19
- export function getPendingOwnTimeObjs(): (OwnTimeObj & { source: OwnTimeObjInternal })[] | undefined {
20
- let time = now();
21
- let instances = getPendingOwnTimeInstances();
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
- (global as any).pendingOwnCallTime = pendingCallTime;
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
- if (pendingCallTime) {
92
- pendingCallTime.child = obj;
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 (pendingCallTime === obj) {
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
- // Our parent is now the last open call
109
- pendingCallTime = obj.parent;
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
- if (obj.child && obj.parent) {
116
- obj.child.parent = obj.parent;
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;
@@ -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 { getOwnTime, getPendingOwnTimeInstances, getPendingOwnTimeObjs, OwnTimeObj } from "./getOwnTime";
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
- /** NOTE: Must be called BEFORE anything else is imported! */
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(getPendingOwnTimeInstances());
85
+ let openAtStart = new Set(getOpenTimesBase());
79
86
 
80
87
  outstandingProfiles.push(profile);
81
88
  return {
82
89
  finish() {
83
- let pending = getPendingOwnTimeObjs() || [];
90
+ let pending = getOpenTimesBase();
91
+ let last = pending[pending.length - 1];
92
+ let time = Date.now();
84
93
  for (let timeObj of pending) {
85
- if (openAtStart.has(timeObj.source)) continue;
86
- addToProfile(profile, timeObj, true);
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
- let perText = formatTime(getTime(entry).sum / getTime(entry).count);
142
- let countText = formatNumber(getTime(entry).count);
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
+ }
@@ -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
- console.error(`TLS client error ${e.message}`);
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
- if (!SocketFunction.silent) {
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
  }