socket-function 0.8.39 → 0.9.0

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
@@ -201,6 +201,8 @@ export class SocketFunction {
201
201
  public static mountedNodeId: string = "";
202
202
  public static mountedIP: string = "";
203
203
  private static hasMounted = false;
204
+ private static onMountCallback: () => void = () => { };
205
+ public static mountPromise: Promise<void> = new Promise(r => this.onMountCallback = r);
204
206
  public static async mount(config: SocketServerConfig) {
205
207
  if (this.mountedNodeId) {
206
208
  throw new Error("SocketFunction already mounted, mounting twice in one thread is not allowed.");
@@ -223,6 +225,7 @@ export class SocketFunction {
223
225
  await callback();
224
226
  }
225
227
  }
228
+ this.onMountCallback();
226
229
  return this.mountedNodeId;
227
230
  }
228
231
 
@@ -246,6 +249,10 @@ export class SocketFunction {
246
249
  return getNodeId(location.address, location.port);
247
250
  }
248
251
 
252
+ public static locationNode() {
253
+ return SocketFunction.connect({ address: location.hostname, port: +location.port });
254
+ }
255
+
249
256
  public static addGlobalHook(hook: SocketFunctionHook<SocketExposedInterface>) {
250
257
  registerGlobalHook(hook as SocketFunctionHook);
251
258
  }
@@ -5,12 +5,25 @@ module.allowclient = true;
5
5
  import { SocketFunction } from "../SocketFunction";
6
6
  import { cache, lazy } from "../src/caching";
7
7
  import * as fs from "fs";
8
+ import debugbreak from "debugbreak";
9
+ import { isNode } from "../src/misc";
10
+ import { red } from "../src/formatting/logColors";
8
11
 
9
- /** Hot reloads server and client files, just trigger a refresh clientside,
10
- * while triggering per file re-evaluation and export updates serverside.
11
- * - Requires HotReloadController to be exposed both serverside and clientside.
12
+ /** Enables some hot reload functionality.
13
+ * - Triggers a refresh clientside
14
+ * - Triggers a reload server, for modules marked with `module.hotreload`
12
15
  */
13
- export function watchFilesAndTriggerHotReloading() {
16
+ export function watchFilesAndTriggerHotReloading(noAutomaticBrowserWatch = false) {
17
+
18
+ SocketFunction.expose(HotReloadController);
19
+ if (!isNode()) {
20
+ if (!noAutomaticBrowserWatch) {
21
+ HotReloadController.nodes[SocketFunction.locationNode()]
22
+ .watchFiles()
23
+ .catch(e => console.error("watchFiles error", e))
24
+ ;
25
+ }
26
+ }
14
27
  setInterval(() => {
15
28
  for (let module of Object.values(require.cache)) {
16
29
  if (!module) continue;
@@ -19,6 +32,14 @@ export function watchFilesAndTriggerHotReloading() {
19
32
  }, 5000);
20
33
  }
21
34
 
35
+ declare global {
36
+ namespace NodeJS {
37
+ interface Module {
38
+ hotreload?: boolean;
39
+ noserverhotreload?: boolean;
40
+ }
41
+ }
42
+ }
22
43
 
23
44
  const hotReloadModule = cache((module: NodeJS.Module) => {
24
45
  if (!module.updateContents) return;
@@ -26,6 +47,23 @@ const hotReloadModule = cache((module: NodeJS.Module) => {
26
47
  if (curr.mtime.getTime() === prev.mtime.getTime()) return;
27
48
  console.log(`Hot reloading due to change: ${module.filename}`);
28
49
  module.updateContents?.();
50
+ if (isNode()) {
51
+ if (
52
+ module.hotreload
53
+ // A fairly big hack (as this could just be in a string, or something similar), but... it also VERY useful
54
+ || module.moduleContents?.includes("\nmodule.hotreload = true;" + "\n")
55
+ || module.moduleContents?.includes("\r\nmodule.hotreload = true;" + "\r\n")
56
+ ) {
57
+ console.log(`Reloading ${module.id}`);
58
+ try {
59
+ module.loaded = false;
60
+ module.load(module.id);
61
+ } catch (e) {
62
+ console.error(red(`Error hot reloading ${module.id}`));
63
+ console.error(e);
64
+ }
65
+ }
66
+ }
29
67
  triggerClientSideReload();
30
68
  });
31
69
  });
@@ -50,10 +88,7 @@ class HotReloadControllerBase {
50
88
  // TODO: Also hot reload when we reconnect to the server, as it is likely setup will need to
51
89
  // be rerun in that case as well (for example, we need to call watchFiles again!)
52
90
  async watchFiles() {
53
- let callerId = HotReloadController.context.caller?.nodeId;
54
- if (!callerId) {
55
- throw new Error("No nodeId?");
56
- }
91
+ let callerId = SocketFunction.getCaller().nodeId;
57
92
  clientWatcherNodes.add(callerId);
58
93
  }
59
94
  async fileUpdated() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "socket-function",
3
- "version": "0.8.39",
3
+ "version": "0.9.0",
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,8 +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
- "rdtsc-now": "^0.3.0",
13
- "typenode": "^4.9.4-c",
12
+ "rdtsc-now": "^0.3.1",
13
+ "typenode": "^4.9.4-g",
14
14
  "ws": "^8.8.0"
15
15
  },
16
16
  "scripts": {
@@ -1,4 +1,6 @@
1
1
  (function () {
2
+ //# sourceURL=require.js
3
+
2
4
  // Globals
3
5
  Object.assign(window, {
4
6
  process: {
@@ -10,8 +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";
14
- import { isSplitableArray } from "./fixLargeNetworkCalls";
13
+ import { red, yellow } from "./formatting/logColors";
14
+ import { isSplitableArray, markArrayAsSplitable } from "./fixLargeNetworkCalls";
15
+ import { delay } from "./batching";
15
16
 
16
17
  const MIN_RETRY_DELAY = 1000;
17
18
 
@@ -56,6 +57,8 @@ export interface SenderInterface {
56
57
  addEventListener(event: "message", listener: (data: ws.RawData | ws.MessageEvent | string) => void): void;
57
58
 
58
59
  readyState: number;
60
+
61
+ ping?(): void;
59
62
  }
60
63
 
61
64
  export async function createCallFactory(
@@ -82,6 +85,18 @@ export async function createCallFactory(
82
85
  // in return calls.
83
86
  let nextSeqNum = Date.now() + Math.random();
84
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
+
85
100
  let lastConnectionAttempt = 0;
86
101
 
87
102
  let callerContext: CallerContextBase = {
@@ -120,6 +135,7 @@ export async function createCallFactory(
120
135
  if (data.byteLength > SocketFunction.MAX_MESSAGE_SIZE * 1.5) {
121
136
  let splitArgIndex = call.args.findIndex(isSplitableArray);
122
137
  if (splitArgIndex >= 0) {
138
+ console.log(yellow(`Splitting large call due to large args: ${call.classGuid}.${call.functionName}`));
123
139
  let SPLIT_GROUPS = 10;
124
140
  let splitArg = call.args[splitArgIndex] as unknown[];
125
141
  let subCalls = list(SPLIT_GROUPS).map(index => {
@@ -127,12 +143,15 @@ export async function createCallFactory(
127
143
  let end = Math.floor((index + 1) / SPLIT_GROUPS * splitArg.length);
128
144
  return splitArg.slice(start, end);
129
145
  }).filter(x => x.length > 0);
130
- for (let splitList of subCalls) {
146
+
147
+ let calls = subCalls.map(async splitList => {
131
148
  let subCall = { ...call };
132
149
  subCall.args = subCall.args.slice();
133
- subCall.args[splitArgIndex] = splitList;
150
+ subCall.args[splitArgIndex] = markArrayAsSplitable(splitList);
134
151
  await callFactory.performCall(subCall);
135
- }
152
+ });
153
+ await Promise.allSettled(calls);
154
+ await Promise.all(calls);
136
155
  // Eh... we COULD return the array of results, but... then the result would sometimes be an array,
137
156
  // some times not, so, it is better to return a string which will make it more clear why it sometimes varies.
138
157
  return "CALLS_SPLIT_DUE_TO_LARGE_ARGS";
@@ -355,9 +374,15 @@ export async function createCallFactory(
355
374
  }
356
375
  throw new Error(`Unhandled data type ${typeof message}`);
357
376
  } catch (e: any) {
358
- debugbreak(1);
359
- debugger;
360
- console.error(e.stack);
377
+ // NOTE: I'm looking for all types of errors here (specifically, .send errors), in case
378
+ // there are errors I should be handling.
379
+ if (e.stack.startsWith("Error: Cannot send data to") && e.stack.includes("as the connection has closed")) {
380
+ // This is fine, just ignore it
381
+ } else {
382
+ debugbreak(2);
383
+ debugger;
384
+ console.error(e.stack);
385
+ }
361
386
  }
362
387
  }
363
388
 
package/src/batching.ts CHANGED
@@ -113,15 +113,18 @@ export async function runInfinitePollCallAtStart(
113
113
  delayTime: number,
114
114
  fnc: () => Promise<void> | void
115
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}`);
116
+ try {
117
+ return await fnc();
118
+ } finally {
119
+ void (async () => {
120
+ while (true) {
121
+ await delay(delayTime);
122
+ try {
123
+ await fnc();
124
+ } catch (e: any) {
125
+ console.error(`Error in infinite poll ${fnc.name} (continuing poll loop)\n${e.stack}`);
126
+ }
123
127
  }
124
- }
125
- })();
126
- return await fnc();
128
+ })();
129
+ }
127
130
  }
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
 
@@ -158,7 +158,7 @@ export async function httpCallHandler(request: http.IncomingMessage, response: h
158
158
 
159
159
 
160
160
  // NOTE: Our ETag caching is only to reduce data sent on the wire, we evaluate the calls
161
- // every time (so it is strictly a wire cache, not a computation cache)
161
+ // every time (so it is strictly a wire cache for HTTP, not a computation cache)
162
162
  if (SocketFunction.httpETagCache) {
163
163
  response.setHeader("cache-control", "private, max-age=0, must-revalidate");
164
164
  let hash = sha256Hash(resultBuffer);
package/src/certStore.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as tls from "tls";
2
- import { sha256Hash } from "./misc";
2
+ import { isNode, sha256Hash } from "./misc";
3
3
 
4
4
  let trustedCerts = new Set<string>();
5
5
  let watchCallbacks = new Set<(certs: string[]) => void>();
@@ -15,8 +15,12 @@ export function trustCertificate(cert: string | Buffer) {
15
15
  }
16
16
  }
17
17
  export function getTrustedCertificates(): string[] {
18
- //console.log(`trustedCerts = ${Array.from(trustedCerts).map(x => sha256Hash(x).slice(0, 10))}`);
19
- return tls.rootCertificates.concat(Array.from(trustedCerts));
18
+ let certs: string[] = [];
19
+ if (isNode()) {
20
+ certs.push(...tls.rootCertificates);
21
+ }
22
+ certs.push(...Array.from(trustedCerts));
23
+ return certs;
20
24
  }
21
25
 
22
26
  export function watchTrustedCertificates(callback: (certs: string[]) => void) {
@@ -5,5 +5,5 @@ export function markArrayAsSplitable<T>(data: T[]): T[] {
5
5
  }
6
6
  export function isSplitableArray<T>(data: T): data is T & (unknown[]) {
7
7
  if (!Array.isArray(data)) return false;
8
- return (data as any)[arrayIsSplitable] === true;
8
+ return !!(data as any)[arrayIsSplitable];
9
9
  }
package/src/misc.ts CHANGED
@@ -139,10 +139,34 @@ 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...
145
148
  process.on("unhandledRejection", async (reason: any, promise) => {
146
149
  console.error(`Uncaught promise rejection: ${String(reason.stack || reason)}`);
147
150
  });
151
+ }
152
+
153
+ export function keyBy<T, K>(arr: T[], getKey: (value: T) => K): Map<K, T> {
154
+ let map = new Map<K, T>();
155
+ for (let item of arr) {
156
+ map.set(getKey(item), item);
157
+ }
158
+ return map;
159
+ }
160
+ export function keyByArray<T, K>(arr: T[], getKey: (value: T) => K): Map<K, T[]> {
161
+ let map = new Map<K, T[]>();
162
+ for (let item of arr) {
163
+ let key = getKey(item);
164
+ let arr = map.get(key);
165
+ if (!arr) {
166
+ arr = [];
167
+ map.set(key, arr);
168
+ }
169
+ arr.push(item);
170
+ }
171
+ return map;
148
172
  }
@@ -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";
@@ -108,9 +108,9 @@ export function logMeasureTable(
108
108
  let totalTime = entries.map(x => getTime(x).sum).reduce((a, b) => a + b, 0);
109
109
 
110
110
  console.log();
111
- let title = yellow(`Profiled ${formatTime(totalTime)}`);
111
+ let title = yellow(`Profiled ${formatTime(totalTime)} (logged at ${new Date().toISOString()})`);
112
112
  if (name) {
113
- title += ` ${blue(name)}`;
113
+ title = `(${blue(name)}) ${title}`;
114
114
  }
115
115
  console.log(title);
116
116
  function percent(value: number) {
@@ -142,7 +142,7 @@ export function logMeasureTable(
142
142
 
143
143
  let ownTimeTop = getStatsTop(getTime(entry));
144
144
  if (ownTimeTop.topHeavy) {
145
- 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)})`);
146
146
  }
147
147
 
148
148
  if (entry.stillOpenCount > 0) {
@@ -12,6 +12,10 @@ 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";
16
+ import { magenta } from "./formatting/logColors";
17
+ import { yellow } from "./formatting/logColors";
18
+ import { green } from "./formatting/logColors";
15
19
 
16
20
  export type SocketServerConfig = (
17
21
  https.ServerOptions & {
@@ -27,6 +31,11 @@ export type SocketServerConfig = (
27
31
  public?: boolean;
28
32
  ip?: string;
29
33
 
34
+ // NOTE: Any same origin accesses are allowed (header.origin === header.host)
35
+ // For example, to allow "letx.ca" to access the server (when the hosted domain
36
+ // may be, "querysub.com", for example), use ["letx.ca"]
37
+ allowHostnames?: string[];
38
+
30
39
  /** If the SNI matches this domain, we use a different key/cert. */
31
40
  SNICerts?: {
32
41
  [domain: string]: Watchable<https.ServerOptions>;
@@ -58,6 +67,11 @@ export async function startSocketServer(
58
67
  });
59
68
  let httpsServer = await httpServerPromise;
60
69
 
70
+ let allowedHostnames = new Set<string>();
71
+ for (let hostname of config.allowHostnames || []) {
72
+ allowedHostnames.add(hostname);
73
+ }
74
+
61
75
  watchTrustedCertificates(() => {
62
76
  lastOptions.ca = getTrustedCertificates();
63
77
  httpsServer.setSecureContext(lastOptions);
@@ -99,8 +113,8 @@ export async function startSocketServer(
99
113
  try {
100
114
  let host = new URL("ws://" + request.headers["host"]).hostname;
101
115
  let origin = new URL(originHeader).hostname;
102
- if (host !== origin) {
103
- throw new Error(`Invalid cross thread request, ${JSON.stringify(host)} !== ${JSON.stringify(origin)}`);
116
+ if (host !== origin && !allowedHostnames.has(origin)) {
117
+ throw new Error(`Invalid cross domain request, ${JSON.stringify(host)} !== ${JSON.stringify(origin)} (also not config.allowedHostnames ${JSON.stringify(config.allowHostnames)})`);
104
118
  }
105
119
  } catch (e) {
106
120
  console.error(e);
@@ -213,7 +227,7 @@ export async function startSocketServer(
213
227
  }
214
228
 
215
229
  if (!SocketFunction.silent) {
216
- console.log(`Trying to listening on ${host}:${port}`);
230
+ console.log(yellow(`Trying to listening on ${host}:${port}`));
217
231
  }
218
232
  realServer.listen(port, host);
219
233
 
@@ -222,7 +236,7 @@ export async function startSocketServer(
222
236
  port = (realServer.address() as net.AddressInfo).port;
223
237
  let nodeId = getNodeId(getCommonName(config.cert), port);
224
238
  if (!SocketFunction.silent) {
225
- console.log(`Started Listening on ${nodeId}`);
239
+ console.log(green(`Started Listening on ${nodeId}`));
226
240
  }
227
241
 
228
242
  return nodeId;
@@ -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 => {
package/test/shared.ts CHANGED
@@ -8,19 +8,13 @@ class TestBase {
8
8
  memberVariable = 5;
9
9
 
10
10
  async add(lhs: number, rhs: number) {
11
- let caller = Test.context.caller?.nodeId;
12
- if (!caller) {
13
- throw new Error("No caller?");
14
- }
11
+ let caller = SocketFunction.getCaller().nodeId;
15
12
  console.log(`Caller is ${caller}`);
16
13
  return lhs + rhs;
17
14
  }
18
15
 
19
16
  async callMe() {
20
- let caller = Test.context.caller?.nodeId;
21
- if (!caller) {
22
- throw new Error("No caller?");
23
- }
17
+ let caller = SocketFunction.getCaller().nodeId;
24
18
  console.log(`Caller is ${caller}`);
25
19
  void (async () => {
26
20
  let seqNum = 1;