socket-function 0.8.37 → 0.8.39

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
@@ -9,6 +9,7 @@ import { Args, MaybePromise } from "./src/types";
9
9
  import { setDefaultHTTPCall } from "./src/callHTTPHandler";
10
10
  import debugbreak from "debugbreak";
11
11
  import { lazy } from "./src/caching";
12
+ import { delay } from "./src/batching";
12
13
 
13
14
  module.allowclient = true;
14
15
 
@@ -28,10 +29,16 @@ type PickByType<T, Value> = {
28
29
 
29
30
  export class SocketFunction {
30
31
  public static logMessages = false;
32
+
33
+ public static MAX_MESSAGE_SIZE = 1024 * 1024 * 32;
31
34
  public static compression: undefined | {
32
35
  type: "gzip";
33
36
  };
37
+
34
38
  public static httpETagCache = false;
39
+ public static silent = true;
40
+
41
+ public static WIRE_WARN_TIME = 100;
35
42
 
36
43
  private static onMountCallbacks = new Map<string, (() => MaybePromise<void>)[]>();
37
44
  public static exposedClasses = new Set<string>();
@@ -70,7 +77,7 @@ export class SocketFunction {
70
77
  return shape as any as SocketExposedShape;
71
78
  });
72
79
 
73
- setImmediate(() => {
80
+ void Promise.resolve().then(() => {
74
81
  registerClass(classGuid, instance as SocketExposedInterface, getShape());
75
82
  });
76
83
 
@@ -82,14 +89,14 @@ export class SocketFunction {
82
89
  console.log(`START\t\t\t${classGuid}.${functionName}`);
83
90
  }
84
91
  try {
85
- let callFactory = await getCreateCallFactory(nodeId, SocketFunction.mountedNodeId);
92
+ let callFactory = await getCreateCallFactory(nodeId);
86
93
 
87
94
  let shapeObj = getShape()[functionName];
88
95
  if (!shapeObj) {
89
96
  throw new Error(`Function ${functionName} is not in shape`);
90
97
  }
91
98
 
92
- let hookResult = await runClientHooks(call, shapeObj as SocketExposedShape[""]);
99
+ let hookResult = await runClientHooks(call, shapeObj as SocketExposedShape[""], callFactory.connectionId);
93
100
 
94
101
  if ("overrideResult" in hookResult) {
95
102
  return hookResult.overrideResult;
@@ -109,7 +116,7 @@ export class SocketFunction {
109
116
  _classGuid: classGuid,
110
117
  };
111
118
 
112
- setImmediate(() => {
119
+ void Promise.resolve().then(() => {
113
120
  let onMount = getDefaultHooks?.().onMount;
114
121
  if (onMount) {
115
122
  let callbacks = SocketFunction.onMountCallbacks.get(classGuid);
@@ -191,12 +198,22 @@ export class SocketFunction {
191
198
  }
192
199
  }
193
200
 
194
- public static mountedNodeId: string = "NOTMOUNTED";
201
+ public static mountedNodeId: string = "";
202
+ public static mountedIP: string = "";
195
203
  private static hasMounted = false;
196
204
  public static async mount(config: SocketServerConfig) {
197
- if (this.mountedNodeId !== "NOTMOUNTED") {
205
+ if (this.mountedNodeId) {
198
206
  throw new Error("SocketFunction already mounted, mounting twice in one thread is not allowed.");
199
207
  }
208
+
209
+ this.mountedIP = config.public ? "0.0.0.0" : "127.0.0.1";
210
+ if (config.ip) {
211
+ this.mountedIP = config.ip;
212
+ }
213
+
214
+ // Wait for any additionals functions to expose themselves
215
+ await delay("immediate");
216
+
200
217
  this.mountedNodeId = await startSocketServer(config);
201
218
  this.hasMounted = true;
202
219
  for (let classGuid of SocketFunction.exposedClasses) {
@@ -57,6 +57,7 @@ export type ClientHookContext<ExposedType extends SocketExposedInterface = Socke
57
57
  call: FullCallType;
58
58
  // If the result is overriden, we continue evaluating hooks BUT DO NOT perform the final call
59
59
  overrideResult?: unknown;
60
+ connectionId: { nodeId: string };
60
61
  };
61
62
  export interface SocketFunctionClientHook<ExposedType extends SocketExposedInterface = SocketExposedInterface> {
62
63
  (config: ClientHookContext<ExposedType>): MaybePromise<void>;
@@ -85,5 +86,6 @@ export type CallerContextBase = {
85
86
  // The nodeId they contacted. This is useful to determine their intention (otherwise
86
87
  // requests can be redirected to us and would accept them, even though they are being
87
88
  // blatantly MITMed).
89
+ // IF they are the server, calling us back, then this will just be ""
88
90
  localNodeId: string;
89
91
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "socket-function",
3
- "version": "0.8.37",
3
+ "version": "0.8.39",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
@@ -9,7 +9,8 @@
9
9
  "mobx": "^6.6.2",
10
10
  "node-forge": "https://github.com/sliftist/forge#name",
11
11
  "preact": "^10.10.6",
12
- "typenode": "^4.9.4-a",
12
+ "rdtsc-now": "^0.3.0",
13
+ "typenode": "^4.9.4-c",
13
14
  "ws": "^8.8.0"
14
15
  },
15
16
  "scripts": {
@@ -21,6 +22,7 @@
21
22
  "@types/node-forge": "^1.3.1",
22
23
  "@types/ws": "^8.5.3",
23
24
  "debugbreak": "^0.6.5",
25
+ "pegjs": "^0.10.0",
24
26
  "typedev": "^0.1.1"
25
27
  }
26
28
  }
@@ -13,6 +13,11 @@ declare global {
13
13
  /** Indicates the module is allowed clientside. */
14
14
  allowclient?: boolean;
15
15
 
16
+ /** Causes the module to not preload, requiring `await import()` for it to load correctly
17
+ * - Shouldn't be set recursively, otherwise nested packages will break.
18
+ */
19
+ lazyload?: boolean;
20
+
16
21
  /** Indicates the module is definitely not allowed clientside */
17
22
  serveronly?: boolean;
18
23
 
@@ -118,7 +123,8 @@ class RequireControllerBase {
118
123
  let modules: {
119
124
  [resolvedPath: string]: SerializedModule;
120
125
  } = Object.create(null);
121
- function addModule(module: NodeJS.Module) {
126
+ function addModule(module: NodeJS.Module, rootImport = false) {
127
+ if (!rootImport && module.lazyload) return;
122
128
  if (!module.requireControllerSeqNum) {
123
129
  module.requireControllerSeqNum = nextModuleSeqNum++;
124
130
  }
@@ -223,7 +229,7 @@ class RequireControllerBase {
223
229
  clientModule = createNotFoundModule(`Module ${pathRequest} (resolved to ${resolvedPath}) is not allowed clientside (set module.allowclient in it, or call setFlag when it is imported).`);
224
230
  }
225
231
 
226
- addModule(clientModule);
232
+ addModule(clientModule, true);
227
233
  }
228
234
 
229
235
  return { requestsResolvedPaths, modules, requireSeqNumProcessId };
@@ -16,7 +16,6 @@
16
16
  global: window,
17
17
  });
18
18
 
19
-
20
19
  // Not real modules, as we just define their exports
21
20
  const builtInModuleExports = {
22
21
  worker_threads: {
@@ -243,6 +242,15 @@
243
242
  }
244
243
 
245
244
  let resolvedPath = serializedModule.requests[request];
245
+ if (resolvedPath !== "NOTALLOWEDCLIENTSIDE" && !serializedModules[resolvedPath]) {
246
+ if (!asyncIsFine) {
247
+ console.warn(`Accessed unexpected module %c${request}%c in %c${module.id}%c\n\tTreating it as an async require.\n\tAll modules require synchronously clientside must be required serverside at a module level.`,
248
+ "color: red", "color: unset",
249
+ "color: red", "color: unset",
250
+ );
251
+ }
252
+ return rootRequire(resolvedPath);
253
+ }
246
254
 
247
255
  let exportsOverride = undefined;
248
256
  if (resolvedPath === "NOTALLOWEDCLIENTSIDE" || !serializedModules[resolvedPath].allowclient) {
@@ -253,6 +261,7 @@
253
261
  if (property === "__esModule") return undefined;
254
262
  // NOTE: Return a toString that evaluates to "" so we can EXPLICITLY detect non-loaded modules
255
263
  if (property === unloadedModule) return true;
264
+ if (property === "default") return exportsOverride;
256
265
 
257
266
  throw new Error(`Module ${childId} is serverside only. Tried to access ${property} from ${module.id}`);
258
267
  }
@@ -263,6 +272,7 @@
263
272
  if (property === "__esModule") return undefined;
264
273
  // NOTE: Return a toString that evaluates to "" so we can EXPLICITLY detect non-loaded modules
265
274
  if (property === unloadedModule) return true;
275
+ if (property === "default") return exportsOverride;
266
276
 
267
277
  serializedModule;
268
278
 
@@ -336,6 +346,7 @@
336
346
  module.id = resolvedId;
337
347
  module.filename = serializedModule?.filename;
338
348
  module.exports = {};
349
+ module.exports.default = module.exports;
339
350
  module.children = [];
340
351
 
341
352
  module.load = load;
@@ -382,19 +393,6 @@
382
393
 
383
394
  let dirname = module.filename.replace(/\\/g, "/").split("/").slice(0, -1).join("/");
384
395
 
385
- var __createBinding = (Object.create ? (function (o, m, k, k2) {
386
- if (k2 === undefined) k2 = k;
387
- Object.defineProperty(o, k2, { enumerable: true, get: function () { return m[k]; } });
388
- }) : (function (o, m, k, k2) {
389
- if (k2 === undefined) k2 = k;
390
- o[k2] = m[k];
391
- }));
392
- var __setModuleDefault = (Object.create ? (function (o, v) {
393
- Object.defineProperty(o, "default", { enumerable: true, value: v });
394
- }) : function (o, v) {
395
- o["default"] = v;
396
- });
397
-
398
396
  let time = Date.now();
399
397
  currentModuleEvaluationStack.push(module.filename);
400
398
  try {
@@ -404,18 +402,11 @@
404
402
  // which checks for unloadedModule and returns undefined in that case.
405
403
  __importStar(mod) {
406
404
  if (mod[unloadedModule]) return undefined;
407
- if (mod && mod.__esModule) return mod;
408
- var result = {};
409
- if (mod !== null && mod !== undefined) {
410
- for (var k in mod) {
411
- if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) {
412
- __createBinding(result, mod, k);
413
- }
414
- }
415
- }
416
- __setModuleDefault(result, mod);
417
- return result;
418
- }
405
+ return mod;
406
+ },
407
+ __importDefault(mod) {
408
+ return mod.default ? mod : { default: mod };
409
+ },
419
410
  },
420
411
  module.exports,
421
412
  module.require,
@@ -1,7 +1,7 @@
1
1
  import { CallerContext, CallerContextBase, CallType, FullCallType } from "../SocketFunctionTypes";
2
2
  import * as ws from "ws";
3
3
  import { performLocalCall } from "./callManager";
4
- import { convertErrorStackToError, formatNumberSuffixed, isNode } from "./misc";
4
+ import { convertErrorStackToError, formatNumberSuffixed, isNode, list } from "./misc";
5
5
  import { createWebsocketFactory, getTLSSocket } from "./websocketFactory";
6
6
  import { SocketFunction } from "../SocketFunction";
7
7
  import { gzip } from "zlib";
@@ -9,6 +9,9 @@ import * as tls from "tls";
9
9
  import { getClientNodeId, getNodeIdLocation, registerNodeClient } from "./nodeCache";
10
10
  import debugbreak from "debugbreak";
11
11
  import { lazy } from "./caching";
12
+ import { JSONLACKS } from "./JSONLACKS/JSONLACKS";
13
+ import { red } from "./formatting/logColors";
14
+ import { isSplitableArray } from "./fixLargeNetworkCalls";
12
15
 
13
16
  const MIN_RETRY_DELAY = 1000;
14
17
 
@@ -37,6 +40,7 @@ export interface CallFactory {
37
40
  // Trigger performLocalCall on the other side of the connection
38
41
  performCall(call: CallType): Promise<unknown>;
39
42
  onNextDisconnect(callback: () => void): void;
43
+ connectionId: { nodeId: string };
40
44
  }
41
45
 
42
46
  export interface SenderInterface {
@@ -56,8 +60,10 @@ export interface SenderInterface {
56
60
 
57
61
  export async function createCallFactory(
58
62
  webSocketBase: SenderInterface | undefined,
63
+ // The node id we are connecting to (or that connected to us)
59
64
  nodeId: string,
60
- localNodeId: string,
65
+ // The node id that we were contacted on
66
+ localNodeId = "",
61
67
  ): Promise<CallFactory> {
62
68
  let niceConnectionName = nodeId;
63
69
 
@@ -66,8 +72,6 @@ export async function createCallFactory(
66
72
 
67
73
  const canReconnect = !!getNodeIdLocation(nodeId);
68
74
 
69
- let lastReceivedSeqNum = 0;
70
-
71
75
  let pendingCalls: Map<number, {
72
76
  data: Buffer;
73
77
  call: InternalCallType;
@@ -76,7 +80,7 @@ export async function createCallFactory(
76
80
  // NOTE: It is important to make this as random as possible, to prevent
77
81
  // reconnections dues to a process being reset causing seqNum collisions
78
82
  // in return calls.
79
- let nextSeqNum = Math.random();
83
+ let nextSeqNum = Date.now() + Math.random();
80
84
 
81
85
  let lastConnectionAttempt = 0;
82
86
 
@@ -93,6 +97,7 @@ export async function createCallFactory(
93
97
  let callFactory: CallFactory = {
94
98
  nodeId,
95
99
  lastClosed: 0,
100
+ connectionId: { nodeId },
96
101
  onNextDisconnect,
97
102
  async performCall(call: CallType) {
98
103
  let seqNum = nextSeqNum++;
@@ -105,7 +110,37 @@ export async function createCallFactory(
105
110
  seqNum,
106
111
  compress: !!SocketFunction.compression,
107
112
  };
108
- let data = Buffer.from(JSON.stringify(fullCall));
113
+ let time = Date.now();
114
+ let data = Buffer.from(JSONLACKS.stringify(fullCall));
115
+ time = Date.now() - time;
116
+ if (time > SocketFunction.WIRE_WARN_TIME) {
117
+ console.log(red(`Slow serialize, took ${time}ms to serialize ${data.byteLength} bytes. For ${call.classGuid}.${call.functionName}`));
118
+ }
119
+
120
+ if (data.byteLength > SocketFunction.MAX_MESSAGE_SIZE * 1.5) {
121
+ let splitArgIndex = call.args.findIndex(isSplitableArray);
122
+ if (splitArgIndex >= 0) {
123
+ let SPLIT_GROUPS = 10;
124
+ let splitArg = call.args[splitArgIndex] as unknown[];
125
+ let subCalls = list(SPLIT_GROUPS).map(index => {
126
+ let start = Math.floor(index / SPLIT_GROUPS * splitArg.length);
127
+ let end = Math.floor((index + 1) / SPLIT_GROUPS * splitArg.length);
128
+ return splitArg.slice(start, end);
129
+ }).filter(x => x.length > 0);
130
+ for (let splitList of subCalls) {
131
+ let subCall = { ...call };
132
+ subCall.args = subCall.args.slice();
133
+ subCall.args[splitArgIndex] = splitList;
134
+ await callFactory.performCall(subCall);
135
+ }
136
+ // Eh... we COULD return the array of results, but... then the result would sometimes be an array,
137
+ // some times not, so, it is better to return a string which will make it more clear why it sometimes varies.
138
+ return "CALLS_SPLIT_DUE_TO_LARGE_ARGS";
139
+ }
140
+
141
+ 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.`);
142
+ }
143
+
109
144
  let resultPromise = new Promise((resolve, reject) => {
110
145
  let callback = (result: InternalReturnType) => {
111
146
  if (SocketFunction.logMessages) {
@@ -137,6 +172,7 @@ export async function createCallFactory(
137
172
  registerOnce();
138
173
 
139
174
  function onClose(error: string) {
175
+ callFactory.connectionId = { nodeId };
140
176
  callFactory.lastClosed = Date.now();
141
177
  webSocketPromise = undefined;
142
178
  if (!canReconnect) {
@@ -181,7 +217,9 @@ export async function createCallFactory(
181
217
  if (newWebSocket.readyState === 0 /* CONNECTING */) {
182
218
  await new Promise<void>(resolve => {
183
219
  newWebSocket.addEventListener("open", () => {
184
- console.log(`Connection established to ${niceConnectionName}`);
220
+ if (!SocketFunction.silent) {
221
+ console.log(`Connection established to ${niceConnectionName}`);
222
+ }
185
223
  callFactory.isConnected = true;
186
224
  resolve();
187
225
  });
@@ -248,9 +286,15 @@ export async function createCallFactory(
248
286
  (message as any) = Buffer.from(arrayBuffer);
249
287
  }
250
288
 
251
- let call = JSON.parse(message.toString()) as InternalCallType | InternalReturnType;
289
+ let time = Date.now();
290
+ let call = JSONLACKS.parse(message.toString(), { extended: false }) as InternalCallType | InternalReturnType;
291
+ time = Date.now() - time;
292
+
252
293
  if (call.isReturn) {
253
294
  let callbackObj = pendingCalls.get(call.seqNum);
295
+ if (time > SocketFunction.WIRE_WARN_TIME) {
296
+ 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}`));
297
+ }
254
298
  if (!callbackObj) {
255
299
  console.log(`Got return for unknown call ${call.seqNum}`);
256
300
  return;
@@ -258,11 +302,9 @@ export async function createCallFactory(
258
302
  call.resultSize = resultSize;
259
303
  callbackObj.callback(call);
260
304
  } else {
261
- if (call.seqNum <= lastReceivedSeqNum) {
262
- console.log(`Received out of sequence call ${call.seqNum}`);
263
- return;
305
+ if (time > SocketFunction.WIRE_WARN_TIME) {
306
+ console.log(red(`Slow parse, took ${time}ms to parse ${message.length} bytes, for call to ${call.classGuid}.${call.functionName}`));
264
307
  }
265
- lastReceivedSeqNum = call.seqNum;
266
308
 
267
309
  let response: InternalReturnType;
268
310
  try {
@@ -288,13 +330,24 @@ export async function createCallFactory(
288
330
  let result: Buffer;
289
331
  if (isNode() && call.compress && SocketFunction.compression?.type === "gzip") {
290
332
  response.compressed = true;
291
- result = Buffer.from(JSON.stringify(response));
333
+ result = Buffer.from(JSONLACKS.stringify(response));
292
334
  result = await new Promise<Buffer>((resolve, reject) =>
293
335
  gzip(result, (err, result) => err ? reject(err) : resolve(result))
294
336
  );
295
337
  result = Buffer.concat([new Uint8Array([0]), result]);
296
338
  } else {
297
- result = Buffer.from(JSON.stringify(response));
339
+ result = Buffer.from(JSONLACKS.stringify(response));
340
+ }
341
+ if (result.byteLength > SocketFunction.MAX_MESSAGE_SIZE * 1.5) {
342
+ response = {
343
+ isReturn: true,
344
+ result: undefined,
345
+ seqNum: call.seqNum,
346
+ 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,
347
+ resultSize: resultSize,
348
+ compressed: false,
349
+ };
350
+ result = Buffer.from(JSONLACKS.stringify(response));
298
351
  }
299
352
  await send(result);
300
353
  }
@@ -302,6 +355,8 @@ export async function createCallFactory(
302
355
  }
303
356
  throw new Error(`Unhandled data type ${typeof message}`);
304
357
  } catch (e: any) {
358
+ debugbreak(1);
359
+ debugger;
305
360
  console.error(e.stack);
306
361
  }
307
362
  }