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.
- package/SocketFunction.ts +464 -462
- package/package.json +1 -1
- package/src/CallFactory.ts +785 -779
- package/src/nodeCache.ts +115 -111
- package/time/trueTimeShim.ts +3 -0
package/src/CallFactory.ts
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
481
|
-
console.log(red(`Slow parse, took ${
|
|
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 (
|
|
579
|
-
console.log(red(`Slow parse, took ${
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
let response: InternalReturnType;
|
|
583
|
-
try {
|
|
584
|
-
let
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
response.
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
let
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
//
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
let
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
count
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
if (
|
|
683
|
-
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
let
|
|
771
|
-
let
|
|
772
|
-
let
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
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
|
});
|