socket-function 0.9.5 → 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.5",
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
  );
@@ -35,6 +35,7 @@
35
35
  stream: {
36
36
  // HACK: Needed to get SAX JS to work correctly.
37
37
  Stream: function () { },
38
+ Transform: function () { },
38
39
  },
39
40
  timers: {
40
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
@@ -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
- let parts = JSONLACKS.parse(linesJSON, config, hydrateState) as T[];
135
- for (let part of parts) {
136
- 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
+ }
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
- debugbreak(2);
275
- debugger;
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/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
 
@@ -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/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()}`;
@@ -52,6 +52,7 @@ let addMeasureOverheadTime = 0;
52
52
 
53
53
  // TIMING: About 60ns, of which 40ns is just now() calls.
54
54
  // If async is closer to 300ns.
55
+ // NOTE: Handles promises correctly
55
56
  export function getOwnTime<T>(
56
57
  name: string,
57
58
  code: () => T,
@@ -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
- let perText = formatTime(getTime(entry).sum / getTime(entry).count);
158
- let countText = formatNumber(getTime(entry).count);
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
+ }
@@ -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
- if (!SocketFunction.silent) {
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
  }