socket-function 1.1.4 → 1.1.6
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.d.ts +4 -0
- package/SocketFunction.ts +9 -2
- package/index.d.ts +12 -1
- package/package.json +1 -2
- package/src/CallFactory.ts +218 -34
- package/src/batching.d.ts +8 -1
- package/src/batching.ts +37 -0
package/SocketFunction.d.ts
CHANGED
|
@@ -29,6 +29,10 @@ export declare class SocketFunction {
|
|
|
29
29
|
serialize: (obj: unknown) => MaybePromise<Buffer[]>;
|
|
30
30
|
deserialize: (buffers: Buffer[]) => MaybePromise<unknown>;
|
|
31
31
|
};
|
|
32
|
+
/** We will try the alternate node IDs first, however, if they fail, we will go through all of them and then eventually try the original node ID.
|
|
33
|
+
* VERY useful, allowing us to change global ips to local ones, which short-circuits the router, massively increasing bandwidth and decreasing latency.
|
|
34
|
+
*/
|
|
35
|
+
static GET_ALTERNATE_NODE_IDS: (nodeId: string) => MaybePromise<string[] | undefined>;
|
|
32
36
|
static WIRE_WARN_TIME: number;
|
|
33
37
|
private static onMountCallbacks;
|
|
34
38
|
static exposedClasses: Set<string>;
|
package/SocketFunction.ts
CHANGED
|
@@ -17,6 +17,7 @@ import cborx from "cbor-x";
|
|
|
17
17
|
import { setFlag } from "./require/compileFlags";
|
|
18
18
|
import { isNode } from "./src/misc";
|
|
19
19
|
import { getPendingCallCount, harvestCallTimes, harvestFailedCallCount } from "./src/CallFactory";
|
|
20
|
+
import { measureWrap } from "./src/profiling/measure";
|
|
20
21
|
|
|
21
22
|
setFlag(require, "cbor-x", "allowclient", true);
|
|
22
23
|
let cborxInstance = new cborx.Encoder({ structuredClone: true });
|
|
@@ -71,11 +72,17 @@ export class SocketFunction {
|
|
|
71
72
|
// In retrospect... dynamically changing the wire serializer is a BAD idea. If any calls happen
|
|
72
73
|
// before it is changed, things just break. Also, it needs to be changed on both sides,
|
|
73
74
|
// or else things break. Also, it is very hard to detect when the issue is different serializers
|
|
75
|
+
// NOTE: The only reason this is still exposed is in case in the future we want to intercept our traffic, and we want convenient functions to know how to decode it (although there are a still few other layers under this, for compression and Buffer[] sending efficiency).
|
|
74
76
|
public static readonly WIRE_SERIALIZER = {
|
|
75
|
-
serialize: (obj: unknown): MaybePromise<Buffer[]> => [cborxInstance.encode(obj)],
|
|
76
|
-
deserialize: (buffers: Buffer[]): MaybePromise<unknown> => cborxInstance.decode(buffers[0]),
|
|
77
|
+
serialize: measureWrap((obj: unknown): MaybePromise<Buffer[]> => [cborxInstance.encode(obj)], "WIRE_SERIALIZER|serialize"),
|
|
78
|
+
deserialize: measureWrap((buffers: Buffer[]): MaybePromise<unknown> => cborxInstance.decode(buffers[0]), "WIRE_SERIALIZER|deserialize"),
|
|
77
79
|
};
|
|
78
80
|
|
|
81
|
+
/** We will try the alternate node IDs first, however, if they fail, we will go through all of them and then eventually try the original node ID.
|
|
82
|
+
* VERY useful, allowing us to change global ips to local ones, which short-circuits the router, massively increasing bandwidth and decreasing latency.
|
|
83
|
+
*/
|
|
84
|
+
public static GET_ALTERNATE_NODE_IDS = (nodeId: string): MaybePromise<string[] | undefined> => undefined;
|
|
85
|
+
|
|
79
86
|
public static WIRE_WARN_TIME = 100;
|
|
80
87
|
|
|
81
88
|
private static onMountCallbacks = new Map<string, (() => MaybePromise<void>)[]>();
|
package/index.d.ts
CHANGED
|
@@ -38,6 +38,10 @@ declare module "socket-function/SocketFunction" {
|
|
|
38
38
|
serialize: (obj: unknown) => MaybePromise<Buffer[]>;
|
|
39
39
|
deserialize: (buffers: Buffer[]) => MaybePromise<unknown>;
|
|
40
40
|
};
|
|
41
|
+
/** We will try the alternate node IDs first, however, if they fail, we will go through all of them and then eventually try the original node ID.
|
|
42
|
+
* VERY useful, allowing us to change global ips to local ones, which short-circuits the router, massively increasing bandwidth and decreasing latency.
|
|
43
|
+
*/
|
|
44
|
+
static GET_ALTERNATE_NODE_IDS: (nodeId: string) => MaybePromise<string[] | undefined>;
|
|
41
45
|
static WIRE_WARN_TIME: number;
|
|
42
46
|
private static onMountCallbacks;
|
|
43
47
|
static exposedClasses: Set<string>;
|
|
@@ -565,7 +569,7 @@ declare module "socket-function/src/args" {
|
|
|
565
569
|
}
|
|
566
570
|
|
|
567
571
|
declare module "socket-function/src/batching" {
|
|
568
|
-
import { AnyFunction } from "socket-function/src/types";
|
|
572
|
+
import { AnyFunction, MaybePromise } from "socket-function/src/types";
|
|
569
573
|
export type DelayType = (number | "afterio" | "immediate" | "afterpromises" | "paintLoop" | "afterPaint");
|
|
570
574
|
export declare function delay(delayTime: DelayType, immediateShortDelays?: "immediateShortDelays"): Promise<void>;
|
|
571
575
|
export declare function batchFunctionNone<Arg, Result = void>(config: unknown, fnc: (arg: Arg[]) => (Promise<Result> | Result)): (arg: Arg) => Promise<Result>;
|
|
@@ -596,6 +600,13 @@ declare module "socket-function/src/batching" {
|
|
|
596
600
|
minDelay?: number;
|
|
597
601
|
maxDelay?: number;
|
|
598
602
|
}): T;
|
|
603
|
+
export declare const safeLoop: typeof unblockLoop;
|
|
604
|
+
export declare const throttledLoop: typeof unblockLoop;
|
|
605
|
+
export declare function unblockLoop<T, R>(config: {
|
|
606
|
+
data: T[];
|
|
607
|
+
maxBlockingTime?: number;
|
|
608
|
+
backOffTime?: number;
|
|
609
|
+
} | T[], fnc: (item: T) => MaybePromise<R>): Promise<R[]>;
|
|
599
610
|
|
|
600
611
|
}
|
|
601
612
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "socket-function",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.6",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"dependencies": {
|
|
@@ -20,7 +20,6 @@
|
|
|
20
20
|
"type": "yarn tsc --noEmit",
|
|
21
21
|
"emit-dts": "yarn tsc --project tsconfig.declarations.json || exit 0",
|
|
22
22
|
"generate-index-dts": "node ./generateIndexDts.js",
|
|
23
|
-
"update-typings": "yarn generate-index-dts && yarn emit-dts",
|
|
24
23
|
"update-types": "yarn emit-dts && yarn generate-index-dts",
|
|
25
24
|
"prepublishOnly": "yarn update-types",
|
|
26
25
|
"testsni": "yarn typenode ./src/sniTest.ts"
|
package/src/CallFactory.ts
CHANGED
|
@@ -19,8 +19,6 @@ import { measureFnc, measureWrap, registerMeasureInfo } from "./profiling/measur
|
|
|
19
19
|
import { MaybePromise } from "./types";
|
|
20
20
|
import { Zip } from "./Zip";
|
|
21
21
|
import { LZ4 } from "./lz4/LZ4";
|
|
22
|
-
//LZ4.compress;
|
|
23
|
-
//LZ4.decompress;
|
|
24
22
|
|
|
25
23
|
setFlag(require, "pako", "allowclient", true);
|
|
26
24
|
|
|
@@ -269,7 +267,7 @@ export async function createCallFactory(
|
|
|
269
267
|
let arg = originalArgs[0] as any;
|
|
270
268
|
fncHack = `.${arg.DomainName}.${arg.ModuleId}.${arg.FunctionId}`;
|
|
271
269
|
}
|
|
272
|
-
console.log(`SIZE\t${(formatNumberSuffixed(resultSize) +
|
|
270
|
+
console.log(`SIZE\t${(formatNumberSuffixed(resultSize) + `B`).padEnd(4, " ")}\t${formatNumber(data.length)} buffers\tREMOTE CALL\t${call.classGuid}.${call.functionName}${fncHack} at ${Date.now()}`);
|
|
273
271
|
}
|
|
274
272
|
}
|
|
275
273
|
// If sending OR resultPromise throws, we want to error out. This solves some issues with resultPromise
|
|
@@ -282,16 +280,22 @@ export async function createCallFactory(
|
|
|
282
280
|
};
|
|
283
281
|
|
|
284
282
|
let webSocketPromise: Promise<SenderInterface> | undefined;
|
|
283
|
+
let hasEverConnected = false;
|
|
285
284
|
if (webSocketBase) {
|
|
286
285
|
webSocketPromise = Promise.resolve(webSocketBase);
|
|
287
286
|
await initializeWebsocket(webSocketBase);
|
|
288
287
|
}
|
|
289
288
|
|
|
290
|
-
async function initializeWebsocket(newWebSocket: SenderInterface) {
|
|
289
|
+
async function initializeWebsocket(newWebSocket: SenderInterface, skipCloseHandling = false) {
|
|
291
290
|
registerOnce();
|
|
292
291
|
callFactory.receivedInitializeState = undefined;
|
|
293
292
|
|
|
294
293
|
function onClose(error: string) {
|
|
294
|
+
// We try various connections, and if they fail, we will just try other node IDs until we finally do connect, and then we stick with that nodeId, and when it disconnects we need to handle disconnections normally.
|
|
295
|
+
if (skipCloseHandling && !hasEverConnected) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
295
299
|
callFactory.connectionId = { nodeId };
|
|
296
300
|
callFactory.lastClosed = Date.now();
|
|
297
301
|
callFactory.isConnected = false;
|
|
@@ -341,6 +345,7 @@ export async function createCallFactory(
|
|
|
341
345
|
console.log(`Connection established to ${niceConnectionName}`);
|
|
342
346
|
}
|
|
343
347
|
callFactory.isConnected = true;
|
|
348
|
+
hasEverConnected = true;
|
|
344
349
|
resolve();
|
|
345
350
|
});
|
|
346
351
|
newWebSocket.addEventListener("close", () => resolve());
|
|
@@ -348,6 +353,7 @@ export async function createCallFactory(
|
|
|
348
353
|
});
|
|
349
354
|
} else if (newWebSocket.readyState === 1 /* OPEN */) {
|
|
350
355
|
callFactory.isConnected = true;
|
|
356
|
+
hasEverConnected = true;
|
|
351
357
|
} else {
|
|
352
358
|
onClose(new Error(`Websocket received in closed state`).stack!);
|
|
353
359
|
}
|
|
@@ -441,6 +447,23 @@ export async function createCallFactory(
|
|
|
441
447
|
}
|
|
442
448
|
lastConnectionAttempt = Date.now();
|
|
443
449
|
|
|
450
|
+
// Try alternates, and if any work, use them
|
|
451
|
+
try {
|
|
452
|
+
let alternates = await SocketFunction.GET_ALTERNATE_NODE_IDS(nodeId);
|
|
453
|
+
if (alternates) {
|
|
454
|
+
for (let alternateNodeId of alternates) {
|
|
455
|
+
let newWebSocket = createWebsocket(alternateNodeId);
|
|
456
|
+
await initializeWebsocket(newWebSocket, true);
|
|
457
|
+
|
|
458
|
+
if (callFactory.isConnected) {
|
|
459
|
+
return newWebSocket;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
} catch (e) {
|
|
464
|
+
console.error("Error getting alternate node IDs", e);
|
|
465
|
+
}
|
|
466
|
+
|
|
444
467
|
let newWebSocket = createWebsocket(nodeId);
|
|
445
468
|
await initializeWebsocket(newWebSocket);
|
|
446
469
|
|
|
@@ -449,6 +472,7 @@ export async function createCallFactory(
|
|
|
449
472
|
|
|
450
473
|
let pendingCall: MessageHeader & {
|
|
451
474
|
buffers: Buffer[];
|
|
475
|
+
firstReceivedTime?: number;
|
|
452
476
|
} | undefined;
|
|
453
477
|
|
|
454
478
|
async function processPendingCall() {
|
|
@@ -523,7 +547,6 @@ export async function createCallFactory(
|
|
|
523
547
|
if (call.isReturn) {
|
|
524
548
|
if (!SocketFunction.LEGACY_INITIALIZE && call.seqNum === INITIALIZE_STATE_SEQ_NUM) {
|
|
525
549
|
callFactory.receivedInitializeState = call.result as InitializeState;
|
|
526
|
-
console.log(green(`Received initialize state: ${JSON.stringify(callFactory.receivedInitializeState)}`));
|
|
527
550
|
return;
|
|
528
551
|
}
|
|
529
552
|
let callbackObj = pendingCalls.get(call.seqNum);
|
|
@@ -536,10 +559,10 @@ export async function createCallFactory(
|
|
|
536
559
|
}
|
|
537
560
|
if (SocketFunction.logMessages) {
|
|
538
561
|
let call = callbackObj.call;
|
|
539
|
-
console.log(`SIZE\t${(formatNumberSuffixed(resultSize) + "B").padEnd(4, " ")}\tRETURN\t${call.classGuid}.${call.functionName} at ${Date.now()}, (${nodeId} / ${localNodeId})`);
|
|
562
|
+
console.log(`SIZE\t${(formatNumberSuffixed(resultSize) + "B").padEnd(4, " ")}\t${formatNumber(currentBuffers.length)} buffers\tRETURN\t${call.classGuid}.${call.functionName} at ${Date.now()}, (${nodeId} / ${localNodeId})`);
|
|
540
563
|
}
|
|
541
564
|
if (call.isResultCompressed === "LZ4") {
|
|
542
|
-
call.result = await decompressObjLZ4(call.result as Buffer, receiveStats);
|
|
565
|
+
call.result = await decompressObjLZ4(call.result as Buffer[], receiveStats);
|
|
543
566
|
call.isResultCompressed = undefined;
|
|
544
567
|
} else if (call.isResultCompressed === "zip" || call.isResultCompressed === true) {
|
|
545
568
|
call.result = await decompressObj(call.result as Buffer, receiveStats);
|
|
@@ -548,7 +571,7 @@ export async function createCallFactory(
|
|
|
548
571
|
callbackObj.callback(call);
|
|
549
572
|
} else {
|
|
550
573
|
if (call.isArgsCompressed === "LZ4") {
|
|
551
|
-
call.args = await decompressObjLZ4(call.args as any as Buffer, sendStats) as any;
|
|
574
|
+
call.args = await decompressObjLZ4(call.args as any as Buffer[], sendStats) as any;
|
|
552
575
|
call.isArgsCompressed = undefined;
|
|
553
576
|
} else if (call.isArgsCompressed === "zip" || call.isArgsCompressed === true) {
|
|
554
577
|
call.args = await decompressObj(call.args as any as Buffer, sendStats) as any;
|
|
@@ -576,12 +599,12 @@ export async function createCallFactory(
|
|
|
576
599
|
if (callFactory.receivedInitializeState?.supportsLZ4) {
|
|
577
600
|
let compressMode = shouldCompressCall(call);
|
|
578
601
|
if (compressMode !== false) {
|
|
579
|
-
response.result = await compressObjLZ4(response.result, sendStats)
|
|
602
|
+
response.result = await compressObjLZ4(response.result, sendStats);
|
|
580
603
|
response.isResultCompressed = "LZ4";
|
|
581
604
|
}
|
|
582
605
|
} else {
|
|
583
606
|
if (shouldCompressCall(call)) {
|
|
584
|
-
response.result = await compressObj(response.result, sendStats)
|
|
607
|
+
response.result = await compressObj(response.result, sendStats);
|
|
585
608
|
response.isResultCompressed = "zip";
|
|
586
609
|
}
|
|
587
610
|
}
|
|
@@ -693,11 +716,19 @@ export async function createCallFactory(
|
|
|
693
716
|
return;
|
|
694
717
|
}
|
|
695
718
|
if (message instanceof Buffer) {
|
|
696
|
-
if (message.byteLength > 1000 * 1000 * 10) {
|
|
697
|
-
console.log(`Received large packet ${formatNumber(message.byteLength)}B at ${Date.now()}`);
|
|
698
|
-
}
|
|
699
719
|
if (!pendingCall) {
|
|
700
|
-
throw new Error(`Received data without size`);
|
|
720
|
+
throw new Error(`Received data without size ${message.byteLength}B, first 100 bytes: ${message.slice(0, 100).toString("hex")}`);
|
|
721
|
+
}
|
|
722
|
+
if (message.byteLength > 1000 * 1000 * 10 || pendingCall.bufferCount > 1000 && (pendingCall.buffers.length % 100 === 0)) {
|
|
723
|
+
if (pendingCall.buffers.length === 0) {
|
|
724
|
+
console.log(`Received large/many packets ${formatNumber(message.byteLength)}B (${pendingCall.buffers.length} / ${pendingCall.bufferCount}) at ${Date.now()}`);
|
|
725
|
+
} else {
|
|
726
|
+
let elapsed = Date.now() - (pendingCall.firstReceivedTime || 0);
|
|
727
|
+
console.log(`Received large/many packets ${formatNumber(message.byteLength)}B (${pendingCall.buffers.length} / ${pendingCall.bufferCount}) after ${formatTime(elapsed)}`);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
if (pendingCall.buffers.length === 0) {
|
|
731
|
+
pendingCall.firstReceivedTime = Date.now();
|
|
701
732
|
}
|
|
702
733
|
pendingCall.buffers.push(message);
|
|
703
734
|
if (pendingCall.buffers.length !== pendingCall.bufferCount) {
|
|
@@ -771,6 +802,9 @@ type CompressionStats = {
|
|
|
771
802
|
|
|
772
803
|
const compressObj = measureWrap(async function wireCallCompress(obj: unknown, stats: CompressionStats): Promise<Buffer> {
|
|
773
804
|
let buffers = await SocketFunction.WIRE_SERIALIZER.serialize(obj);
|
|
805
|
+
if (buffers.length > 1) {
|
|
806
|
+
throw new Error("Legacy CompressObj only supports single buffer");
|
|
807
|
+
}
|
|
774
808
|
let lengthBuffer = Buffer.from((new Float64Array(buffers.map(x => x.length))).buffer);
|
|
775
809
|
let buffer = Buffer.concat([lengthBuffer, ...buffers]);
|
|
776
810
|
stats.uncompressedSize += buffer.length;
|
|
@@ -794,27 +828,177 @@ const decompressObj = measureWrap(async function wireCallDecompress(obj: Buffer,
|
|
|
794
828
|
return result;
|
|
795
829
|
});
|
|
796
830
|
|
|
797
|
-
const compressObjLZ4 = measureWrap(async function wireCallCompressLZ4(obj: unknown, stats: CompressionStats): Promise<Buffer> {
|
|
798
|
-
let
|
|
799
|
-
let
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
831
|
+
const compressObjLZ4 = measureWrap(async function wireCallCompressLZ4(obj: unknown, stats: CompressionStats): Promise<Buffer[]> {
|
|
832
|
+
let headerParts: number[];
|
|
833
|
+
let dataBuffers: Buffer[];
|
|
834
|
+
|
|
835
|
+
if (obj instanceof Buffer) {
|
|
836
|
+
headerParts = [1];
|
|
837
|
+
dataBuffers = [obj];
|
|
838
|
+
} else if (Array.isArray(obj) && obj.every((x: any) => x instanceof Buffer)) {
|
|
839
|
+
let bufferArray = obj as Buffer[];
|
|
840
|
+
const TARGET_SIZE = 50 * 1024 * 1024;
|
|
841
|
+
const MIN_INDIVIDUAL_SIZE = 10 * 1024 * 1024;
|
|
842
|
+
const MAX_UNSPLIT_SIZE = 100 * 1024 * 1024;
|
|
843
|
+
|
|
844
|
+
let outputBuffers: Buffer[] = [];
|
|
845
|
+
let outputDescriptors: number[][] = [];
|
|
846
|
+
let currentGroup: Buffer[] = [];
|
|
847
|
+
let currentGroupSize = 0;
|
|
848
|
+
|
|
849
|
+
function flushCurrentGroup() {
|
|
850
|
+
if (currentGroup.length > 0) {
|
|
851
|
+
outputBuffers.push(Buffer.concat(currentGroup));
|
|
852
|
+
outputDescriptors.push(currentGroup.map(b => b.length));
|
|
853
|
+
currentGroup = [];
|
|
854
|
+
currentGroupSize = 0;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
for (let buf of bufferArray) {
|
|
859
|
+
if (buf.length >= MIN_INDIVIDUAL_SIZE) {
|
|
860
|
+
flushCurrentGroup();
|
|
861
|
+
|
|
862
|
+
if (buf.length > MAX_UNSPLIT_SIZE) {
|
|
863
|
+
let offset = 0;
|
|
864
|
+
while (offset < buf.length) {
|
|
865
|
+
let chunkSize = Math.min(TARGET_SIZE, buf.length - offset);
|
|
866
|
+
outputBuffers.push(buf.slice(offset, offset + chunkSize));
|
|
867
|
+
outputDescriptors.push([chunkSize]);
|
|
868
|
+
offset += chunkSize;
|
|
869
|
+
}
|
|
870
|
+
} else {
|
|
871
|
+
outputBuffers.push(buf);
|
|
872
|
+
outputDescriptors.push([buf.length]);
|
|
873
|
+
}
|
|
874
|
+
} else {
|
|
875
|
+
currentGroup.push(buf);
|
|
876
|
+
currentGroupSize += buf.length;
|
|
877
|
+
|
|
878
|
+
if (currentGroupSize >= TARGET_SIZE) {
|
|
879
|
+
flushCurrentGroup();
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
flushCurrentGroup();
|
|
885
|
+
|
|
886
|
+
headerParts = [2, outputBuffers.length];
|
|
887
|
+
for (let descriptor of outputDescriptors) {
|
|
888
|
+
headerParts.push(descriptor.length, ...descriptor);
|
|
889
|
+
}
|
|
890
|
+
dataBuffers = outputBuffers;
|
|
891
|
+
} else {
|
|
892
|
+
let buffers = await SocketFunction.WIRE_SERIALIZER.serialize(obj);
|
|
893
|
+
headerParts = [3, buffers.length];
|
|
894
|
+
dataBuffers = buffers;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
let headerBuffer = Buffer.from((new Float64Array(headerParts)).buffer);
|
|
898
|
+
let allBuffers = [headerBuffer, ...dataBuffers];
|
|
899
|
+
|
|
900
|
+
stats.uncompressedSize += allBuffers.reduce((sum, buf) => sum + buf.length, 0);
|
|
901
|
+
|
|
902
|
+
let compressed: Buffer[] = [];
|
|
903
|
+
let startTime = Date.now();
|
|
904
|
+
let lastWarnTime = startTime;
|
|
905
|
+
let currentUncompressedSize = 0;
|
|
906
|
+
let currentCompressedSize = 0;
|
|
907
|
+
|
|
908
|
+
function logIfSlow(i: number) {
|
|
909
|
+
let now = Date.now();
|
|
910
|
+
if (now - lastWarnTime > 500) {
|
|
911
|
+
let elapsed = now - startTime;
|
|
912
|
+
console.log(`Slow LZ4 compress (${formatTime(elapsed)}: ${i + 1}/${allBuffers.length} buffers, ${formatNumber(currentUncompressedSize)}B => ${formatNumber(currentCompressedSize)}B`);
|
|
913
|
+
lastWarnTime = now;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
for (let i = 0; i < allBuffers.length; i++) {
|
|
918
|
+
let buf = allBuffers[i];
|
|
919
|
+
currentUncompressedSize += buf.length;
|
|
920
|
+
let compressedBuf = LZ4.compress(buf);
|
|
921
|
+
compressed.push(compressedBuf);
|
|
922
|
+
currentCompressedSize += compressedBuf.length;
|
|
923
|
+
|
|
924
|
+
logIfSlow(i);
|
|
925
|
+
}
|
|
926
|
+
logIfSlow(allBuffers.length);
|
|
927
|
+
|
|
928
|
+
stats.compressedSize += currentCompressedSize;
|
|
929
|
+
|
|
930
|
+
return compressed;
|
|
805
931
|
});
|
|
806
932
|
|
|
807
|
-
const decompressObjLZ4 = measureWrap(async function wireCallDecompressLZ4(obj: Buffer, stats: CompressionStats): Promise<unknown> {
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
let
|
|
811
|
-
let
|
|
812
|
-
let
|
|
813
|
-
let
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
933
|
+
const decompressObjLZ4 = measureWrap(async function wireCallDecompressLZ4(obj: Buffer[], stats: CompressionStats): Promise<unknown> {
|
|
934
|
+
stats.compressedSize += obj.reduce((sum, buf) => sum + buf.length, 0);
|
|
935
|
+
|
|
936
|
+
let decompressed: Buffer[] = [];
|
|
937
|
+
let startTime = Date.now();
|
|
938
|
+
let lastWarnTime = startTime;
|
|
939
|
+
let currentCompressedSize = 0;
|
|
940
|
+
let currentUncompressedSize = 0;
|
|
941
|
+
function logIfSlow(i: number) {
|
|
942
|
+
let now = Date.now();
|
|
943
|
+
if (now - lastWarnTime > 500) {
|
|
944
|
+
let elapsed = now - startTime;
|
|
945
|
+
console.log(`Slow LZ4 decompress (${formatTime(elapsed)}): ${i + 1}/${obj.length} buffers, ${formatNumber(currentCompressedSize)}B => ${formatNumber(currentUncompressedSize)}B`);
|
|
946
|
+
lastWarnTime = now;
|
|
947
|
+
}
|
|
817
948
|
}
|
|
818
|
-
|
|
819
|
-
|
|
949
|
+
|
|
950
|
+
for (let i = 0; i < obj.length; i++) {
|
|
951
|
+
let buf = obj[i];
|
|
952
|
+
currentCompressedSize += buf.length;
|
|
953
|
+
let decompressedBuf = LZ4.decompress(buf);
|
|
954
|
+
decompressed.push(decompressedBuf);
|
|
955
|
+
currentUncompressedSize += decompressedBuf.length;
|
|
956
|
+
|
|
957
|
+
logIfSlow(i);
|
|
958
|
+
}
|
|
959
|
+
logIfSlow(obj.length);
|
|
960
|
+
|
|
961
|
+
stats.uncompressedSize += currentUncompressedSize;
|
|
962
|
+
|
|
963
|
+
let headerBuffer = decompressed[0];
|
|
964
|
+
let dataBuffers = decompressed.slice(1);
|
|
965
|
+
|
|
966
|
+
let typeBuffer = headerBuffer.slice(0, 8);
|
|
967
|
+
let type = new Float64Array(typeBuffer.buffer, typeBuffer.byteOffset, 1)[0];
|
|
968
|
+
|
|
969
|
+
if (type === 1) {
|
|
970
|
+
return dataBuffers[0];
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if (type === 2) {
|
|
974
|
+
let headerData = new Float64Array(headerBuffer.buffer, headerBuffer.byteOffset, headerBuffer.byteLength / 8);
|
|
975
|
+
let outputBufferCount = headerData[1];
|
|
976
|
+
|
|
977
|
+
let buffers: Buffer[] = [];
|
|
978
|
+
let headerIndex = 2;
|
|
979
|
+
|
|
980
|
+
for (let i = 0; i < outputBufferCount; i++) {
|
|
981
|
+
let inputBufferCount = headerData[headerIndex++];
|
|
982
|
+
let sizes: number[] = [];
|
|
983
|
+
for (let j = 0; j < inputBufferCount; j++) {
|
|
984
|
+
sizes.push(headerData[headerIndex++]);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
let outputBuffer = dataBuffers[i];
|
|
988
|
+
let offset = 0;
|
|
989
|
+
for (let size of sizes) {
|
|
990
|
+
buffers.push(outputBuffer.slice(offset, offset + size));
|
|
991
|
+
offset += size;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
return buffers;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
if (type === 3) {
|
|
999
|
+
let result = await SocketFunction.WIRE_SERIALIZER.deserialize(dataBuffers);
|
|
1000
|
+
return result;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
throw new Error(`Unknown compression type ${type}`);
|
|
820
1004
|
});
|
package/src/batching.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AnyFunction } from "./types";
|
|
1
|
+
import { AnyFunction, MaybePromise } from "./types";
|
|
2
2
|
export type DelayType = (number | "afterio" | "immediate" | "afterpromises" | "paintLoop" | "afterPaint");
|
|
3
3
|
export declare function delay(delayTime: DelayType, immediateShortDelays?: "immediateShortDelays"): Promise<void>;
|
|
4
4
|
export declare function batchFunctionNone<Arg, Result = void>(config: unknown, fnc: (arg: Arg[]) => (Promise<Result> | Result)): (arg: Arg) => Promise<Result>;
|
|
@@ -29,3 +29,10 @@ export declare function retryFunctional<T extends AnyFunction>(fnc: T, config?:
|
|
|
29
29
|
minDelay?: number;
|
|
30
30
|
maxDelay?: number;
|
|
31
31
|
}): T;
|
|
32
|
+
export declare const safeLoop: typeof unblockLoop;
|
|
33
|
+
export declare const throttledLoop: typeof unblockLoop;
|
|
34
|
+
export declare function unblockLoop<T, R>(config: {
|
|
35
|
+
data: T[];
|
|
36
|
+
maxBlockingTime?: number;
|
|
37
|
+
backOffTime?: number;
|
|
38
|
+
} | T[], fnc: (item: T) => MaybePromise<R>): Promise<R[]>;
|
package/src/batching.ts
CHANGED
|
@@ -329,4 +329,41 @@ export function retryFunctional<T extends AnyFunction>(fnc: T, config?: {
|
|
|
329
329
|
return async function (...args: any[]) {
|
|
330
330
|
return await runFnc(args, maxRetries);
|
|
331
331
|
} as any;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export const safeLoop = unblockLoop;
|
|
335
|
+
export const throttledLoop = unblockLoop;
|
|
336
|
+
export async function unblockLoop<T, R>(config: {
|
|
337
|
+
data: T[];
|
|
338
|
+
maxBlockingTime?: number;
|
|
339
|
+
backOffTime?: number;
|
|
340
|
+
} | T[], fnc: (item: T) => MaybePromise<R>): Promise<R[]> {
|
|
341
|
+
|
|
342
|
+
let data = Array.isArray(config) ? config : config.data;
|
|
343
|
+
let maxBlockingTime = 300;
|
|
344
|
+
let backOffTime = 50;
|
|
345
|
+
if (!Array.isArray(config)) {
|
|
346
|
+
maxBlockingTime = config.maxBlockingTime ?? 300;
|
|
347
|
+
backOffTime = config.backOffTime ?? 50;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
let lastYieldTime = Date.now();
|
|
351
|
+
let results: R[] = [];
|
|
352
|
+
|
|
353
|
+
for (let item of data) {
|
|
354
|
+
let result = fnc(item);
|
|
355
|
+
|
|
356
|
+
// If the function returns a promise, await it
|
|
357
|
+
if (result && typeof result === "object" && "then" in result) {
|
|
358
|
+
result = await result;
|
|
359
|
+
}
|
|
360
|
+
results.push(result);
|
|
361
|
+
|
|
362
|
+
// Check if we've been blocking for too long
|
|
363
|
+
if (Date.now() - lastYieldTime > maxBlockingTime) {
|
|
364
|
+
await delay(backOffTime);
|
|
365
|
+
lastYieldTime = Date.now();
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return results;
|
|
332
369
|
}
|