socket-function 0.8.38 → 0.8.40

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
 
@@ -76,7 +77,7 @@ export class SocketFunction {
76
77
  return shape as any as SocketExposedShape;
77
78
  });
78
79
 
79
- setImmediate(() => {
80
+ void Promise.resolve().then(() => {
80
81
  registerClass(classGuid, instance as SocketExposedInterface, getShape());
81
82
  });
82
83
 
@@ -115,7 +116,7 @@ export class SocketFunction {
115
116
  _classGuid: classGuid,
116
117
  };
117
118
 
118
- setImmediate(() => {
119
+ void Promise.resolve().then(() => {
119
120
  let onMount = getDefaultHooks?.().onMount;
120
121
  if (onMount) {
121
122
  let callbacks = SocketFunction.onMountCallbacks.get(classGuid);
@@ -200,6 +201,8 @@ export class SocketFunction {
200
201
  public static mountedNodeId: string = "";
201
202
  public static mountedIP: string = "";
202
203
  private static hasMounted = false;
204
+ private static onMountCallback: () => void = () => { };
205
+ public static mountPromise: Promise<void> = new Promise(r => this.onMountCallback = r);
203
206
  public static async mount(config: SocketServerConfig) {
204
207
  if (this.mountedNodeId) {
205
208
  throw new Error("SocketFunction already mounted, mounting twice in one thread is not allowed.");
@@ -209,6 +212,10 @@ export class SocketFunction {
209
212
  if (config.ip) {
210
213
  this.mountedIP = config.ip;
211
214
  }
215
+
216
+ // Wait for any additionals functions to expose themselves
217
+ await delay("immediate");
218
+
212
219
  this.mountedNodeId = await startSocketServer(config);
213
220
  this.hasMounted = true;
214
221
  for (let classGuid of SocketFunction.exposedClasses) {
@@ -218,6 +225,7 @@ export class SocketFunction {
218
225
  await callback();
219
226
  }
220
227
  }
228
+ this.onMountCallback();
221
229
  return this.mountedNodeId;
222
230
  }
223
231
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "socket-function",
3
- "version": "0.8.38",
3
+ "version": "0.8.40",
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",
@@ -10,7 +10,7 @@
10
10
  "node-forge": "https://github.com/sliftist/forge#name",
11
11
  "preact": "^10.10.6",
12
12
  "rdtsc-now": "^0.3.0",
13
- "typenode": "^4.9.4-b",
13
+ "typenode": "^4.9.4-f",
14
14
  "ws": "^8.8.0"
15
15
  },
16
16
  "scripts": {
@@ -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 };
@@ -242,6 +242,15 @@
242
242
  }
243
243
 
244
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
+ }
245
254
 
246
255
  let exportsOverride = undefined;
247
256
  if (resolvedPath === "NOTALLOWEDCLIENTSIDE" || !serializedModules[resolvedPath].allowclient) {
@@ -337,6 +346,7 @@
337
346
  module.id = resolvedId;
338
347
  module.filename = serializedModule?.filename;
339
348
  module.exports = {};
349
+ module.exports.default = module.exports;
340
350
  module.children = [];
341
351
 
342
352
  module.load = load;
@@ -383,19 +393,6 @@
383
393
 
384
394
  let dirname = module.filename.replace(/\\/g, "/").split("/").slice(0, -1).join("/");
385
395
 
386
- var __createBinding = (Object.create ? (function (o, m, k, k2) {
387
- if (k2 === undefined) k2 = k;
388
- Object.defineProperty(o, k2, { enumerable: true, get: function () { return m[k]; } });
389
- }) : (function (o, m, k, k2) {
390
- if (k2 === undefined) k2 = k;
391
- o[k2] = m[k];
392
- }));
393
- var __setModuleDefault = (Object.create ? (function (o, v) {
394
- Object.defineProperty(o, "default", { enumerable: true, value: v });
395
- }) : function (o, v) {
396
- o["default"] = v;
397
- });
398
-
399
396
  let time = Date.now();
400
397
  currentModuleEvaluationStack.push(module.filename);
401
398
  try {
@@ -405,21 +402,9 @@
405
402
  // which checks for unloadedModule and returns undefined in that case.
406
403
  __importStar(mod) {
407
404
  if (mod[unloadedModule]) return undefined;
408
- if (mod && mod.__esModule) return mod;
409
- var result = {};
410
- if (mod !== null && mod !== undefined) {
411
- for (var k in mod) {
412
- if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) {
413
- __createBinding(result, mod, k);
414
- }
415
- }
416
- }
417
- __setModuleDefault(result, mod);
418
- return result;
405
+ return mod;
419
406
  },
420
407
  __importDefault(mod) {
421
- // If typescript isn't going to complain about importing from a module with no default export,
422
- // then we'll just change our implementation to work the same way as typescript types...
423
408
  return mod.default ? mod : { default: mod };
424
409
  },
425
410
  },
@@ -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";
@@ -10,7 +10,9 @@ import { getClientNodeId, getNodeIdLocation, registerNodeClient } from "./nodeCa
10
10
  import debugbreak from "debugbreak";
11
11
  import { lazy } from "./caching";
12
12
  import { JSONLACKS } from "./JSONLACKS/JSONLACKS";
13
- import { red } from "./formatting/logColors";
13
+ import { red, yellow } from "./formatting/logColors";
14
+ import { isSplitableArray, markArrayAsSplitable } from "./fixLargeNetworkCalls";
15
+ import { delay } from "./batching";
14
16
 
15
17
  const MIN_RETRY_DELAY = 1000;
16
18
 
@@ -55,6 +57,8 @@ export interface SenderInterface {
55
57
  addEventListener(event: "message", listener: (data: ws.RawData | ws.MessageEvent | string) => void): void;
56
58
 
57
59
  readyState: number;
60
+
61
+ ping?(): void;
58
62
  }
59
63
 
60
64
  export async function createCallFactory(
@@ -81,6 +85,18 @@ export async function createCallFactory(
81
85
  // in return calls.
82
86
  let nextSeqNum = Date.now() + Math.random();
83
87
 
88
+ // NOTE: I'm not sure if this is needed, I thought it was, but... now I think
89
+ // it probably isn't...
90
+ // if (webSocketBase?.readyState === 1 /* OPEN */ && webSocketBase.ping) {
91
+ // // Heartbeat loop, otherwise onDisconnect is never called.
92
+ // ((async () => {
93
+ // while (webSocketBase?.readyState === 1 /* OPEN */ && webSocketBase.ping) {
94
+ // await delay(1000 * 60);
95
+ // webSocketBase.ping?.();
96
+ // }
97
+ // }))().catch(() => { });
98
+ // }
99
+
84
100
  let lastConnectionAttempt = 0;
85
101
 
86
102
  let callerContext: CallerContextBase = {
@@ -115,6 +131,35 @@ export async function createCallFactory(
115
131
  if (time > SocketFunction.WIRE_WARN_TIME) {
116
132
  console.log(red(`Slow serialize, took ${time}ms to serialize ${data.byteLength} bytes. For ${call.classGuid}.${call.functionName}`));
117
133
  }
134
+
135
+ if (data.byteLength > SocketFunction.MAX_MESSAGE_SIZE * 1.5) {
136
+ let splitArgIndex = call.args.findIndex(isSplitableArray);
137
+ if (splitArgIndex >= 0) {
138
+ console.log(yellow(`Splitting large call due to large args: ${call.classGuid}.${call.functionName}`));
139
+ let SPLIT_GROUPS = 10;
140
+ let splitArg = call.args[splitArgIndex] as unknown[];
141
+ let subCalls = list(SPLIT_GROUPS).map(index => {
142
+ let start = Math.floor(index / SPLIT_GROUPS * splitArg.length);
143
+ let end = Math.floor((index + 1) / SPLIT_GROUPS * splitArg.length);
144
+ return splitArg.slice(start, end);
145
+ }).filter(x => x.length > 0);
146
+
147
+ let calls = subCalls.map(async splitList => {
148
+ let subCall = { ...call };
149
+ subCall.args = subCall.args.slice();
150
+ subCall.args[splitArgIndex] = markArrayAsSplitable(splitList);
151
+ await callFactory.performCall(subCall);
152
+ });
153
+ await Promise.allSettled(calls);
154
+ await Promise.all(calls);
155
+ // Eh... we COULD return the array of results, but... then the result would sometimes be an array,
156
+ // some times not, so, it is better to return a string which will make it more clear why it sometimes varies.
157
+ return "CALLS_SPLIT_DUE_TO_LARGE_ARGS";
158
+ }
159
+
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.`);
161
+ }
162
+
118
163
  let resultPromise = new Promise((resolve, reject) => {
119
164
  let callback = (result: InternalReturnType) => {
120
165
  if (SocketFunction.logMessages) {
@@ -130,10 +175,6 @@ export async function createCallFactory(
130
175
  pendingCalls.set(seqNum, { callback, data, call: fullCall });
131
176
  });
132
177
 
133
- if (data.byteLength > SocketFunction.MAX_MESSAGE_SIZE * 1.5) {
134
- 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.`);
135
- }
136
-
137
178
  await send(data);
138
179
 
139
180
  return await resultPromise;
@@ -333,7 +374,7 @@ export async function createCallFactory(
333
374
  }
334
375
  throw new Error(`Unhandled data type ${typeof message}`);
335
376
  } catch (e: any) {
336
- debugbreak(1);
377
+ debugbreak(2);
337
378
  debugger;
338
379
  console.error(e.stack);
339
380
  }
package/src/batching.ts CHANGED
@@ -91,4 +91,37 @@ export function runInSerial<T extends (...args: any[]) => Promise<any>>(fnc: T):
91
91
  updateQueue[0]?.();
92
92
  }
93
93
  }) as T;
94
+ }
95
+
96
+ export function runInfinitePoll(
97
+ delayTime: number,
98
+ fnc: () => Promise<void> | void
99
+ ) {
100
+ void (async () => {
101
+ while (true) {
102
+ await delay(delayTime);
103
+ try {
104
+ await fnc();
105
+ } catch (e: any) {
106
+ console.error(`Error in infinite poll ${fnc.name || fnc.toString().slice(0, 100).split("\n").slice(0, 2).join("\n")} (continuing poll loop)\n${e.stack}`);
107
+ }
108
+ }
109
+ })();
110
+ }
111
+
112
+ export async function runInfinitePollCallAtStart(
113
+ delayTime: number,
114
+ fnc: () => Promise<void> | void
115
+ ) {
116
+ void (async () => {
117
+ while (true) {
118
+ await delay(delayTime);
119
+ try {
120
+ await fnc();
121
+ } catch (e: any) {
122
+ console.error(`Error in infinite poll ${fnc.name} (continuing poll loop)\n${e.stack}`);
123
+ }
124
+ }
125
+ })();
126
+ return await fnc();
94
127
  }
package/src/caching.ts CHANGED
@@ -3,7 +3,6 @@ import { AnyFunction, Args, canHaveChildren } from "./types";
3
3
 
4
4
  export function lazy<T>(factory: () => T): () => T {
5
5
  let value: { value: T } | undefined = undefined;
6
-
7
6
  return () => {
8
7
  if (!value) {
9
8
  value = { value: factory() };
@@ -33,8 +32,10 @@ export function cacheEmptyArray<T>(array: T[]): T[] {
33
32
 
34
33
  export function cache<Output, Key>(getValue: (key: Key) => Output): {
35
34
  (key: Key): Output;
35
+ // NOTE: If you want to clear all, just make a new cache!
36
36
  clear(key: Key): void;
37
37
  forceSet(key: Key, value: Output): void;
38
+ getAllKeys(): Key[];
38
39
  } {
39
40
  let startingCalculating = new Set<Key>();
40
41
  let values = new Map<Key, Output>();
@@ -61,6 +62,9 @@ export function cache<Output, Key>(getValue: (key: Key) => Output): {
61
62
  values.set(key, value);
62
63
  startingCalculating.add(key);
63
64
  };
65
+ cache.getAllKeys = () => {
66
+ return [...values.keys()];
67
+ };
64
68
  return cache;
65
69
  }
66
70
 
@@ -0,0 +1,9 @@
1
+ const arrayIsSplitable = Symbol.for("arrayIsSplitable");
2
+ export function markArrayAsSplitable<T>(data: T[]): T[] {
3
+ (data as any)[arrayIsSplitable] = true;
4
+ return data;
5
+ }
6
+ export function isSplitableArray<T>(data: T): data is T & (unknown[]) {
7
+ if (!Array.isArray(data)) return false;
8
+ return !!(data as any)[arrayIsSplitable];
9
+ }
package/src/misc.ts CHANGED
@@ -139,6 +139,9 @@ export function getKeys(obj: unknown) {
139
139
  }
140
140
  return keyArray;
141
141
  }
142
+ export function getStringKeys<T extends {}>(obj: T): ((keyof T) & string)[] {
143
+ return Object.keys(obj) as any;
144
+ }
142
145
 
143
146
  if (isNode()) {
144
147
  // TODO: Find a better place for this...
@@ -1,6 +1,6 @@
1
1
  import debugbreak from "debugbreak";
2
2
  import { formatTime, formatNumber } from "../formatting/format";
3
- import { red, yellow, blue } from "../formatting/logColors";
3
+ import { red, yellow, blue, magenta } from "../formatting/logColors";
4
4
 
5
5
  import { getOwnTime, getPendingOwnTimeInstances, getPendingOwnTimeObjs, OwnTimeObj } from "./getOwnTime";
6
6
  import { addToStats, addToStatsValue, createStatsValue, getStatsTop, StatsValue } from "./stats";
@@ -12,6 +12,10 @@ export function enableMeasurements() {
12
12
  }
13
13
  measurementsEnabled = true;
14
14
  }
15
+ /** NOTE: Must be called BEFORE anything else is imported! */
16
+ export function disableMeasurements() {
17
+ measurementsEnabled = false;
18
+ }
15
19
 
16
20
  let functionsSkipped = 0;
17
21
 
@@ -104,9 +108,9 @@ export function logMeasureTable(
104
108
  let totalTime = entries.map(x => getTime(x).sum).reduce((a, b) => a + b, 0);
105
109
 
106
110
  console.log();
107
- let title = yellow(`Profiled ${formatTime(totalTime)}`);
111
+ let title = yellow(`Profiled ${formatTime(totalTime)} (logged at ${new Date().toISOString()})`);
108
112
  if (name) {
109
- title += ` ${blue(name)}`;
113
+ title = `(${blue(name)}) ${title}`;
110
114
  }
111
115
  console.log(title);
112
116
  function percent(value: number) {
@@ -138,7 +142,7 @@ export function logMeasureTable(
138
142
 
139
143
  let ownTimeTop = getStatsTop(getTime(entry));
140
144
  if (ownTimeTop.topHeavy) {
141
- output += red(` TOP ${percent(ownTimeTop.valueFraction)} of the time is owned by ${percent(ownTimeTop.countFraction)} of the calls`);
145
+ output += red(` TOP ${percent(ownTimeTop.valueFraction)} of the time is owned by ${percent(ownTimeTop.countFraction)} of the calls (${formatTime(ownTimeTop.value / ownTimeTop.count)} per * ${formatNumber(ownTimeTop.count)} = ${formatTime(ownTimeTop.value)})`);
142
146
  }
143
147
 
144
148
  if (entry.stillOpenCount > 0) {
@@ -209,7 +213,7 @@ interface ProfileEntry {
209
213
  stillOpenCount: number;
210
214
  }
211
215
 
212
- let measurementsEnabled = false;
216
+ let measurementsEnabled = true;
213
217
 
214
218
  let outstandingProfiles: MeasureProfile[] = [];
215
219
  function recordOwnTime(ownTimeObj: OwnTimeObj) {
@@ -12,6 +12,7 @@ import debugbreak from "debugbreak";
12
12
  import { getNodeId } from "./nodeCache";
13
13
  import crypto from "crypto";
14
14
  import { Watchable } from "./misc";
15
+ import { delay, runInfinitePoll } from "./batching";
15
16
 
16
17
  export type SocketServerConfig = (
17
18
  https.ServerOptions & {
@@ -38,7 +38,7 @@ export function createWebsocketFactory(): (nodeId: string) => SenderInterface {
38
38
  console.log(`Connecting to ${address}:${port}`);
39
39
  }
40
40
  let webSocket = new ws.WebSocket(`wss://${address}:${port}`, {
41
- ca: tls.rootCertificates.concat(getTrustedCertificates()),
41
+ ca: getTrustedCertificates()
42
42
  });
43
43
  let result = Object.assign(webSocket, { socket: undefined as tls.TLSSocket | undefined });
44
44
  webSocket.once("upgrade", e => {