socket-function 0.12.16 → 0.14.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/src/batching.ts CHANGED
@@ -11,8 +11,19 @@ import { MaybePromise } from "./types";
11
11
  - The ensures a prompt return, without resorting to setTimeout in the browser (which will cause
12
12
  the callback to be delayed a frame).
13
13
  */
14
- export type DelayType = number | "afterio" | "immediate" | "afterpromises";
15
- export function delay(delayTime: DelayType): Promise<void> {
14
+ export type DelayType = (
15
+ number | "afterio" | "immediate" | "afterpromises"
16
+ // Waits for paint, usable in a loop. The first wait doesn't wait until the next
17
+ // wait, but the second wait will.
18
+ | "paintLoop"
19
+ // Waits until after paint, by waiting twice.
20
+ | "afterPaint"
21
+ )
22
+ export function delay(
23
+ delayTime: DelayType,
24
+ // Delays < 10ms become "immediate"
25
+ immediateShortDelays?: "immediateShortDelays"
26
+ ): Promise<void> {
16
27
  if (delayTime === "afterio") {
17
28
  return new Promise<void>(resolve => setImmediate(resolve));
18
29
  } else if (delayTime === "afterpromises") {
@@ -25,16 +36,45 @@ export function delay(delayTime: DelayType): Promise<void> {
25
36
  } else {
26
37
  return delay("afterpromises");
27
38
  }
39
+ } else if (delayTime === "paintLoop") {
40
+ if (isNode()) {
41
+ return delay("immediate");
42
+ }
43
+ return (async () => {
44
+ await new Promise(resolve => requestAnimationFrame(resolve));
45
+ })();
46
+ } else if (delayTime === "afterPaint") {
47
+ if (isNode()) {
48
+ return delay("immediate");
49
+ } else {
50
+ return (async () => {
51
+ await new Promise(resolve => requestAnimationFrame(resolve));
52
+ // Before first paint
53
+ await new Promise(resolve => requestAnimationFrame(resolve));
54
+ // After first paint
55
+ })();
56
+ }
28
57
  } else {
29
- // NOTE: setTimeout can't wait this short of a time, so just setImmediate. This should be hard to distinguish
30
- // anyways, as setImmediate (at least in nodejs), should happen after io, so... it should just work
31
- // (the only difference is there will be less unnecessary delay).
32
- // NOTE: THIS DOES break certain cases where io is depending on true delay, and by only waiting a microtick
33
- // we don't give it a chance. But... we should just handle those cases explicitly, via an explicit "afterio".
34
- if (delayTime < 10) {
58
+
59
+ if (delayTime < 10 && immediateShortDelays) {
60
+ // NOTE: setTimeout can't wait this short of a time, so just setImmediate. This should be hard to distinguish
61
+ // anyways, as setImmediate (at least in nodejs), should happen after io, so... it should just work
62
+ // (the only difference is there will be less unnecessary delay).
63
+ // NOTE: THIS DOES break certain cases where io is depending on true delay, and by only waiting a microtick
64
+ // we don't give it a chance. But... we should just handle those cases explicitly, via an explicit "afterio".
35
65
  return delay("immediate");
36
66
  }
37
- return new Promise<void>(resolve => setTimeout(resolve, delayTime));
67
+ // NOTE: We check Date.now() and wait longer if setTimeout didn't wait long enough.
68
+ return (async () => {
69
+ let targetTime = Date.now() + delayTime;
70
+ while (true) {
71
+ let timeToWait = targetTime - Date.now();
72
+ await new Promise<void>(resolve => setTimeout(resolve, timeToWait));
73
+ if (Date.now() >= targetTime) {
74
+ break;
75
+ }
76
+ }
77
+ })();
38
78
  }
39
79
  }
40
80
 
@@ -110,7 +150,7 @@ export function batchFunction<Arg, Result = void>(
110
150
  let args: Arg[] = [arg];
111
151
  let promise = Promise.resolve().then(async () => {
112
152
  await curPrevPromise;
113
- await delay(curDelay);
153
+ await delay(curDelay, "immediateShortDelays");
114
154
  // Reset batching, as we once we start the function we can't accept args. `prevPromise` will block
115
155
  // the next batch from starting before we finish.
116
156
  batching = undefined;
package/src/caching.ts CHANGED
@@ -37,8 +37,10 @@ export function cacheEmptyArray<T>(array: T[]): T[] {
37
37
  return array;
38
38
  }
39
39
 
40
- export function cache<Output, Key>(getValue: (key: Key) => Output): {
41
- (key: Key): Output;
40
+ export function cache<Output, Key, Untracked extends unknown[]>(
41
+ getValue: (key: Key, ...untracked: Untracked) => Output
42
+ ): {
43
+ (key: Key, ...untracked: Untracked): Output;
42
44
  // NOTE: If you want to clear all, just make a new cache!
43
45
  clear(key: Key): void;
44
46
  clearAll(): void;
@@ -48,7 +50,7 @@ export function cache<Output, Key>(getValue: (key: Key) => Output): {
48
50
  } {
49
51
  let startingCalculating = new Set<Key>();
50
52
  let values = new Map<Key, Output>();
51
- function cache(input: Key) {
53
+ function cache(input: Key, ...untracked: Untracked) {
52
54
  let key = input;
53
55
  if (values.has(key)) {
54
56
  return values.get(key) as any;
@@ -59,7 +61,7 @@ export function cache<Output, Key>(getValue: (key: Key) => Output): {
59
61
  return undefined;
60
62
  }
61
63
  startingCalculating.add(key);
62
- let value = getValue(input);
64
+ let value = getValue(input, ...untracked);
63
65
  values.set(key, value);
64
66
  return value;
65
67
  }
@@ -1,6 +1,6 @@
1
- import { CallerContext, CallType, ClientHookContext, FullCallType, HookContext, SocketExposedInterface, SocketExposedInterfaceClass, SocketExposedShape, SocketFunctionClientHook, SocketFunctionHook, SocketRegistered } from "../SocketFunctionTypes";
1
+ import { CallerContext, CallType, ClientHookContext, FullCallType, FunctionFlags, HookContext, SocketExposedInterface, SocketExposedInterfaceClass, SocketExposedShape, SocketFunctionClientHook, SocketFunctionHook, SocketRegistered } from "../SocketFunctionTypes";
2
2
  import { _setSocketContext } from "../SocketFunction";
3
- import { isNode } from "./misc";
3
+ import { entries, isNode } from "./misc";
4
4
  import debugbreak from "debugbreak";
5
5
  import { measureWrap } from "./profiling/measure";
6
6
 
@@ -15,6 +15,9 @@ let exposedClasses = new Set<string>();
15
15
  let globalHooks: SocketFunctionHook[] = [];
16
16
  let globalClientHooks: SocketFunctionClientHook[] = [];
17
17
 
18
+ export function getCallFlags(call: CallType): FunctionFlags | undefined {
19
+ return classes[call.classGuid]?.shape[call.functionName];
20
+ }
18
21
  export function shouldCompressCall(call: CallType) {
19
22
  return !!classes[call.classGuid]?.shape[call.functionName]?.compress;
20
23
  }
@@ -63,11 +66,26 @@ export function isDataImmutable(call: CallType) {
63
66
  return !!classes[call.classGuid]?.shape[call.functionName]?.dataImmutable;
64
67
  }
65
68
 
66
- export function registerClass(classGuid: string, controller: SocketExposedInterface, shape: SocketExposedShape) {
69
+ export function registerClass(classGuid: string, controller: SocketExposedInterface, shape: SocketExposedShape, config?: {
70
+ noFunctionMeasure?: boolean;
71
+ }) {
67
72
  if (classes[classGuid]) {
68
73
  throw new Error(`Class ${classGuid} already registered`);
69
74
  }
70
75
 
76
+ if (!config?.noFunctionMeasure) {
77
+ let keys = new Set([
78
+ ...Object.keys(controller),
79
+ ...Object.getOwnPropertyNames(controller.__proto__ || {}),
80
+ ]);
81
+ let niceClassName = controller.constructor.name || classGuid;
82
+ for (let functionName of keys) {
83
+ if (functionName === "constructor") continue;
84
+ let fnc = controller[functionName];
85
+ if (typeof fnc !== "function") continue;
86
+ controller[functionName] = measureWrap(fnc, `${niceClassName}().${functionName}`);
87
+ }
88
+ }
71
89
  classes[classGuid] = {
72
90
  controller,
73
91
  shape,
@@ -122,6 +122,7 @@ export function formatMaxDecimals(num: number, targetDigits: number, maxAbsolute
122
122
 
123
123
  /** Actually formats any number, including decimals, by using K, M and B suffixes to get smaller values
124
124
  * TODO: Support uK, uM and uB suffixes for very small numbers?
125
+ * <= 6 characters (<= 5 if positive)
125
126
  */
126
127
  export function formatNumber(count: number | undefined, maxAbsoluteValue?: number, noDecimal?: boolean, specialCurrency?: boolean): string {
127
128
  if (typeof count !== "number") return "0";
@@ -230,4 +231,11 @@ export function formatDate(time: number) {
230
231
  }
231
232
  let date = new Date(time);
232
233
  return date.getFullYear() + "/" + p(date.getMonth() + 1) + "/" + p(date.getDate());
234
+ }
235
+
236
+ /** <= 6 characters (<= 5 if positive) */
237
+ export function formatPercent(value: number) {
238
+ if (Number.isNaN(value)) return "0%";
239
+ // 1 decimal point, so we have 5 characters (just like formatNumber returns 5 characters)
240
+ return Math.round(value * 1000) / 10 + "%";
233
241
  }
package/src/misc.ts CHANGED
@@ -324,6 +324,16 @@ export function compare(lhs: unknown, rhs: unknown): number {
324
324
  return compare(typeof lhs, typeof rhs);
325
325
  }
326
326
  if (lhs === rhs) return 0;
327
+ if (lhs === null && rhs !== null) return -1;
328
+ if (lhs !== null && rhs === null) return 1;
329
+ if (typeof lhs === "number") {
330
+ if (Number.isNaN(lhs)) {
331
+ if (Number.isNaN(rhs)) return 0;
332
+ return -1;
333
+ } else {
334
+ if (Number.isNaN(rhs)) return +1;
335
+ }
336
+ }
327
337
  if (lhs as any < (rhs as any)) return -1;
328
338
  return 1;
329
339
  }
package/src/nodeCache.ts CHANGED
@@ -18,6 +18,7 @@ export function getNodeId(domain: string, port: number): string {
18
18
  return `${domain}:${port}`;
19
19
  }
20
20
 
21
+ /** @deprecated, call getBrowserUrlNode instead, which does important additional checks. */
21
22
  export function getNodeIdFromLocation() {
22
23
  return SocketFunction.browserNodeId();
23
24
  }
@@ -61,7 +61,7 @@ export function measureWrap<T extends (...args: any[]) => any>(fnc: T, name?: st
61
61
  functionsSkipped++;
62
62
  return fnc;
63
63
  }
64
- let usedName = name || fnc.name;
64
+ let usedName = name || fnc.name || fnc.toString().slice(0, 100).replaceAll(/\s/g, " ");
65
65
  return nameFunction(usedName, (function (this: any, ...args: unknown[]): unknown {
66
66
  if (outstandingProfiles.length === 0) {
67
67
  return fnc.apply(this, args);
@@ -127,17 +127,18 @@ export interface LogMeasureTableConfig {
127
127
  minTimeToLog?: number;
128
128
  // Defaults to 2
129
129
  mergeDepth?: number;
130
+ // Defaults to 10
131
+ maxTableEntries?: number;
130
132
  }
131
133
 
132
134
  export function logMeasureTable(
133
135
  profile: MeasureProfile,
134
136
  config?: LogMeasureTableConfig
135
137
  ) {
136
- let { useTotalTime, name, thresholdInTable } = config || {};
137
- if (thresholdInTable === undefined) {
138
- thresholdInTable = 0.05;
139
- }
138
+ let { useTotalTime, name } = config || {};
139
+ const thresholdInTable = config?.thresholdInTable ?? 0.05;
140
140
  let minTimeToLog = config?.minTimeToLog ?? 50;
141
+ const maxTableEntries = config?.maxTableEntries ?? 10;
141
142
 
142
143
  function getTime(entry: ProfileEntry) {
143
144
  return useTotalTime ? entry.totalTime : entry.ownTime;
@@ -188,11 +189,30 @@ export function logMeasureTable(
188
189
  return `${(value * 100).toFixed(2)}%`;
189
190
  }
190
191
 
191
- entries = entries.slice(0, 10);
192
+ let remaining = entries.slice(maxTableEntries);
193
+ entries = entries.slice(0, maxTableEntries);
194
+ entries = entries.filter(entry => {
195
+ const include = getTime(entry).sum / totalTime >= thresholdInTable;
196
+ if (!include) {
197
+ remaining.push(entry);
198
+ }
199
+ return include;
200
+ });
201
+ entries.push({
202
+ name: "Other",
203
+ ownTime: createStatsValue(),
204
+ totalTime: createStatsValue(),
205
+ stillOpenCount: 0,
206
+ });
207
+ let remainingEntry = entries[entries.length - 1];
208
+ for (let entry of remaining) {
209
+ addToStats(remainingEntry.ownTime, entry.ownTime);
210
+ addToStats(remainingEntry.totalTime, entry.totalTime);
211
+ remainingEntry.stillOpenCount += entry.stillOpenCount;
212
+ }
192
213
  let maxNameLength = Math.max(...entries.map(x => x.name.length));
193
214
 
194
- for (let entry of entries.slice(0, 10)) {
195
- if (getTime(entry).sum / totalTime < thresholdInTable) break;
215
+ for (let entry of entries) {
196
216
  let output = "";
197
217
  output += `${blue(entry.name)}`;
198
218
  output += Array(maxNameLength + 4 - entry.name.length).fill(" ").join("");
package/src/tlsParsing.ts CHANGED
@@ -3,14 +3,17 @@ export function parseTLSHello(buffer: Buffer): {
3
3
  type: number;
4
4
  data: Buffer;
5
5
  }[];
6
+ missingBytes: number;
6
7
  } {
7
8
  let output: {
8
9
  extensions: {
9
10
  type: number;
10
11
  data: Buffer;
11
12
  }[];
13
+ missingBytes: number;
12
14
  } = {
13
- extensions: []
15
+ extensions: [],
16
+ missingBytes: 0
14
17
  };
15
18
 
16
19
  try {
@@ -50,6 +53,7 @@ export function parseTLSHello(buffer: Buffer): {
50
53
  pos += compressionLength;
51
54
 
52
55
  let extensionsLength = readShort();
56
+ output.missingBytes = contentLength - (pos + extensionsLength);
53
57
  let extensionsEnd = pos + extensionsLength;
54
58
  while (pos < extensionsEnd) {
55
59
  let extensionType = readShort();
@@ -58,7 +58,12 @@ export async function startSocketServer(
58
58
  let httpServerPromise = new Promise<https.Server>(r => onHttpServer = r);
59
59
  let lastOptions!: https.ServerOptions;
60
60
  await watchOptions(value => {
61
- lastOptions = { ...value, ca: getTrustedCertificates() };
61
+ lastOptions = {
62
+ ...value,
63
+ ca: getTrustedCertificates(),
64
+ // Attempt to disable sessions, because they make SNI significantly harder to parse.
65
+ secureOptions: require("node:constants").SSL_OP_NO_TICKET,
66
+ };
62
67
  if (!httpsServerLast) {
63
68
  httpsServerLast = https.createServer(lastOptions);
64
69
  } else {
@@ -119,7 +124,7 @@ export async function startSocketServer(
119
124
  let host = new URL("ws://" + request.headers["host"]).hostname;
120
125
  let origin = new URL(originHeader).hostname;
121
126
  if (host !== origin && !allowedHostnames.has(origin)) {
122
- throw new Error(`Invalid cross domain request, ${JSON.stringify(host)} !== ${JSON.stringify(origin)} (also not config.allowedHostnames ${JSON.stringify(config.allowHostnames)})`);
127
+ throw new Error(`Invalid cross domain request, ${JSON.stringify(host)} !== ${JSON.stringify(origin)} (also not in config.allowedHostnames ${JSON.stringify(config.allowHostnames)})`);
123
128
  }
124
129
  } catch (e) {
125
130
  console.error(e);
@@ -167,18 +172,17 @@ export async function startSocketServer(
167
172
  });
168
173
 
169
174
  let realServer = net.createServer(socket => {
170
- // NOTE: ONCE is used, so we only look at the first buffer, and then after that
171
- // we pipe. This should be very efficient, as pipe has insane throughput
172
- // (100s of MB/s, easily, even on a terrible machine).
173
- socket.once("data", buffer => {
175
+ function handleTLSHello(buffer: Buffer, packetCount: number): void | "more" {
174
176
  // All HTTPS requests start with 22, and no HTTP requests start with 22,
175
177
  // so we just need to read the first byte.
176
-
177
178
  let server: https.Server | http.Server;
178
179
  if (buffer[0] !== 22) {
179
180
  server = httpServer;
180
181
  } else {
181
182
  let data = parseTLSHello(buffer);
183
+ if (data.missingBytes > 0) {
184
+ return "more";
185
+ }
182
186
  let sni = data.extensions.filter(x => x.type === SNIType).flatMap(x => parseSNIExtension(x.data))[0];
183
187
  if (!SocketFunction.silent) {
184
188
  console.log(`Received TCP connection with SNI ${JSON.stringify(sni)}`);
@@ -189,7 +193,21 @@ export async function startSocketServer(
189
193
  // NOTE: Messages aren't dequeued until the current handler finishes, so we don't need to pause the socket or anything.
190
194
  server.emit("connection", socket);
191
195
  socket.unshift(buffer);
192
- });
196
+ }
197
+ let buffers: Buffer[] = [];
198
+ function getNextData() {
199
+ // NOTE: ONCE is used, so we only look at the first buffer, and then after that
200
+ // we pipe. This should be very efficient, as pipe has insane throughput
201
+ // (100s of MB/s, easily, even on a terrible machine).
202
+ socket.once("data", buffer => {
203
+ buffers.push(buffer);
204
+ let result = handleTLSHello(Buffer.concat(buffers), buffers.length);
205
+ if (result === "more") {
206
+ getNextData();
207
+ }
208
+ });
209
+ }
210
+ getNextData();
193
211
  socket.on("error", (e) => {
194
212
  console.error(`Exposed socket error, ${e.stack}`);
195
213
  });
@@ -1,51 +1,57 @@
1
- import ws from "ws";
2
- import tls from "tls";
3
- import { isNode } from "./misc";
4
- import { SenderInterface } from "./CallFactory";
5
- import { getTrustedCertificates } from "./certStore";
6
- import { getNodeIdLocation } from "./nodeCache";
7
- import debugbreak from "debugbreak";
8
- import { SocketFunction } from "../SocketFunction";
9
-
10
-
11
- export function getTLSSocket(webSocket: ws.WebSocket) {
12
- return (webSocket as any)._socket as tls.TLSSocket;
13
- }
14
-
15
- /** NOTE: We create a factory, which embeds the key/cert information. Otherwise retries might use
16
- * a different key/cert context.
17
- */
18
- export function createWebsocketFactory(): (nodeId: string) => SenderInterface {
19
-
20
- if (!isNode()) {
21
- return (nodeId: string) => {
22
- let location = getNodeIdLocation(nodeId);
23
- if (!location) throw new Error(`Cannot connect to ${nodeId}, no address known`);
24
- let { address, port } = location;
25
-
26
- if (!SocketFunction.silent) {
27
- console.log(`Connecting to ${address}:${port}`);
28
- }
29
- return new WebSocket(`wss://${address}:${port}`);
30
- };
31
- } else {
32
- return (nodeId: string) => {
33
- let location = getNodeIdLocation(nodeId);
34
- if (!location) throw new Error(`Cannot connect to ${nodeId}, no address known`);
35
- let { address, port } = location;
36
-
37
- if (!SocketFunction.silent) {
38
- console.log(`Connecting to ${address}:${port}`);
39
- }
40
- let webSocket = new ws.WebSocket(`wss://${address}:${port}`, {
41
- ca: getTrustedCertificates()
42
- });
43
- let result = Object.assign(webSocket, { socket: undefined as tls.TLSSocket | undefined });
44
- webSocket.once("upgrade", e => {
45
- result.socket = e.socket as tls.TLSSocket;
46
- });
47
- return result;
48
- };
49
- }
50
- }
51
-
1
+ import ws from "ws";
2
+ import tls from "tls";
3
+ import { isNode } from "./misc";
4
+ import { SenderInterface } from "./CallFactory";
5
+ import { getTrustedCertificates } from "./certStore";
6
+ import { getNodeIdLocation } from "./nodeCache";
7
+ import debugbreak from "debugbreak";
8
+ import { SocketFunction } from "../SocketFunction";
9
+
10
+ export function getTLSSocket(webSocket: ws.WebSocket) {
11
+ return (webSocket as any)._socket as tls.TLSSocket;
12
+ }
13
+
14
+ /** NOTE: We create a factory, which embeds the key/cert information. Otherwise retries might use
15
+ * a different key/cert context.
16
+ */
17
+ export function createWebsocketFactory(): (nodeId: string) => SenderInterface {
18
+
19
+ if (!isNode()) {
20
+ return (nodeId: string) => {
21
+ let location = getNodeIdLocation(nodeId);
22
+ if (!location) throw new Error(`Cannot connect to ${nodeId}, no address known`);
23
+ let { address, port } = location;
24
+
25
+ if (!SocketFunction.silent) {
26
+ console.log(`Connecting to ${address}:${port}`);
27
+ }
28
+ return new WebSocket(`wss://${address}:${port}`);
29
+ };
30
+ } else {
31
+ return (nodeId: string) => {
32
+ let location = getNodeIdLocation(nodeId);
33
+ if (!location) throw new Error(`Cannot connect to ${nodeId}, no address known`);
34
+ let { address, port } = location;
35
+
36
+ if (!SocketFunction.silent) {
37
+ console.log(`Connecting to ${address}:${port}`);
38
+ }
39
+ let webSocket = new ws.WebSocket(`wss://${address}:${port}`, undefined, {
40
+ ca: getTrustedCertificates(),
41
+ // createConnection(options, oncreate) {
42
+ // // NOTE: If our latency is 500ms, with 10MB/s, then we need a high water
43
+ // // mark of at least 5MB, otherwise our connection is slowed down.
44
+ // //(options as any).writableHighWaterMark = 5 * 1024 * 1024;
45
+ // return tls.connect(options as any, oncreate as any);
46
+ // },
47
+ });
48
+
49
+ // NOTE: Little setup is done here, because Sometimes websockets are created here,
50
+ // and sometimes via incoming connections, We should do most setup in
51
+ // CallFactory.ts:initializeWebsocket
52
+
53
+ return webSocket;
54
+ };
55
+ }
56
+ }
57
+
@@ -1,4 +1,5 @@
1
1
  import { SocketFunction } from "../SocketFunction";
2
+ import { blue, green, red, yellow } from "../src/formatting/logColors";
2
3
  import { isNode } from "../src/misc";
3
4
 
4
5
  module.allowclient = true;
@@ -121,6 +122,13 @@ async function updateTimeOffset() {
121
122
  }
122
123
 
123
124
  let prevOffset = trueTimeOffset;
125
+ let offsetRound = Math.abs(Math.round(offset));
126
+ let offsetColored = (
127
+ Math.abs(offset) > 600 && red(offsetRound + "ms")
128
+ || Math.abs(offset) > 300 && yellow(offsetRound + "ms")
129
+ || green(offsetRound + "ms")
130
+ );
131
+ console.log(`${blue("Synchronized time")}, local clock was ${offset > 0 ? "behind" : "ahead"} by ${offsetColored}`);
124
132
  for (let i = 0; i < currentSmearCount; i++) {
125
133
  let fraction = (i + 1) / currentSmearCount;
126
134
  trueTimeOffset = prevOffset * (1 - fraction) + offset * fraction;