socket-function 0.114.0 → 0.116.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.
@@ -1,780 +1,786 @@
1
- import { CallerContext, CallerContextBase, CallType, FullCallType } from "../SocketFunctionTypes";
2
- import * as ws from "ws";
3
- import { getCallFlags, performLocalCall, shouldCompressCall } from "./callManager";
4
- import { convertErrorStackToError, formatNumberSuffixed, isBufferType, isNode, list, timeInHour, timeInMinute } from "./misc";
5
- import { createWebsocketFactory, getTLSSocket } from "./websocketFactory";
6
- import { SocketFunction } from "../SocketFunction";
7
- import * as tls from "tls";
8
- import { getClientNodeId, getNodeIdLocation, registerNodeClient } from "./nodeCache";
9
- import debugbreak from "debugbreak";
10
- import { lazy } from "./caching";
11
- import { red, yellow } from "./formatting/logColors";
12
- import { isSplitableArray, markArrayAsSplitable } from "./fixLargeNetworkCalls";
13
- import { delay, runInfinitePoll, runInSerial } from "./batching";
14
- import { formatNumber, formatTime } from "./formatting/format";
15
- import zlib from "zlib";
16
- import pako from "pako";
17
- import { setFlag } from "../require/compileFlags";
18
- import { measureFnc, measureWrap } from "./profiling/measure";
19
- import { MaybePromise } from "./types";
20
- setFlag(require, "pako", "allowclient", true);
21
-
22
- // NOTE: If it is too low, and too many servers disconnect, we can easily spend 100% of our time
23
- // trying to reconnect.
24
- // (Or... maybe the delay is just waiting, and we aren't actually overloading the server?)
25
- const MIN_RETRY_DELAY = 5000;
26
-
27
- type InternalCallType = FullCallType & {
28
- seqNum: number;
29
- isReturn: false;
30
- isArgsCompressed?: boolean;
31
- }
32
-
33
- type InternalReturnType = {
34
- isReturn: true;
35
- result: unknown;
36
- error?: string;
37
- seqNum: number;
38
- isResultCompressed?: boolean;
39
- };
40
-
41
-
42
- export interface CallFactory {
43
- nodeId: string;
44
- lastClosed: number;
45
- closedForever?: boolean;
46
- isConnected?: boolean;
47
- // NOTE: May or may not have reconnection or retry logic inside of performCall.
48
- // Trigger performLocalCall on the other side of the connection
49
- performCall(call: CallType): Promise<unknown>;
50
- onNextDisconnect(callback: () => void): void;
51
- connectionId: { nodeId: string };
52
- }
53
-
54
- export interface SenderInterface {
55
- nodeId?: string;
56
- // Only set AFTER "open" (if set at all, as in the browser we don't have access to the socket).
57
- _socket?: tls.TLSSocket;
58
-
59
- send(data: string | Buffer): void;
60
-
61
- addEventListener(event: "open", listener: () => void): void;
62
- addEventListener(event: "close", listener: () => void): void;
63
- addEventListener(event: "error", listener: (err: { message: string }) => void): void;
64
- addEventListener(event: "message", listener: (data: ws.RawData | ws.MessageEvent | string) => void): void;
65
-
66
- readyState: number;
67
-
68
- ping?(): void;
69
- }
70
-
71
- let pendingCallCount = 0;
72
- let harvestableFailedCalls = 0;
73
- const CALL_TIMES_LIMIT = 1000 * 1000 * 10;
74
- let harvestableCallTimes: { start: number; end: number; }[] = [];
75
- export function harvestFailedCallCount() {
76
- let count = harvestableFailedCalls;
77
- harvestableFailedCalls = 0;
78
- return count;
79
- }
80
- export function getPendingCallCount() {
81
- return pendingCallCount;
82
- }
83
- export function harvestCallTimes() {
84
- let times = harvestableCallTimes;
85
- harvestableCallTimes = [];
86
- return times;
87
- }
88
- runInfinitePoll(timeInMinute * 15, () => {
89
- if (harvestableCallTimes.length > CALL_TIMES_LIMIT) {
90
- harvestableCallTimes = harvestableCallTimes.slice(-CALL_TIMES_LIMIT);
91
- }
92
- });
93
-
94
-
95
- export async function createCallFactory(
96
- webSocketBase: SenderInterface | undefined,
97
- // The node id we are connecting to (or that connected to us)
98
- nodeId: string,
99
- // The node id that we were contacted on
100
- localNodeId = "",
101
- ): Promise<CallFactory> {
102
- let niceConnectionName = nodeId;
103
-
104
- const createWebsocket = createWebsocketFactory();
105
- const registerOnce = lazy(() => registerNodeClient(callFactory));
106
-
107
- const canReconnect = !!getNodeIdLocation(nodeId);
108
-
109
- let pendingCalls: Map<number, {
110
- data: Buffer[];
111
- call: InternalCallType;
112
- callback: (resultJSON: InternalReturnType) => void;
113
- }> = new Map();
114
- // NOTE: It is important to make this as random as possible, to prevent
115
- // reconnections dues to a process being reset causing seqNum collisions
116
- // in return calls.
117
- let nextSeqNum = Date.now() + Math.random();
118
-
119
- // NOTE: I'm not sure if this is needed, I thought it was, but... now I think
120
- // it probably isn't...
121
- // if (webSocketBase?.readyState === 1 /* OPEN */ && webSocketBase.ping) {
122
- // // Heartbeat loop, otherwise onDisconnect is never called.
123
- // ((async () => {
124
- // while (webSocketBase?.readyState === 1 /* OPEN */ && webSocketBase.ping) {
125
- // await delay(1000 * 60);
126
- // webSocketBase.ping?.();
127
- // }
128
- // }))().catch(() => { });
129
- // }
130
-
131
- let lastConnectionAttempt = 0;
132
-
133
- let callerContext: CallerContextBase = {
134
- nodeId,
135
- localNodeId
136
- };
137
-
138
- let disconnectCallbacks: (() => void)[] = [];
139
- function onNextDisconnect(callback: () => void): void {
140
- disconnectCallbacks.push(callback);
141
- }
142
-
143
- let callFactory: CallFactory = {
144
- nodeId,
145
- lastClosed: 0,
146
- connectionId: { nodeId },
147
- onNextDisconnect,
148
- async performCall(call: CallType) {
149
- let seqNum = nextSeqNum++;
150
- let fullCall: InternalCallType = {
151
- nodeId,
152
- isReturn: false,
153
- args: call.args,
154
- classGuid: call.classGuid,
155
- functionName: call.functionName,
156
- seqNum,
157
- };
158
- let data: Buffer[];
159
- let originalArgs = call.args;
160
- let time = Date.now();
161
- try {
162
- if (shouldCompressCall(fullCall)) {
163
- fullCall.args = await compressObj(fullCall.args) as any;
164
- fullCall.isArgsCompressed = true;
165
- }
166
- let dataMaybePromise = SocketFunction.WIRE_SERIALIZER.serialize(fullCall);
167
- if (dataMaybePromise instanceof Promise) {
168
- data = await dataMaybePromise;
169
- } else {
170
- data = dataMaybePromise;
171
- }
172
- } catch (e: any) {
173
- throw new Error(`Error serializing data for call ${call.classGuid}.${call.functionName}\n${e.stack}`);
174
- }
175
- time = Date.now() - time;
176
- let size = data.map(x => x.length).reduce((a, b) => a + b, 0);
177
- if (time > SocketFunction.WIRE_WARN_TIME) {
178
- console.log(red(`Slow serialize, took ${formatTime(time)} to serialize ${formatNumber(size)} bytes. For ${call.classGuid}.${call.functionName}`));
179
- }
180
-
181
- if (size > SocketFunction.MAX_MESSAGE_SIZE * 1.5) {
182
- let splitArgIndex = originalArgs.findIndex(isSplitableArray);
183
- if (splitArgIndex >= 0) {
184
- console.log(yellow(`Splitting large call due to large args: ${call.classGuid}.${call.functionName}`));
185
- let SPLIT_GROUPS = 10;
186
- let splitArg = originalArgs[splitArgIndex] as unknown[];
187
- let subCalls = list(SPLIT_GROUPS).map(index => {
188
- let start = Math.floor(index / SPLIT_GROUPS * splitArg.length);
189
- let end = Math.floor((index + 1) / SPLIT_GROUPS * splitArg.length);
190
- return splitArg.slice(start, end);
191
- }).filter(x => x.length > 0);
192
-
193
- let calls = subCalls.map(async splitList => {
194
- let subCall = { ...call };
195
- subCall.args = subCall.args.slice();
196
- subCall.args[splitArgIndex] = markArrayAsSplitable(splitList);
197
- await callFactory.performCall(subCall);
198
- });
199
- await Promise.allSettled(calls);
200
- await Promise.all(calls);
201
- // Eh... we COULD return the array of results, but... then the result would sometimes be an array,
202
- // some times not, so, it is better to return a string which will make it more clear why it sometimes varies.
203
- return "CALLS_SPLIT_DUE_TO_LARGE_ARGS";
204
- }
205
-
206
- 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.`);
207
- }
208
-
209
- let resultPromise = new Promise((resolve, reject) => {
210
- let startTime = Date.now();
211
- pendingCallCount++;
212
- let callback = (result: InternalReturnType) => {
213
- pendingCallCount--;
214
- pendingCalls.delete(seqNum);
215
- harvestableCallTimes.push({ start: startTime, end: Date.now(), });
216
-
217
- if (result.error) {
218
- reject(convertErrorStackToError(result.error));
219
- } else {
220
- resolve(result.result);
221
- }
222
- };
223
- pendingCalls.set(seqNum, { callback, data, call: fullCall });
224
- });
225
-
226
- {
227
- let resultSize = data.map(x => x.length).reduce((a, b) => a + b, 0);
228
- for (let callback of SocketFunction.trackMessageSizes.upload) {
229
- callback(resultSize);
230
- }
231
- if (SocketFunction.logMessages) {
232
- let fncHack = "";
233
- if (call.functionName === "addCall") {
234
- let arg = originalArgs[0] as any;
235
- fncHack = `.${arg.DomainName}.${arg.ModuleId}.${arg.FunctionId}`;
236
- }
237
- console.log(`SIZE\t${(formatNumberSuffixed(resultSize) + "B").padEnd(4, " ")}\tREMOTE CALL\t${call.classGuid}.${call.functionName}${fncHack} at ${Date.now()}`);
238
- }
239
- }
240
- // If sending OR resultPromise throws, we want to error out. This solves some issues with resultPromise
241
- // erroring out first, which is before we await it, which makes NodeJS angry (unhandled promise rejection).
242
- // Also, technically, we could receive the result before we finish sending, in which case, we might
243
- // as well return it immediately.
244
- await Promise.race([send(data), resultPromise]);
245
- return await resultPromise;
246
- }
247
- };
248
-
249
- let webSocketPromise: Promise<SenderInterface> | undefined;
250
- if (webSocketBase) {
251
- webSocketPromise = Promise.resolve(webSocketBase);
252
- await initializeWebsocket(webSocketBase);
253
- }
254
-
255
- async function initializeWebsocket(newWebSocket: SenderInterface) {
256
- registerOnce();
257
-
258
- function onClose(error: string) {
259
- callFactory.connectionId = { nodeId };
260
- callFactory.lastClosed = Date.now();
261
- callFactory.isConnected = false;
262
- webSocketPromise = undefined;
263
- if (!canReconnect) {
264
- callFactory.closedForever = true;
265
- }
266
- for (let [key, call] of pendingCalls) {
267
- harvestableFailedCalls++;
268
- pendingCalls.delete(key);
269
- call.callback({
270
- isReturn: true,
271
- result: undefined,
272
- error: error,
273
- seqNum: call.call.seqNum,
274
- });
275
- }
276
-
277
- let callbacks = disconnectCallbacks;
278
- disconnectCallbacks = [];
279
- for (let callback of callbacks) {
280
- try {
281
- callback();
282
- } catch { }
283
- }
284
- }
285
-
286
- newWebSocket.addEventListener("error", e => {
287
- // NOTE: No more logging, as we throw, so the caller should be logging the
288
- // error (or swallowing it, if that is what it wants to do).
289
- //console.log(`Websocket error for ${niceConnectionName}`, e.message);
290
- onClose(new Error(`Connection error for ${niceConnectionName}: ${e.message}`).stack!);
291
- });
292
-
293
- newWebSocket.addEventListener("close", async () => {
294
- //console.log(`Websocket closed ${niceConnectionName}`);
295
- onClose(new Error(`Connection closed to ${niceConnectionName}`).stack!);
296
- });
297
-
298
- newWebSocket.addEventListener("message", onMessage);
299
-
300
-
301
- if (newWebSocket.readyState === 0 /* CONNECTING */) {
302
- await new Promise<void>(resolve => {
303
- newWebSocket.addEventListener("open", () => {
304
- if (!SocketFunction.silent) {
305
- console.log(`Connection established to ${niceConnectionName}`);
306
- }
307
- callFactory.isConnected = true;
308
- resolve();
309
- });
310
- newWebSocket.addEventListener("close", () => resolve());
311
- newWebSocket.addEventListener("error", () => resolve());
312
- });
313
- } else if (newWebSocket.readyState === 1 /* OPEN */) {
314
- callFactory.isConnected = true;
315
- } else {
316
- onClose(new Error(`Websocket received in closed state`).stack!);
317
- }
318
- }
319
-
320
- const BASE_LENGTH_OFFSET = 324_432_461_592_612;
321
- type MessageHeader = {
322
- type: "serialized";
323
- bufferCount: number;
324
- } | {
325
- type: "Buffer[]" | "Buffer";
326
- bufferCount: number;
327
- bufferLengths?: number[];
328
- metadata: Omit<InternalReturnType, "result">;
329
- };
330
- let sendInSerial = runInSerial(async (val: () => Promise<void>) => val());
331
- async function sendRaw(data: (string | Buffer)[]) {
332
- if (!webSocketPromise) {
333
- if (canReconnect) {
334
- webSocketPromise = tryToReconnect();
335
- } else {
336
- throw new Error(`Cannot send data to ${niceConnectionName} as the connection has closed`);
337
- }
338
- }
339
- let webSocket = await webSocketPromise;
340
- await sendInSerial(async () => {
341
- for (let d of data) {
342
- if (d.length > 1000 * 1000 * 10) {
343
- console.log(`Sending large packet ${formatNumber(d.length)}B to ${nodeId} at ${Date.now()}`);
344
- }
345
-
346
- // NOTE: If our latency is 500ms, with 10MB/s, then we need a high water
347
- // mark of at least 5MB, otherwise our connection is slowed down.
348
- // - Using the actual high water mark is too difficult, as we receive incoming connections.
349
- // This is also easier to configure, and we can dynamically change it if we have to.
350
- // NOTE: In practice we only hit this when sending large Buffers (~30MB), so low values
351
- // are equivalent to waiting for drain. We want to avoid waiting for drain, so we use a high value.
352
- const maxWriteBuffer = 128 * 1024 * 1024;
353
- webSocket.send(d);
354
-
355
- let socket = webSocket._socket;
356
- if (socket) {
357
- while (socket.writableLength > maxWriteBuffer) {
358
- // NOTE: Waiting 1ms probably waits more like 16ms.
359
- await new Promise(r => setTimeout(r, 1));
360
- }
361
- }
362
- }
363
- });
364
- }
365
- async function send(data: Buffer[]) {
366
- await sendRaw([
367
- (data.length + BASE_LENGTH_OFFSET).toString(),
368
- ...data,
369
- ]);
370
- }
371
- async function sendWithHeader(data: Buffer[], header: MessageHeader) {
372
- if (data.some(x => x.length > SocketFunction.MAX_MESSAGE_SIZE * 1.5)) {
373
- if (header.type === "Buffer" || header.type === "Buffer[]") {
374
- header.bufferLengths = data.map(x => x.length);
375
- let fitBuffers: Buffer[] = [];
376
- for (let buf of data) {
377
- if (buf.length > SocketFunction.MAX_MESSAGE_SIZE) {
378
- let offset = 0;
379
- while (offset < buf.length) {
380
- fitBuffers.push(buf.slice(offset, offset + SocketFunction.MAX_MESSAGE_SIZE));
381
- offset += SocketFunction.MAX_MESSAGE_SIZE;
382
- }
383
- } else {
384
- fitBuffers.push(buf);
385
- }
386
- }
387
- data = fitBuffers;
388
- header.bufferCount = fitBuffers.length;
389
- } else {
390
- throw new Error(`Cannot send large amounts of data unless we are returning Buffer or Buffer[]`);
391
- }
392
- }
393
- // if (totalResultSize > SocketFunction.MAX_MESSAGE_SIZE * 1.5) {
394
- // Split up Buffer[] if they are too large
395
- await sendRaw([
396
- JSON.stringify(header),
397
- ...data,
398
- ]);
399
- }
400
- async function tryToReconnect(): Promise<SenderInterface> {
401
- // Don't try to reconnect too often!
402
- let timeSinceLastAttempt = Date.now() - lastConnectionAttempt;
403
- if (timeSinceLastAttempt < MIN_RETRY_DELAY) {
404
- await new Promise(r => setTimeout(r, MIN_RETRY_DELAY - timeSinceLastAttempt));
405
- }
406
- lastConnectionAttempt = Date.now();
407
-
408
- let newWebSocket = createWebsocket(nodeId);
409
- await initializeWebsocket(newWebSocket);
410
-
411
- return newWebSocket;
412
- }
413
-
414
- let pendingCall: MessageHeader & {
415
- buffers: Buffer[];
416
- } | undefined;
417
-
418
- async function processPendingCall() {
419
- if (!pendingCall) throw new Error(`No pending call`);
420
- let currentCall = pendingCall;
421
- pendingCall = undefined;
422
- let currentBuffers = currentCall.buffers;
423
- let call: InternalCallType | InternalReturnType;
424
- let resultSize: number;
425
- let time = Date.now();
426
- if (currentCall.type === "Buffer" || currentCall.type === "Buffer[]") {
427
- let result: Buffer | Buffer[] = currentBuffers;
428
- if (currentCall.bufferLengths) {
429
- let pendingBuffers = currentBuffers;
430
- function takeBuffer(len: number) {
431
- let lenLeft = len;
432
- let buffers: Buffer[] = [];
433
- while (lenLeft > 0) {
434
- let buf = currentBuffers.shift();
435
- if (!buf) {
436
- throw new Error(`Not enough buffers received.`);
437
- }
438
- if (buf.length > lenLeft) {
439
- buffers.push(buf.slice(0, lenLeft));
440
- currentBuffers.unshift(buf.slice(lenLeft));
441
- break;
442
- } else {
443
- buffers.push(buf);
444
- lenLeft -= buf.length;
445
- }
446
- }
447
- if (buffers.length === 1) {
448
- return buffers[0];
449
- }
450
- return Buffer.concat(buffers);
451
- }
452
- result = currentCall.bufferLengths.map(takeBuffer);
453
- if (pendingBuffers.length > 0) {
454
- throw new Error(`Received too many buffers.`);
455
- }
456
- }
457
- resultSize = result.map(x => x.length).reduce((a, b) => a + b, 0);
458
- if (currentCall.type === "Buffer") {
459
- if (result.length === 1) {
460
- result = result[0];
461
- } else {
462
- result = Buffer.concat(result);
463
- }
464
- }
465
- call = {
466
- ...currentCall.metadata,
467
- result,
468
- };
469
- } else {
470
- resultSize = currentBuffers.map(x => x.length).reduce((a, b) => a + b, 0);
471
- call = await SocketFunction.WIRE_SERIALIZER.deserialize(currentBuffers) as InternalCallType | InternalReturnType;
472
- }
473
- time = Date.now() - time;
474
- for (let callback of SocketFunction.trackMessageSizes.download) {
475
- callback(resultSize);
476
- }
477
-
478
- if (call.isReturn) {
479
- let callbackObj = pendingCalls.get(call.seqNum);
480
- if (time > SocketFunction.WIRE_WARN_TIME) {
481
- console.log(red(`Slow parse, took ${time}ms to parse ${resultSize} bytes, for receiving result of call to ${callbackObj?.call.classGuid}.${callbackObj?.call.functionName}`));
482
- }
483
- if (!callbackObj) {
484
- console.log(`Got return for unknown call ${call.seqNum} (created at time ${new Date(call.seqNum)})`);
485
- return;
486
- }
487
- if (SocketFunction.logMessages) {
488
- let call = callbackObj.call;
489
- console.log(`SIZE\t${(formatNumberSuffixed(resultSize) + "B").padEnd(4, " ")}\tRETURN\t${call.classGuid}.${call.functionName} at ${Date.now()}, (${nodeId} / ${localNodeId})`);
490
- }
491
- if (call.isResultCompressed) {
492
- call.result = await decompressObj(call.result as Buffer);
493
- call.isResultCompressed = false;
494
- }
495
- callbackObj.callback(call);
496
- } else {
497
- if (call.isArgsCompressed) {
498
- call.args = await decompressObj(call.args as any as Buffer) as any;
499
- call.isArgsCompressed = false;
500
- }
501
- if (call.functionName === "changeIdentity") {
502
- /*
503
- TODO: Sometimes calls don't get through, even though we know the client made the call. Here are the logs from a failing case:
504
- Exposing Controller ServerController-17ea53da-bbef-4c8b-9eb0-99e263464c6f
505
- Exposing Controller HotReloadController-032b2250-3aac-4187-8c95-75412742b8f5
506
- Exposing Controller TimeController-ddf4753e-fc8a-413f-8cc2-b927dd449976
507
- Updating websocket server options
508
- Updating websocket server trusted certificates
509
- Updating websocket server options
510
- Updating websocket server trusted certificates
511
- Updating websocket server options
512
- Updating websocket server trusted certificates
513
- Trying to listening on 127.0.0.1:4231
514
- Started Listening on planquickly.com:4231 (127.0.0.1) after 5.54s
515
- Mounted on 127-0-0-1.planquickly.com:4231
516
- Exposing Controller RequireController-e2f811f3-14b8-4759-b0d6-73f14516cf1d
517
- Received TCP connection from 127.0.0.1:42105
518
- Received TCP header packet from 127.0.0.1:42105, have 1894 bytes so far, 1 packets
519
- Received TCP connection with SNI "127-0-0-1.planquickly.com". Have handlers for: planquickly.com, 127-0-0-1.planquickly.com
520
- HTTP server connection established 127.0.0.1:42105
521
- HTTP request (GET) https://127-0-0-1.planquickly.com:4231/?hot
522
- HTTP response 106KB (GET) https://127-0-0-1.planquickly.com:4231/?hot
523
- HTTP server socket closed for 127.0.0.1:42105
524
- Received TCP connection from 127.0.0.1:42106
525
- Received TCP header packet from 127.0.0.1:42106, have 1862 bytes so far, 1 packets
526
- Received TCP connection with SNI "127-0-0-1.planquickly.com". Have handlers for: planquickly.com, 127-0-0-1.planquickly.com
527
- HTTP server connection established 127.0.0.1:42106
528
- HTTP request (GET) https://127-0-0-1.planquickly.com:4231/?classGuid=RequireController-e2f811f3-14b8-4759-b0d6-73f14516cf1d&functionName=getModules&args=%5B%5B%22.%2Fsite%2FsiteMain%22%5D%2Cnull%5D
529
- HTTP response 10.8MB (GET) https://127-0-0-1.planquickly.com:4231/?classGuid=RequireController-e2f811f3-14b8-4759-b0d6-73f14516cf1d&functionName=getModules&args=%5B%5B%22.%2Fsite%2FsiteMain%22%5D%2Cnull%5D
530
- Received TCP connection from 127.0.0.1:42107
531
- Received TCP header packet from 127.0.0.1:42107, have 1894 bytes so far, 1 packets
532
- Received TCP connection with SNI "127-0-0-1.planquickly.com". Have handlers for: planquickly.com, 127-0-0-1.planquickly.com
533
- HTTP server connection established 127.0.0.1:42107
534
- HTTP server socket closed for 127.0.0.1:42106
535
- HTTP server socket closed for 127.0.0.1:42107
536
- Received TCP connection from 127.0.0.1:42108
537
- Received TCP header packet from 127.0.0.1:42108, have 1830 bytes so far, 1 packets
538
- Received TCP connection with SNI "127-0-0-1.planquickly.com". Have handlers for: planquickly.com, 127-0-0-1.planquickly.com
539
- HTTP server connection established 127.0.0.1:42108
540
- HTTP request (GET) https://127-0-0-1.planquickly.com:4231/node.cjs.map
541
- HTTP response 106KB (GET) https://127-0-0-1.planquickly.com:4231/node.cjs.map
542
- HTTP server socket closed for 127.0.0.1:42108
543
- Received TCP connection from 127.0.0.1:42110
544
- Received TCP header packet from 127.0.0.1:42110, have 1818 bytes so far, 1 packets
545
- Received TCP connection with SNI "127-0-0-1.planquickly.com". Have handlers for: planquickly.com, 127-0-0-1.planquickly.com
546
- HTTP server connection established 127.0.0.1:42110
547
- Received TCP connection from 127.0.0.1:42111
548
- Received TCP header packet from 127.0.0.1:42111, have 1830 bytes so far, 1 packets
549
- Received TCP connection with SNI "127-0-0-1.planquickly.com". Have handlers for: planquickly.com, 127-0-0-1.planquickly.com
550
- HTTP server connection established 127.0.0.1:42111
551
- Received websocket upgrade request for 127.0.0.1:42110
552
- Connection established to client:127.0.0.1:1744150129862.296:0.4118126921519041
553
- HTTP request (GET) https://127-0-0-1.planquickly.com:4231/?classGuid=RequireController-e2f811f3-14b8-4759-b0d6-73f14516cf1d&functionName=getModules&args=%5B%5B%22D%3A%2Frepos%2Fperspectanalytics%2Fai3%2Fnode_modules%2Fsocket-function%2Ftime%2FtrueTimeShim.ts%22%5D%2C%7B%22requireSeqNumProcessId%22%3A%22requireSeqNumProcessId_1744150120269_0.5550074391586426%22%2C%22seqNumRanges%22%3A%5B%7B%22s%22%3A1%2C%22e%22%3A892%7D%5D%7D%5D
554
- HTTP response 31.1KB (GET) https://127-0-0-1.planquickly.com:4231/?classGuid=RequireController-e2f811f3-14b8-4759-b0d6-73f14516cf1d&functionName=getModules&args=%5B%5B%22D%3A%2Frepos%2Fperspectanalytics%2Fai3%2Fnode_modules%2Fsocket-function%2Ftime%2FtrueTimeShim.ts%22%5D%2C%7B%22requireSeqNumProcessId%22%3A%22requireSeqNumProcessId_1744150120269_0.5550074391586426%22%2C%22seqNumRanges%22%3A%5B%7B%22s%22%3A1%2C%22e%22%3A892%7D%5D%7D%5D
555
- SIZE 171B EVALUATE HotReloadController-032b2250-3aac-4187-8c95-75412742b8f5.watchFiles at 1744150129869.296
556
- SIZE 174B EVALUATE ServerController-17ea53da-bbef-4c8b-9eb0-99e263464c6f.testSiteFunction at 1744150129872.296
557
- HTTP server socket closed for 127.0.0.1:42111
558
- SIZE 167B EVALUATE TimeController-ddf4753e-fc8a-413f-8cc2-b927dd449976.getTrueTime at 1744150129893.296
559
- SIZE 167B EVALUATE TimeController-ddf4753e-fc8a-413f-8cc2-b927dd449976.getTrueTime at 1744150129897.296
560
- SIZE 167B EVALUATE TimeController-ddf4753e-fc8a-413f-8cc2-b927dd449976.getTrueTime at 1744150129899.296
561
- SIZE 167B EVALUATE TimeController-ddf4753e-fc8a-413f-8cc2-b927dd449976.getTrueTime at 1744150139907.0776
562
- SIZE 167B EVALUATE TimeController-ddf4753e-fc8a-413f-8cc2-b927dd449976.getTrueTime at 1744150139909.0776
563
- SIZE 167B EVALUATE TimeController-ddf4753e-fc8a-413f-8cc2-b927dd449976.getTrueTime at 1744150139911.0776
564
- Hot reloading due to change: D:/repos/perspectanalytics/ai3/node_modules/socket-function/src/webSocketServer.ts
565
- - The upgrade request finishes, at least once: Received websocket upgrade
566
- - AND, we are receiving some calls, so... that appears to work.
567
- - Maybe the time calls never finish?
568
- - We added logging for when calls finish as well, so we can tell if all the TimeController calls timed out
569
- - ALSO, added more logging to see if the calls were from the same client (which WOULD be a bug, because
570
- the client shouldn't be calling us so often), or, different clients.
571
- - We DO receive more connections than http connections closed. But not that many more...
572
- */
573
- console.log(red(`Call to ${call.classGuid}.${call.functionName} at ${Date.now()}`));
574
- }
575
- if (SocketFunction.logMessages) {
576
- console.log(`SIZE\t${(formatNumberSuffixed(resultSize) + "B").padEnd(4, " ")}\tEVALUATE\t${call.classGuid}.${call.functionName} at ${Date.now()}, (${nodeId} / ${localNodeId})`);
577
- }
578
- if (time > SocketFunction.WIRE_WARN_TIME) {
579
- console.log(red(`Slow parse, took ${time}ms to parse ${resultSize} bytes, for call to ${call.classGuid}.${call.functionName}`));
580
- }
581
-
582
- let response: InternalReturnType;
583
- try {
584
- let time = Date.now();
585
- let result = await performLocalCall({ call, caller: callerContext });
586
- response = {
587
- isReturn: true,
588
- result,
589
- seqNum: call.seqNum,
590
- };
591
- if (SocketFunction.logMessages) {
592
- time = Date.now() - time;
593
- console.log(`DUR\t${(formatTime(time)).padEnd(6, " ")}\tFINISH\t${call.classGuid}.${call.functionName} at ${Date.now()}, (${nodeId} / ${localNodeId})`);
594
- }
595
- if (shouldCompressCall(call)) {
596
- response.result = await compressObj(response.result) as any;
597
- response.isResultCompressed = true;
598
- }
599
- } catch (e: any) {
600
- response = {
601
- isReturn: true,
602
- result: undefined,
603
- seqNum: call.seqNum,
604
- error: e.stack,
605
- };
606
- }
607
-
608
- if (response.result instanceof Buffer) {
609
- let { result, ...remaining } = response;
610
- await sendWithHeader([result], { type: "Buffer", bufferCount: 1, metadata: remaining });
611
- } else if (Array.isArray(response.result) && response.result.every(x => x instanceof Buffer)) {
612
- let { result, ...remaining } = response;
613
- await sendWithHeader(result, { type: "Buffer[]", bufferCount: result.length, metadata: remaining });
614
- } else {
615
- const LIMIT = getCallFlags(call)?.responseLimit || SocketFunction.MAX_MESSAGE_SIZE * 1.5;
616
- let result: Buffer[] = await SocketFunction.WIRE_SERIALIZER.serialize(response);
617
- let totalResultSize = result.map(x => x.length).reduce((a, b) => a + b, 0);
618
- if (totalResultSize > LIMIT) {
619
- response = {
620
- isReturn: true,
621
- result: undefined,
622
- seqNum: call.seqNum,
623
- error: new Error(`Response too large to send. Return Buffer[] to exceed the limits, or set responseLimit when registering the collection. ${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, if absolutely required, set SocketFunction.MAX_MESSAGE_SIZE to a higher value.`).stack,
624
- };
625
- result = await SocketFunction.WIRE_SERIALIZER.serialize(response);
626
- }
627
- await send(result);
628
- }
629
- }
630
- }
631
-
632
- let clientsideSerial = runInSerial(async <T>(val: Promise<T>) => val);
633
- async function onMessage(message: ws.RawData | ws.MessageEvent | string) {
634
- try {
635
- if (typeof message === "object" && "data" in message) {
636
- message = message.data;
637
- }
638
- // Extra clienside parsing is required
639
- if (!isNode()) {
640
- // Immediately start the arrayBuffer conversion. This should be fast, but...
641
- // maybe we will add more here, and so doing it in parallel might be useful.
642
- let fixMessageBlob = (async () => {
643
- if (message instanceof Blob) {
644
- message = Buffer.from(await message.arrayBuffer());
645
- }
646
- })();
647
- // We need to force the results to be in serial, otherwise strings leapfrog
648
- // ahead of buffers, which breaks things.
649
- await clientsideSerial(fixMessageBlob);
650
- }
651
- if (typeof message === "string") {
652
- if (message.startsWith("{")) {
653
- let obj = JSON.parse(message);
654
- pendingCall = {
655
- ...obj,
656
- buffers: [],
657
- };
658
- } else {
659
- let count = parseInt(message);
660
- if (isNaN(count)) {
661
- throw new Error(`Invalid message count ${message}`);
662
- }
663
- if (count < BASE_LENGTH_OFFSET) {
664
- throw new Error(`Invalid message count ${message}`);
665
- }
666
- count -= BASE_LENGTH_OFFSET;
667
- if (count > 1000 * 1000) {
668
- throw new Error(`Invalid message count ${count}`);
669
- }
670
- pendingCall = {
671
- buffers: [],
672
- bufferCount: count,
673
- type: "serialized",
674
- };
675
- }
676
- if (pendingCall?.bufferCount === 0) {
677
- await processPendingCall();
678
- }
679
- return;
680
- }
681
- if (message instanceof Buffer) {
682
- if (message.byteLength > 1000 * 1000 * 10) {
683
- console.log(`Received large packet ${formatNumber(message.byteLength)}B at ${Date.now()}`);
684
- }
685
- if (!pendingCall) {
686
- throw new Error(`Received data without size`);
687
- }
688
- pendingCall.buffers.push(message);
689
- if (pendingCall.buffers.length !== pendingCall.bufferCount) {
690
- return;
691
- }
692
-
693
- await processPendingCall();
694
- return;
695
- }
696
- throw new Error(`Unhandled data type ${typeof message}`);
697
- } catch (e: any) {
698
- let err = e.stack || e.message || e;
699
- // NOTE: I'm looking for all types of errors here (specifically, .send errors), in case
700
- // there are errors I should be handling.
701
- if (err.startsWith("Error: Cannot send data to") && err.includes("as the connection has closed")) {
702
- // This is fine, just ignore it
703
- } else if (err.includes("The requested file could not be read, typically due to permission problems that have occurred after a reference to a file was acquired.")) {
704
- console.error(`WebSocket data was dropped by the browser due to exceeding the Blob limit. Either you are about to run out of memory, or you hit the much lower Incognito Blob limit. This will likely break the application. To reset the memory you must close all tabs of this site. This is a bug/feature in chrome.`);
705
- } else {
706
- console.error(e.stack);
707
- }
708
- }
709
- }
710
-
711
- return callFactory;
712
- }
713
-
714
- async function doStream(stream: GenericTransformStream, buffer: Buffer): Promise<Buffer> {
715
- let reader = stream.readable.getReader();
716
- let writer = stream.writable.getWriter();
717
- let writePromise = writer.write(buffer);
718
- let closePromise = writer.close();
719
-
720
- let outputBuffers: Buffer[] = [];
721
- while (true) {
722
- let { value, done } = await reader.read();
723
- if (done) {
724
- await writePromise;
725
- await closePromise;
726
- return Buffer.concat(outputBuffers);
727
- }
728
- outputBuffers.push(Buffer.from(value));
729
- }
730
- }
731
- async function unzipBase(buffer: Buffer): Promise<Buffer> {
732
- if (isNode()) {
733
- return new Promise((resolve, reject) => {
734
- zlib.gunzip(buffer, (err: any, result: Buffer) => {
735
- if (err) reject(err);
736
- else resolve(result);
737
- });
738
- });
739
- } else {
740
- // NOTE: pako seems to be faster, at least clientside.
741
- // TIMING: 700ms vs 1200ms
742
- // - This might just be faster for small files.
743
- return Buffer.from(pako.inflate(buffer));
744
- // @ts-ignore
745
- // return await doStream(new DecompressionStream("gzip"), buffer);
746
- }
747
- }
748
- async function zipBase(buffer: Buffer, level?: number): Promise<Buffer> {
749
- if (isNode()) {
750
- return new Promise((resolve, reject) => {
751
- zlib.gzip(buffer, { level }, (err: any, result: Buffer) => {
752
- if (err) reject(err);
753
- else resolve(result);
754
- });
755
- });
756
- } else {
757
- // @ts-ignore
758
- return await doStream(new CompressionStream("gzip"), buffer);
759
- }
760
- }
761
-
762
- const compressObj = measureWrap(async function wireCallCompress(obj: unknown): Promise<Buffer> {
763
- let buffers = await SocketFunction.WIRE_SERIALIZER.serialize(obj);
764
- let lengthBuffer = Buffer.from((new Float64Array(buffers.map(x => x.length))).buffer);
765
- let buffer = Buffer.concat([lengthBuffer, ...buffers]);
766
- let result = await zipBase(buffer);
767
- return result;
768
- });
769
- const decompressObj = measureWrap(async function wireCallDecompress(obj: Buffer): Promise<unknown> {
770
- let buffer = await unzipBase(obj);
771
- let lengthBuffer = buffer.slice(0, 8);
772
- let lengths = new Float64Array(lengthBuffer.buffer, lengthBuffer.byteOffset, lengthBuffer.byteLength / 8);
773
- let buffers: Buffer[] = [];
774
- let offset = 8;
775
- for (let length of lengths) {
776
- buffers.push(buffer.slice(offset, offset + length));
777
- offset += length;
778
- }
779
- return await SocketFunction.WIRE_SERIALIZER.deserialize(buffers);
1
+ import { CallerContext, CallerContextBase, CallType, FullCallType } from "../SocketFunctionTypes";
2
+ import * as ws from "ws";
3
+ import { getCallFlags, performLocalCall, shouldCompressCall } from "./callManager";
4
+ import { convertErrorStackToError, formatNumberSuffixed, isBufferType, isNode, list, timeInHour, timeInMinute } from "./misc";
5
+ import { createWebsocketFactory, getTLSSocket } from "./websocketFactory";
6
+ import { SocketFunction } from "../SocketFunction";
7
+ import * as tls from "tls";
8
+ import { getClientNodeId, getNodeIdLocation, registerNodeClient } from "./nodeCache";
9
+ import debugbreak from "debugbreak";
10
+ import { lazy } from "./caching";
11
+ import { red, yellow } from "./formatting/logColors";
12
+ import { isSplitableArray, markArrayAsSplitable } from "./fixLargeNetworkCalls";
13
+ import { delay, runInfinitePoll, runInSerial } from "./batching";
14
+ import { formatNumber, formatTime } from "./formatting/format";
15
+ import zlib from "zlib";
16
+ import pako from "pako";
17
+ import { setFlag } from "../require/compileFlags";
18
+ import { measureFnc, measureWrap } from "./profiling/measure";
19
+ import { MaybePromise } from "./types";
20
+ setFlag(require, "pako", "allowclient", true);
21
+
22
+ // NOTE: If it is too low, and too many servers disconnect, we can easily spend 100% of our time
23
+ // trying to reconnect.
24
+ // (Or... maybe the delay is just waiting, and we aren't actually overloading the server?)
25
+ const MIN_RETRY_DELAY = 5000;
26
+
27
+ type InternalCallType = FullCallType & {
28
+ seqNum: number;
29
+ isReturn: false;
30
+ isArgsCompressed?: boolean;
31
+ }
32
+
33
+ type InternalReturnType = {
34
+ isReturn: true;
35
+ result: unknown;
36
+ error?: string;
37
+ seqNum: number;
38
+ isResultCompressed?: boolean;
39
+ };
40
+
41
+
42
+ export interface CallFactory {
43
+ nodeId: string;
44
+ lastClosed: number;
45
+ closedForever?: boolean;
46
+ isConnected?: boolean;
47
+ // NOTE: May or may not have reconnection or retry logic inside of performCall.
48
+ // Trigger performLocalCall on the other side of the connection
49
+ performCall(call: CallType): Promise<unknown>;
50
+ onNextDisconnect(callback: () => void): void;
51
+ connectionId: { nodeId: string };
52
+ }
53
+
54
+ export interface SenderInterface {
55
+ nodeId?: string;
56
+ // Only set AFTER "open" (if set at all, as in the browser we don't have access to the socket).
57
+ _socket?: tls.TLSSocket;
58
+
59
+ send(data: string | Buffer): void;
60
+
61
+ addEventListener(event: "open", listener: () => void): void;
62
+ addEventListener(event: "close", listener: () => void): void;
63
+ addEventListener(event: "error", listener: (err: { message: string }) => void): void;
64
+ addEventListener(event: "message", listener: (data: ws.RawData | ws.MessageEvent | string) => void): void;
65
+
66
+ readyState: number;
67
+
68
+ ping?(): void;
69
+ }
70
+
71
+ let pendingCallCount = 0;
72
+ let harvestableFailedCalls = 0;
73
+ const CALL_TIMES_LIMIT = 1000 * 1000 * 10;
74
+ let harvestableCallTimes: { start: number; end: number; }[] = [];
75
+ export function harvestFailedCallCount() {
76
+ let count = harvestableFailedCalls;
77
+ harvestableFailedCalls = 0;
78
+ return count;
79
+ }
80
+ export function getPendingCallCount() {
81
+ return pendingCallCount;
82
+ }
83
+ export function harvestCallTimes() {
84
+ let times = harvestableCallTimes;
85
+ harvestableCallTimes = [];
86
+ return times;
87
+ }
88
+ runInfinitePoll(timeInMinute * 15, () => {
89
+ if (harvestableCallTimes.length > CALL_TIMES_LIMIT) {
90
+ harvestableCallTimes = harvestableCallTimes.slice(-CALL_TIMES_LIMIT);
91
+ }
92
+ });
93
+
94
+
95
+ export async function createCallFactory(
96
+ webSocketBase: SenderInterface | undefined,
97
+ // The node id we are connecting to (or that connected to us)
98
+ nodeId: string,
99
+ // The node id that we were contacted on
100
+ localNodeId = "",
101
+ ): Promise<CallFactory> {
102
+ let niceConnectionName = nodeId;
103
+
104
+ const createWebsocket = createWebsocketFactory();
105
+ const registerOnce = lazy(() => registerNodeClient(callFactory));
106
+
107
+ const canReconnect = !!getNodeIdLocation(nodeId);
108
+
109
+ let pendingCalls: Map<number, {
110
+ data: Buffer[];
111
+ call: InternalCallType;
112
+ callback: (resultJSON: InternalReturnType) => void;
113
+ }> = new Map();
114
+ // NOTE: It is important to make this as random as possible, to prevent
115
+ // reconnections dues to a process being reset causing seqNum collisions
116
+ // in return calls.
117
+ let nextSeqNum = Date.now() + Math.random();
118
+
119
+ // NOTE: I'm not sure if this is needed, I thought it was, but... now I think
120
+ // it probably isn't...
121
+ // if (webSocketBase?.readyState === 1 /* OPEN */ && webSocketBase.ping) {
122
+ // // Heartbeat loop, otherwise onDisconnect is never called.
123
+ // ((async () => {
124
+ // while (webSocketBase?.readyState === 1 /* OPEN */ && webSocketBase.ping) {
125
+ // await delay(1000 * 60);
126
+ // webSocketBase.ping?.();
127
+ // }
128
+ // }))().catch(() => { });
129
+ // }
130
+
131
+ let lastConnectionAttempt = 0;
132
+
133
+ let callerContext: CallerContextBase = {
134
+ nodeId,
135
+ localNodeId
136
+ };
137
+
138
+ let disconnectCallbacks: (() => void)[] = [];
139
+ function onNextDisconnect(callback: () => void): void {
140
+ disconnectCallbacks.push(callback);
141
+ }
142
+
143
+ let callFactory: CallFactory = {
144
+ nodeId,
145
+ lastClosed: 0,
146
+ connectionId: { nodeId },
147
+ onNextDisconnect,
148
+ async performCall(call: CallType) {
149
+ let seqNum = nextSeqNum++;
150
+ let fullCall: InternalCallType = {
151
+ nodeId,
152
+ isReturn: false,
153
+ args: call.args,
154
+ classGuid: call.classGuid,
155
+ functionName: call.functionName,
156
+ seqNum,
157
+ };
158
+ let data: Buffer[];
159
+ let originalArgs = call.args;
160
+ let time = Date.now();
161
+ try {
162
+ if (shouldCompressCall(fullCall)) {
163
+ fullCall.args = await compressObj(fullCall.args) as any;
164
+ fullCall.isArgsCompressed = true;
165
+ }
166
+ let dataMaybePromise = SocketFunction.WIRE_SERIALIZER.serialize(fullCall);
167
+ if (dataMaybePromise instanceof Promise) {
168
+ data = await dataMaybePromise;
169
+ } else {
170
+ data = dataMaybePromise;
171
+ }
172
+ } catch (e: any) {
173
+ throw new Error(`Error serializing data for call ${call.classGuid}.${call.functionName}\n${e.stack}`);
174
+ }
175
+ time = Date.now() - time;
176
+ let size = data.map(x => x.length).reduce((a, b) => a + b, 0);
177
+ if (time > SocketFunction.WIRE_WARN_TIME) {
178
+ console.log(red(`Slow serialize, took ${formatTime(time)} to serialize ${formatNumber(size)} bytes. For ${call.classGuid}.${call.functionName}`));
179
+ }
180
+
181
+ if (size > SocketFunction.MAX_MESSAGE_SIZE * 1.5) {
182
+ let splitArgIndex = originalArgs.findIndex(isSplitableArray);
183
+ if (splitArgIndex >= 0) {
184
+ console.log(yellow(`Splitting large call due to large args: ${call.classGuid}.${call.functionName}`));
185
+ let SPLIT_GROUPS = 10;
186
+ let splitArg = originalArgs[splitArgIndex] as unknown[];
187
+ let subCalls = list(SPLIT_GROUPS).map(index => {
188
+ let start = Math.floor(index / SPLIT_GROUPS * splitArg.length);
189
+ let end = Math.floor((index + 1) / SPLIT_GROUPS * splitArg.length);
190
+ return splitArg.slice(start, end);
191
+ }).filter(x => x.length > 0);
192
+
193
+ let calls = subCalls.map(async splitList => {
194
+ let subCall = { ...call };
195
+ subCall.args = subCall.args.slice();
196
+ subCall.args[splitArgIndex] = markArrayAsSplitable(splitList);
197
+ await callFactory.performCall(subCall);
198
+ });
199
+ await Promise.allSettled(calls);
200
+ await Promise.all(calls);
201
+ // Eh... we COULD return the array of results, but... then the result would sometimes be an array,
202
+ // some times not, so, it is better to return a string which will make it more clear why it sometimes varies.
203
+ return "CALLS_SPLIT_DUE_TO_LARGE_ARGS";
204
+ }
205
+
206
+ 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.`);
207
+ }
208
+
209
+ let resultPromise = new Promise((resolve, reject) => {
210
+ let startTime = Date.now();
211
+ pendingCallCount++;
212
+ let callback = (result: InternalReturnType) => {
213
+ pendingCallCount--;
214
+ pendingCalls.delete(seqNum);
215
+ harvestableCallTimes.push({ start: startTime, end: Date.now(), });
216
+
217
+ if (result.error) {
218
+ reject(convertErrorStackToError(result.error));
219
+ } else {
220
+ resolve(result.result);
221
+ }
222
+ };
223
+ pendingCalls.set(seqNum, { callback, data, call: fullCall });
224
+ });
225
+
226
+ {
227
+ let resultSize = data.map(x => x.length).reduce((a, b) => a + b, 0);
228
+ for (let callback of SocketFunction.trackMessageSizes.upload) {
229
+ callback(resultSize);
230
+ }
231
+ if (SocketFunction.logMessages) {
232
+ let fncHack = "";
233
+ if (call.functionName === "addCall") {
234
+ let arg = originalArgs[0] as any;
235
+ fncHack = `.${arg.DomainName}.${arg.ModuleId}.${arg.FunctionId}`;
236
+ }
237
+ console.log(`SIZE\t${(formatNumberSuffixed(resultSize) + "B").padEnd(4, " ")}\tREMOTE CALL\t${call.classGuid}.${call.functionName}${fncHack} at ${Date.now()}`);
238
+ }
239
+ }
240
+ // If sending OR resultPromise throws, we want to error out. This solves some issues with resultPromise
241
+ // erroring out first, which is before we await it, which makes NodeJS angry (unhandled promise rejection).
242
+ // Also, technically, we could receive the result before we finish sending, in which case, we might
243
+ // as well return it immediately.
244
+ await Promise.race([send(data), resultPromise]);
245
+ return await resultPromise;
246
+ }
247
+ };
248
+
249
+ let webSocketPromise: Promise<SenderInterface> | undefined;
250
+ if (webSocketBase) {
251
+ webSocketPromise = Promise.resolve(webSocketBase);
252
+ await initializeWebsocket(webSocketBase);
253
+ }
254
+
255
+ async function initializeWebsocket(newWebSocket: SenderInterface) {
256
+ registerOnce();
257
+
258
+ function onClose(error: string) {
259
+ callFactory.connectionId = { nodeId };
260
+ callFactory.lastClosed = Date.now();
261
+ callFactory.isConnected = false;
262
+ webSocketPromise = undefined;
263
+ if (!canReconnect) {
264
+ callFactory.closedForever = true;
265
+ }
266
+ for (let [key, call] of pendingCalls) {
267
+ harvestableFailedCalls++;
268
+ pendingCalls.delete(key);
269
+ call.callback({
270
+ isReturn: true,
271
+ result: undefined,
272
+ error: error,
273
+ seqNum: call.call.seqNum,
274
+ });
275
+ }
276
+
277
+ let callbacks = disconnectCallbacks;
278
+ disconnectCallbacks = [];
279
+ for (let callback of callbacks) {
280
+ try {
281
+ callback();
282
+ } catch { }
283
+ }
284
+ }
285
+
286
+ newWebSocket.addEventListener("error", e => {
287
+ // NOTE: No more logging, as we throw, so the caller should be logging the
288
+ // error (or swallowing it, if that is what it wants to do).
289
+ //console.log(`Websocket error for ${niceConnectionName}`, e.message);
290
+ onClose(new Error(`Connection error for ${niceConnectionName}: ${e.message}`).stack!);
291
+ });
292
+
293
+ newWebSocket.addEventListener("close", async () => {
294
+ //console.log(`Websocket closed ${niceConnectionName}`);
295
+ onClose(new Error(`Connection closed to ${niceConnectionName}`).stack!);
296
+ });
297
+
298
+ newWebSocket.addEventListener("message", onMessage);
299
+
300
+
301
+ if (newWebSocket.readyState === 0 /* CONNECTING */) {
302
+ await new Promise<void>(resolve => {
303
+ newWebSocket.addEventListener("open", () => {
304
+ if (!SocketFunction.silent) {
305
+ console.log(`Connection established to ${niceConnectionName}`);
306
+ }
307
+ callFactory.isConnected = true;
308
+ resolve();
309
+ });
310
+ newWebSocket.addEventListener("close", () => resolve());
311
+ newWebSocket.addEventListener("error", () => resolve());
312
+ });
313
+ } else if (newWebSocket.readyState === 1 /* OPEN */) {
314
+ callFactory.isConnected = true;
315
+ } else {
316
+ onClose(new Error(`Websocket received in closed state`).stack!);
317
+ }
318
+ }
319
+
320
+ const BASE_LENGTH_OFFSET = 324_432_461_592_612;
321
+ type MessageHeader = {
322
+ type: "serialized";
323
+ bufferCount: number;
324
+ } | {
325
+ type: "Buffer[]" | "Buffer";
326
+ bufferCount: number;
327
+ bufferLengths?: number[];
328
+ metadata: Omit<InternalReturnType, "result">;
329
+ };
330
+ let sendInSerial = runInSerial(async (val: () => Promise<void>) => val());
331
+ async function sendRaw(data: (string | Buffer)[]) {
332
+ if (!webSocketPromise) {
333
+ if (canReconnect) {
334
+ webSocketPromise = tryToReconnect();
335
+ } else {
336
+ throw new Error(`Cannot send data to ${niceConnectionName} as the connection has closed`);
337
+ }
338
+ }
339
+ let webSocket = await webSocketPromise;
340
+ await sendInSerial(async () => {
341
+ for (let d of data) {
342
+ if (d.length > 1000 * 1000 * 10) {
343
+ console.log(`Sending large packet ${formatNumber(d.length)}B to ${nodeId} at ${Date.now()}`);
344
+ }
345
+
346
+ // NOTE: If our latency is 500ms, with 10MB/s, then we need a high water
347
+ // mark of at least 5MB, otherwise our connection is slowed down.
348
+ // - Using the actual high water mark is too difficult, as we receive incoming connections.
349
+ // This is also easier to configure, and we can dynamically change it if we have to.
350
+ // NOTE: In practice we only hit this when sending large Buffers (~30MB), so low values
351
+ // are equivalent to waiting for drain. We want to avoid waiting for drain, so we use a high value.
352
+ const maxWriteBuffer = 128 * 1024 * 1024;
353
+ webSocket.send(d);
354
+
355
+ let socket = webSocket._socket;
356
+ if (socket) {
357
+ while (socket.writableLength > maxWriteBuffer) {
358
+ // NOTE: Waiting 1ms probably waits more like 16ms.
359
+ await new Promise(r => setTimeout(r, 1));
360
+ }
361
+ }
362
+ }
363
+ });
364
+ }
365
+ async function send(data: Buffer[]) {
366
+ await sendRaw([
367
+ (data.length + BASE_LENGTH_OFFSET).toString(),
368
+ ...data,
369
+ ]);
370
+ }
371
+ async function sendWithHeader(data: Buffer[], header: MessageHeader) {
372
+ if (data.some(x => x.length > SocketFunction.MAX_MESSAGE_SIZE * 1.5)) {
373
+ if (header.type === "Buffer" || header.type === "Buffer[]") {
374
+ header.bufferLengths = data.map(x => x.length);
375
+ let fitBuffers: Buffer[] = [];
376
+ for (let buf of data) {
377
+ if (buf.length > SocketFunction.MAX_MESSAGE_SIZE) {
378
+ let offset = 0;
379
+ while (offset < buf.length) {
380
+ fitBuffers.push(buf.slice(offset, offset + SocketFunction.MAX_MESSAGE_SIZE));
381
+ offset += SocketFunction.MAX_MESSAGE_SIZE;
382
+ }
383
+ } else {
384
+ fitBuffers.push(buf);
385
+ }
386
+ }
387
+ data = fitBuffers;
388
+ header.bufferCount = fitBuffers.length;
389
+ } else {
390
+ throw new Error(`Cannot send large amounts of data unless we are returning Buffer or Buffer[]`);
391
+ }
392
+ }
393
+ // if (totalResultSize > SocketFunction.MAX_MESSAGE_SIZE * 1.5) {
394
+ // Split up Buffer[] if they are too large
395
+ await sendRaw([
396
+ JSON.stringify(header),
397
+ ...data,
398
+ ]);
399
+ }
400
+ async function tryToReconnect(): Promise<SenderInterface> {
401
+ // Don't try to reconnect too often!
402
+ let timeSinceLastAttempt = Date.now() - lastConnectionAttempt;
403
+ if (timeSinceLastAttempt < MIN_RETRY_DELAY) {
404
+ await new Promise(r => setTimeout(r, MIN_RETRY_DELAY - timeSinceLastAttempt));
405
+ }
406
+ lastConnectionAttempt = Date.now();
407
+
408
+ let newWebSocket = createWebsocket(nodeId);
409
+ await initializeWebsocket(newWebSocket);
410
+
411
+ return newWebSocket;
412
+ }
413
+
414
+ let pendingCall: MessageHeader & {
415
+ buffers: Buffer[];
416
+ } | undefined;
417
+
418
+ async function processPendingCall() {
419
+ if (!pendingCall) throw new Error(`No pending call`);
420
+ let currentCall = pendingCall;
421
+ pendingCall = undefined;
422
+ let currentBuffers = currentCall.buffers;
423
+ let call: InternalCallType | InternalReturnType;
424
+ let resultSize: number;
425
+ let time = Date.now();
426
+ if (currentCall.type === "Buffer" || currentCall.type === "Buffer[]") {
427
+ let result: Buffer | Buffer[] = currentBuffers;
428
+ if (currentCall.bufferLengths) {
429
+ let pendingBuffers = currentBuffers;
430
+ function takeBuffer(len: number) {
431
+ let lenLeft = len;
432
+ let buffers: Buffer[] = [];
433
+ while (lenLeft > 0) {
434
+ let buf = currentBuffers.shift();
435
+ if (!buf) {
436
+ throw new Error(`Not enough buffers received.`);
437
+ }
438
+ if (buf.length > lenLeft) {
439
+ buffers.push(buf.slice(0, lenLeft));
440
+ currentBuffers.unshift(buf.slice(lenLeft));
441
+ break;
442
+ } else {
443
+ buffers.push(buf);
444
+ lenLeft -= buf.length;
445
+ }
446
+ }
447
+ if (buffers.length === 1) {
448
+ return buffers[0];
449
+ }
450
+ return Buffer.concat(buffers);
451
+ }
452
+ result = currentCall.bufferLengths.map(takeBuffer);
453
+ if (pendingBuffers.length > 0) {
454
+ throw new Error(`Received too many buffers.`);
455
+ }
456
+ }
457
+ resultSize = result.map(x => x.length).reduce((a, b) => a + b, 0);
458
+ if (currentCall.type === "Buffer") {
459
+ if (result.length === 1) {
460
+ result = result[0];
461
+ } else {
462
+ result = Buffer.concat(result);
463
+ }
464
+ }
465
+ call = {
466
+ ...currentCall.metadata,
467
+ result,
468
+ };
469
+ } else {
470
+ resultSize = currentBuffers.map(x => x.length).reduce((a, b) => a + b, 0);
471
+ call = await SocketFunction.WIRE_SERIALIZER.deserialize(currentBuffers) as InternalCallType | InternalReturnType;
472
+ }
473
+ let parseTime = Date.now() - time;
474
+ for (let callback of SocketFunction.trackMessageSizes.download) {
475
+ callback(resultSize);
476
+ }
477
+
478
+ if (call.isReturn) {
479
+ let callbackObj = pendingCalls.get(call.seqNum);
480
+ if (parseTime > SocketFunction.WIRE_WARN_TIME) {
481
+ console.log(red(`Slow parse, took ${parseTime}ms to parse ${resultSize} bytes, for receiving result of call to ${callbackObj?.call.classGuid}.${callbackObj?.call.functionName}`));
482
+ }
483
+ if (!callbackObj) {
484
+ console.log(`Got return for unknown call ${call.seqNum} (created at time ${new Date(call.seqNum)})`);
485
+ return;
486
+ }
487
+ if (SocketFunction.logMessages) {
488
+ let call = callbackObj.call;
489
+ console.log(`SIZE\t${(formatNumberSuffixed(resultSize) + "B").padEnd(4, " ")}\tRETURN\t${call.classGuid}.${call.functionName} at ${Date.now()}, (${nodeId} / ${localNodeId})`);
490
+ }
491
+ if (call.isResultCompressed) {
492
+ call.result = await decompressObj(call.result as Buffer);
493
+ call.isResultCompressed = false;
494
+ }
495
+ callbackObj.callback(call);
496
+ } else {
497
+ if (call.isArgsCompressed) {
498
+ call.args = await decompressObj(call.args as any as Buffer) as any;
499
+ call.isArgsCompressed = false;
500
+ }
501
+ if (call.functionName === "changeIdentity") {
502
+ /*
503
+ TODO: Sometimes calls don't get through, even though we know the client made the call. Here are the logs from a failing case:
504
+ Exposing Controller ServerController-17ea53da-bbef-4c8b-9eb0-99e263464c6f
505
+ Exposing Controller HotReloadController-032b2250-3aac-4187-8c95-75412742b8f5
506
+ Exposing Controller TimeController-ddf4753e-fc8a-413f-8cc2-b927dd449976
507
+ Updating websocket server options
508
+ Updating websocket server trusted certificates
509
+ Updating websocket server options
510
+ Updating websocket server trusted certificates
511
+ Updating websocket server options
512
+ Updating websocket server trusted certificates
513
+ Trying to listening on 127.0.0.1:4231
514
+ Started Listening on planquickly.com:4231 (127.0.0.1) after 5.54s
515
+ Mounted on 127-0-0-1.planquickly.com:4231
516
+ Exposing Controller RequireController-e2f811f3-14b8-4759-b0d6-73f14516cf1d
517
+ Received TCP connection from 127.0.0.1:42105
518
+ Received TCP header packet from 127.0.0.1:42105, have 1894 bytes so far, 1 packets
519
+ Received TCP connection with SNI "127-0-0-1.planquickly.com". Have handlers for: planquickly.com, 127-0-0-1.planquickly.com
520
+ HTTP server connection established 127.0.0.1:42105
521
+ HTTP request (GET) https://127-0-0-1.planquickly.com:4231/?hot
522
+ HTTP response 106KB (GET) https://127-0-0-1.planquickly.com:4231/?hot
523
+ HTTP server socket closed for 127.0.0.1:42105
524
+ Received TCP connection from 127.0.0.1:42106
525
+ Received TCP header packet from 127.0.0.1:42106, have 1862 bytes so far, 1 packets
526
+ Received TCP connection with SNI "127-0-0-1.planquickly.com". Have handlers for: planquickly.com, 127-0-0-1.planquickly.com
527
+ HTTP server connection established 127.0.0.1:42106
528
+ HTTP request (GET) https://127-0-0-1.planquickly.com:4231/?classGuid=RequireController-e2f811f3-14b8-4759-b0d6-73f14516cf1d&functionName=getModules&args=%5B%5B%22.%2Fsite%2FsiteMain%22%5D%2Cnull%5D
529
+ HTTP response 10.8MB (GET) https://127-0-0-1.planquickly.com:4231/?classGuid=RequireController-e2f811f3-14b8-4759-b0d6-73f14516cf1d&functionName=getModules&args=%5B%5B%22.%2Fsite%2FsiteMain%22%5D%2Cnull%5D
530
+ Received TCP connection from 127.0.0.1:42107
531
+ Received TCP header packet from 127.0.0.1:42107, have 1894 bytes so far, 1 packets
532
+ Received TCP connection with SNI "127-0-0-1.planquickly.com". Have handlers for: planquickly.com, 127-0-0-1.planquickly.com
533
+ HTTP server connection established 127.0.0.1:42107
534
+ HTTP server socket closed for 127.0.0.1:42106
535
+ HTTP server socket closed for 127.0.0.1:42107
536
+ Received TCP connection from 127.0.0.1:42108
537
+ Received TCP header packet from 127.0.0.1:42108, have 1830 bytes so far, 1 packets
538
+ Received TCP connection with SNI "127-0-0-1.planquickly.com". Have handlers for: planquickly.com, 127-0-0-1.planquickly.com
539
+ HTTP server connection established 127.0.0.1:42108
540
+ HTTP request (GET) https://127-0-0-1.planquickly.com:4231/node.cjs.map
541
+ HTTP response 106KB (GET) https://127-0-0-1.planquickly.com:4231/node.cjs.map
542
+ HTTP server socket closed for 127.0.0.1:42108
543
+ Received TCP connection from 127.0.0.1:42110
544
+ Received TCP header packet from 127.0.0.1:42110, have 1818 bytes so far, 1 packets
545
+ Received TCP connection with SNI "127-0-0-1.planquickly.com". Have handlers for: planquickly.com, 127-0-0-1.planquickly.com
546
+ HTTP server connection established 127.0.0.1:42110
547
+ Received TCP connection from 127.0.0.1:42111
548
+ Received TCP header packet from 127.0.0.1:42111, have 1830 bytes so far, 1 packets
549
+ Received TCP connection with SNI "127-0-0-1.planquickly.com". Have handlers for: planquickly.com, 127-0-0-1.planquickly.com
550
+ HTTP server connection established 127.0.0.1:42111
551
+ Received websocket upgrade request for 127.0.0.1:42110
552
+ Connection established to client:127.0.0.1:1744150129862.296:0.4118126921519041
553
+ HTTP request (GET) https://127-0-0-1.planquickly.com:4231/?classGuid=RequireController-e2f811f3-14b8-4759-b0d6-73f14516cf1d&functionName=getModules&args=%5B%5B%22D%3A%2Frepos%2Fperspectanalytics%2Fai3%2Fnode_modules%2Fsocket-function%2Ftime%2FtrueTimeShim.ts%22%5D%2C%7B%22requireSeqNumProcessId%22%3A%22requireSeqNumProcessId_1744150120269_0.5550074391586426%22%2C%22seqNumRanges%22%3A%5B%7B%22s%22%3A1%2C%22e%22%3A892%7D%5D%7D%5D
554
+ HTTP response 31.1KB (GET) https://127-0-0-1.planquickly.com:4231/?classGuid=RequireController-e2f811f3-14b8-4759-b0d6-73f14516cf1d&functionName=getModules&args=%5B%5B%22D%3A%2Frepos%2Fperspectanalytics%2Fai3%2Fnode_modules%2Fsocket-function%2Ftime%2FtrueTimeShim.ts%22%5D%2C%7B%22requireSeqNumProcessId%22%3A%22requireSeqNumProcessId_1744150120269_0.5550074391586426%22%2C%22seqNumRanges%22%3A%5B%7B%22s%22%3A1%2C%22e%22%3A892%7D%5D%7D%5D
555
+ SIZE 171B EVALUATE HotReloadController-032b2250-3aac-4187-8c95-75412742b8f5.watchFiles at 1744150129869.296
556
+ SIZE 174B EVALUATE ServerController-17ea53da-bbef-4c8b-9eb0-99e263464c6f.testSiteFunction at 1744150129872.296
557
+ HTTP server socket closed for 127.0.0.1:42111
558
+ SIZE 167B EVALUATE TimeController-ddf4753e-fc8a-413f-8cc2-b927dd449976.getTrueTime at 1744150129893.296
559
+ SIZE 167B EVALUATE TimeController-ddf4753e-fc8a-413f-8cc2-b927dd449976.getTrueTime at 1744150129897.296
560
+ SIZE 167B EVALUATE TimeController-ddf4753e-fc8a-413f-8cc2-b927dd449976.getTrueTime at 1744150129899.296
561
+ SIZE 167B EVALUATE TimeController-ddf4753e-fc8a-413f-8cc2-b927dd449976.getTrueTime at 1744150139907.0776
562
+ SIZE 167B EVALUATE TimeController-ddf4753e-fc8a-413f-8cc2-b927dd449976.getTrueTime at 1744150139909.0776
563
+ SIZE 167B EVALUATE TimeController-ddf4753e-fc8a-413f-8cc2-b927dd449976.getTrueTime at 1744150139911.0776
564
+ Hot reloading due to change: D:/repos/perspectanalytics/ai3/node_modules/socket-function/src/webSocketServer.ts
565
+ - The upgrade request finishes, at least once: Received websocket upgrade
566
+ - AND, we are receiving some calls, so... that appears to work.
567
+ - Maybe the time calls never finish?
568
+ - We added logging for when calls finish as well, so we can tell if all the TimeController calls timed out
569
+ - ALSO, added more logging to see if the calls were from the same client (which WOULD be a bug, because
570
+ the client shouldn't be calling us so often), or, different clients.
571
+ - We DO receive more connections than http connections closed. But not that many more...
572
+ */
573
+ console.log(red(`Call to ${call.classGuid}.${call.functionName} at ${Date.now()}`));
574
+ }
575
+ if (SocketFunction.logMessages) {
576
+ console.log(`SIZE\t${(formatNumberSuffixed(resultSize) + "B").padEnd(4, " ")}\tEVALUATE\t${call.classGuid}.${call.functionName} at ${Date.now()}, (${nodeId} / ${localNodeId})`);
577
+ }
578
+ if (parseTime > SocketFunction.WIRE_WARN_TIME) {
579
+ console.log(red(`Slow parse, took ${parseTime}ms to parse ${resultSize} bytes, for call to ${call.classGuid}.${call.functionName}`));
580
+ }
581
+
582
+ let response: InternalReturnType;
583
+ try {
584
+ let result = await performLocalCall({ call, caller: callerContext });
585
+ response = {
586
+ isReturn: true,
587
+ result,
588
+ seqNum: call.seqNum,
589
+ };
590
+ if (SocketFunction.logMessages) {
591
+ let timeTaken = Date.now() - time;
592
+ console.log(`DUR\t${(formatTime(timeTaken)).padEnd(6, " ")}\tFINISH\t${call.classGuid}.${call.functionName} at ${Date.now()}, (${nodeId} / ${localNodeId})`);
593
+ }
594
+ if (shouldCompressCall(call)) {
595
+ response.result = await compressObj(response.result) as any;
596
+ response.isResultCompressed = true;
597
+ }
598
+ } catch (e: any) {
599
+ response = {
600
+ isReturn: true,
601
+ result: undefined,
602
+ seqNum: call.seqNum,
603
+ error: e.stack,
604
+ };
605
+ }
606
+ {
607
+ let start = time;
608
+ let end = Date.now();
609
+ for (let fnc of SocketFunction.trackMessageSizes.callTimes) {
610
+ fnc({ start, end });
611
+ }
612
+ }
613
+
614
+ if (response.result instanceof Buffer) {
615
+ let { result, ...remaining } = response;
616
+ await sendWithHeader([result], { type: "Buffer", bufferCount: 1, metadata: remaining });
617
+ } else if (Array.isArray(response.result) && response.result.every(x => x instanceof Buffer)) {
618
+ let { result, ...remaining } = response;
619
+ await sendWithHeader(result, { type: "Buffer[]", bufferCount: result.length, metadata: remaining });
620
+ } else {
621
+ const LIMIT = getCallFlags(call)?.responseLimit || SocketFunction.MAX_MESSAGE_SIZE * 1.5;
622
+ let result: Buffer[] = await SocketFunction.WIRE_SERIALIZER.serialize(response);
623
+ let totalResultSize = result.map(x => x.length).reduce((a, b) => a + b, 0);
624
+ if (totalResultSize > LIMIT) {
625
+ response = {
626
+ isReturn: true,
627
+ result: undefined,
628
+ seqNum: call.seqNum,
629
+ error: new Error(`Response too large to send. Return Buffer[] to exceed the limits, or set responseLimit when registering the collection. ${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, if absolutely required, set SocketFunction.MAX_MESSAGE_SIZE to a higher value.`).stack,
630
+ };
631
+ result = await SocketFunction.WIRE_SERIALIZER.serialize(response);
632
+ }
633
+ await send(result);
634
+ }
635
+ }
636
+ }
637
+
638
+ let clientsideSerial = runInSerial(async <T>(val: Promise<T>) => val);
639
+ async function onMessage(message: ws.RawData | ws.MessageEvent | string) {
640
+ try {
641
+ if (typeof message === "object" && "data" in message) {
642
+ message = message.data;
643
+ }
644
+ // Extra clienside parsing is required
645
+ if (!isNode()) {
646
+ // Immediately start the arrayBuffer conversion. This should be fast, but...
647
+ // maybe we will add more here, and so doing it in parallel might be useful.
648
+ let fixMessageBlob = (async () => {
649
+ if (message instanceof Blob) {
650
+ message = Buffer.from(await message.arrayBuffer());
651
+ }
652
+ })();
653
+ // We need to force the results to be in serial, otherwise strings leapfrog
654
+ // ahead of buffers, which breaks things.
655
+ await clientsideSerial(fixMessageBlob);
656
+ }
657
+ if (typeof message === "string") {
658
+ if (message.startsWith("{")) {
659
+ let obj = JSON.parse(message);
660
+ pendingCall = {
661
+ ...obj,
662
+ buffers: [],
663
+ };
664
+ } else {
665
+ let count = parseInt(message);
666
+ if (isNaN(count)) {
667
+ throw new Error(`Invalid message count ${message}`);
668
+ }
669
+ if (count < BASE_LENGTH_OFFSET) {
670
+ throw new Error(`Invalid message count ${message}`);
671
+ }
672
+ count -= BASE_LENGTH_OFFSET;
673
+ if (count > 1000 * 1000) {
674
+ throw new Error(`Invalid message count ${count}`);
675
+ }
676
+ pendingCall = {
677
+ buffers: [],
678
+ bufferCount: count,
679
+ type: "serialized",
680
+ };
681
+ }
682
+ if (pendingCall?.bufferCount === 0) {
683
+ await processPendingCall();
684
+ }
685
+ return;
686
+ }
687
+ if (message instanceof Buffer) {
688
+ if (message.byteLength > 1000 * 1000 * 10) {
689
+ console.log(`Received large packet ${formatNumber(message.byteLength)}B at ${Date.now()}`);
690
+ }
691
+ if (!pendingCall) {
692
+ throw new Error(`Received data without size`);
693
+ }
694
+ pendingCall.buffers.push(message);
695
+ if (pendingCall.buffers.length !== pendingCall.bufferCount) {
696
+ return;
697
+ }
698
+
699
+ await processPendingCall();
700
+ return;
701
+ }
702
+ throw new Error(`Unhandled data type ${typeof message}`);
703
+ } catch (e: any) {
704
+ let err = e.stack || e.message || e;
705
+ // NOTE: I'm looking for all types of errors here (specifically, .send errors), in case
706
+ // there are errors I should be handling.
707
+ if (err.startsWith("Error: Cannot send data to") && err.includes("as the connection has closed")) {
708
+ // This is fine, just ignore it
709
+ } else if (err.includes("The requested file could not be read, typically due to permission problems that have occurred after a reference to a file was acquired.")) {
710
+ console.error(`WebSocket data was dropped by the browser due to exceeding the Blob limit. Either you are about to run out of memory, or you hit the much lower Incognito Blob limit. This will likely break the application. To reset the memory you must close all tabs of this site. This is a bug/feature in chrome.`);
711
+ } else {
712
+ console.error(e.stack);
713
+ }
714
+ }
715
+ }
716
+
717
+ return callFactory;
718
+ }
719
+
720
+ async function doStream(stream: GenericTransformStream, buffer: Buffer): Promise<Buffer> {
721
+ let reader = stream.readable.getReader();
722
+ let writer = stream.writable.getWriter();
723
+ let writePromise = writer.write(buffer);
724
+ let closePromise = writer.close();
725
+
726
+ let outputBuffers: Buffer[] = [];
727
+ while (true) {
728
+ let { value, done } = await reader.read();
729
+ if (done) {
730
+ await writePromise;
731
+ await closePromise;
732
+ return Buffer.concat(outputBuffers);
733
+ }
734
+ outputBuffers.push(Buffer.from(value));
735
+ }
736
+ }
737
+ async function unzipBase(buffer: Buffer): Promise<Buffer> {
738
+ if (isNode()) {
739
+ return new Promise((resolve, reject) => {
740
+ zlib.gunzip(buffer, (err: any, result: Buffer) => {
741
+ if (err) reject(err);
742
+ else resolve(result);
743
+ });
744
+ });
745
+ } else {
746
+ // NOTE: pako seems to be faster, at least clientside.
747
+ // TIMING: 700ms vs 1200ms
748
+ // - This might just be faster for small files.
749
+ return Buffer.from(pako.inflate(buffer));
750
+ // @ts-ignore
751
+ // return await doStream(new DecompressionStream("gzip"), buffer);
752
+ }
753
+ }
754
+ async function zipBase(buffer: Buffer, level?: number): Promise<Buffer> {
755
+ if (isNode()) {
756
+ return new Promise((resolve, reject) => {
757
+ zlib.gzip(buffer, { level }, (err: any, result: Buffer) => {
758
+ if (err) reject(err);
759
+ else resolve(result);
760
+ });
761
+ });
762
+ } else {
763
+ // @ts-ignore
764
+ return await doStream(new CompressionStream("gzip"), buffer);
765
+ }
766
+ }
767
+
768
+ const compressObj = measureWrap(async function wireCallCompress(obj: unknown): Promise<Buffer> {
769
+ let buffers = await SocketFunction.WIRE_SERIALIZER.serialize(obj);
770
+ let lengthBuffer = Buffer.from((new Float64Array(buffers.map(x => x.length))).buffer);
771
+ let buffer = Buffer.concat([lengthBuffer, ...buffers]);
772
+ let result = await zipBase(buffer);
773
+ return result;
774
+ });
775
+ const decompressObj = measureWrap(async function wireCallDecompress(obj: Buffer): Promise<unknown> {
776
+ let buffer = await unzipBase(obj);
777
+ let lengthBuffer = buffer.slice(0, 8);
778
+ let lengths = new Float64Array(lengthBuffer.buffer, lengthBuffer.byteOffset, lengthBuffer.byteLength / 8);
779
+ let buffers: Buffer[] = [];
780
+ let offset = 8;
781
+ for (let length of lengths) {
782
+ buffers.push(buffer.slice(offset, offset + length));
783
+ offset += length;
784
+ }
785
+ return await SocketFunction.WIRE_SERIALIZER.deserialize(buffers);
780
786
  });