socket-function 1.1.3 → 1.1.5

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.
@@ -8,7 +8,7 @@ import * as tls from "tls";
8
8
  import { getClientNodeId, getNodeIdLocation, registerNodeClient } from "./nodeCache";
9
9
  import debugbreak from "debugbreak";
10
10
  import { lazy } from "./caching";
11
- import { red, yellow } from "./formatting/logColors";
11
+ import { blue, green, red, yellow } from "./formatting/logColors";
12
12
  import { isSplitableArray, markArrayAsSplitable } from "./fixLargeNetworkCalls";
13
13
  import { delay, runInfinitePoll, runInSerial } from "./batching";
14
14
  import { formatNumber, formatTime } from "./formatting/format";
@@ -18,6 +18,10 @@ import { setFlag } from "../require/compileFlags";
18
18
  import { measureFnc, measureWrap, registerMeasureInfo } from "./profiling/measure";
19
19
  import { MaybePromise } from "./types";
20
20
  import { Zip } from "./Zip";
21
+ import { LZ4 } from "./lz4/LZ4";
22
+ //LZ4.compress;
23
+ //LZ4.decompress;
24
+
21
25
  setFlag(require, "pako", "allowclient", true);
22
26
 
23
27
  // NOTE: If it is too low, and too many servers disconnect, we can easily spend 100% of our time
@@ -28,7 +32,7 @@ const MIN_RETRY_DELAY = 5000;
28
32
  type InternalCallType = FullCallType & {
29
33
  seqNum: number;
30
34
  isReturn: false;
31
- isArgsCompressed?: boolean;
35
+ isArgsCompressed?: boolean | "LZ4" | "zip";
32
36
  }
33
37
 
34
38
  type InternalReturnType = {
@@ -36,7 +40,7 @@ type InternalReturnType = {
36
40
  result: unknown;
37
41
  error?: string;
38
42
  seqNum: number;
39
- isResultCompressed?: boolean;
43
+ isResultCompressed?: boolean | "LZ4" | "zip";
40
44
  };
41
45
 
42
46
 
@@ -45,6 +49,7 @@ export interface CallFactory {
45
49
  lastClosed: number;
46
50
  closedForever?: boolean;
47
51
  isConnected?: boolean;
52
+ receivedInitializeState?: InitializeState;
48
53
  // NOTE: May or may not have reconnection or retry logic inside of performCall.
49
54
  // Trigger performLocalCall on the other side of the connection
50
55
  performCall(call: CallType): Promise<unknown>;
@@ -69,6 +74,12 @@ export interface SenderInterface {
69
74
  ping?(): void;
70
75
  }
71
76
 
77
+ type InitializeState = {
78
+ supportsLZ4?: boolean;
79
+ };
80
+
81
+ const INITIALIZE_STATE_SEQ_NUM = -1;
82
+
72
83
  let pendingCallCount = 0;
73
84
  let harvestableFailedCalls = 0;
74
85
  const CALL_TIMES_LIMIT = 1000 * 1000 * 10;
@@ -145,6 +156,7 @@ export async function createCallFactory(
145
156
  nodeId,
146
157
  lastClosed: 0,
147
158
  connectionId: { nodeId },
159
+ receivedInitializeState: undefined,
148
160
  onNextDisconnect,
149
161
  async performCall(call: CallType) {
150
162
  let seqNum = nextSeqNum++;
@@ -164,9 +176,18 @@ export async function createCallFactory(
164
176
  compressedSize: 0,
165
177
  };
166
178
  try {
167
- if (shouldCompressCall(fullCall)) {
168
- fullCall.args = await compressObj(fullCall.args, sendStats) as any;
169
- fullCall.isArgsCompressed = true;
179
+ if (callFactory.receivedInitializeState?.supportsLZ4) {
180
+ let compressMode = shouldCompressCall(fullCall);
181
+ // If it's undefined, then we compress it. We basically always want to compress from now on, because LZ4 is so fast.
182
+ if (compressMode !== false) {
183
+ fullCall.args = await compressObjLZ4(fullCall.args, sendStats) as any;
184
+ fullCall.isArgsCompressed = "LZ4";
185
+ }
186
+ } else {
187
+ if (shouldCompressCall(fullCall)) {
188
+ fullCall.args = await compressObj(fullCall.args, sendStats) as any;
189
+ fullCall.isArgsCompressed = "zip";
190
+ }
170
191
  }
171
192
  let dataMaybePromise = SocketFunction.WIRE_SERIALIZER.serialize(fullCall);
172
193
  if (dataMaybePromise instanceof Promise) {
@@ -248,7 +269,7 @@ export async function createCallFactory(
248
269
  let arg = originalArgs[0] as any;
249
270
  fncHack = `.${arg.DomainName}.${arg.ModuleId}.${arg.FunctionId}`;
250
271
  }
251
- console.log(`SIZE\t${(formatNumberSuffixed(resultSize) + "B").padEnd(4, " ")}\tREMOTE CALL\t${call.classGuid}.${call.functionName}${fncHack} at ${Date.now()}`);
272
+ 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()}`);
252
273
  }
253
274
  }
254
275
  // If sending OR resultPromise throws, we want to error out. This solves some issues with resultPromise
@@ -268,6 +289,7 @@ export async function createCallFactory(
268
289
 
269
290
  async function initializeWebsocket(newWebSocket: SenderInterface) {
270
291
  registerOnce();
292
+ callFactory.receivedInitializeState = undefined;
271
293
 
272
294
  function onClose(error: string) {
273
295
  callFactory.connectionId = { nodeId };
@@ -427,6 +449,7 @@ export async function createCallFactory(
427
449
 
428
450
  let pendingCall: MessageHeader & {
429
451
  buffers: Buffer[];
452
+ firstReceivedTime?: number;
430
453
  } | undefined;
431
454
 
432
455
  async function processPendingCall() {
@@ -499,101 +522,37 @@ export async function createCallFactory(
499
522
  };
500
523
 
501
524
  if (call.isReturn) {
525
+ if (!SocketFunction.LEGACY_INITIALIZE && call.seqNum === INITIALIZE_STATE_SEQ_NUM) {
526
+ callFactory.receivedInitializeState = call.result as InitializeState;
527
+ return;
528
+ }
502
529
  let callbackObj = pendingCalls.get(call.seqNum);
503
530
  if (parseTime > SocketFunction.WIRE_WARN_TIME) {
504
531
  console.log(red(`Slow parse, took ${parseTime}ms to parse ${resultSize} bytes, for receiving result of call to ${callbackObj?.call.classGuid}.${callbackObj?.call.functionName}`));
505
532
  }
506
533
  if (!callbackObj) {
507
- console.log(`Got return for unknown call ${call.seqNum} (created at time ${new Date(call.seqNum)})`);
534
+ console.log(blue(`Got return for unknown call ${call.seqNum} (created at time ${new Date(call.seqNum)}), ${nodeId} / ${localNodeId}`));
508
535
  return;
509
536
  }
510
537
  if (SocketFunction.logMessages) {
511
538
  let call = callbackObj.call;
512
- console.log(`SIZE\t${(formatNumberSuffixed(resultSize) + "B").padEnd(4, " ")}\tRETURN\t${call.classGuid}.${call.functionName} at ${Date.now()}, (${nodeId} / ${localNodeId})`);
539
+ 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})`);
513
540
  }
514
- if (call.isResultCompressed) {
541
+ if (call.isResultCompressed === "LZ4") {
542
+ call.result = await decompressObjLZ4(call.result as Buffer[], receiveStats);
543
+ call.isResultCompressed = undefined;
544
+ } else if (call.isResultCompressed === "zip" || call.isResultCompressed === true) {
515
545
  call.result = await decompressObj(call.result as Buffer, receiveStats);
516
- call.isResultCompressed = false;
546
+ call.isResultCompressed = undefined;
517
547
  }
518
548
  callbackObj.callback(call);
519
549
  } else {
520
- if (call.isArgsCompressed) {
550
+ if (call.isArgsCompressed === "LZ4") {
551
+ call.args = await decompressObjLZ4(call.args as any as Buffer[], sendStats) as any;
552
+ call.isArgsCompressed = undefined;
553
+ } else if (call.isArgsCompressed === "zip" || call.isArgsCompressed === true) {
521
554
  call.args = await decompressObj(call.args as any as Buffer, sendStats) as any;
522
- call.isArgsCompressed = false;
523
- }
524
- if (call.functionName === "changeIdentity") {
525
- /*
526
- TODO: Sometimes calls don't get through, even though we know the client made the call. Here are the logs from a failing case:
527
- Exposing Controller ServerController-17ea53da-bbef-4c8b-9eb0-99e263464c6f
528
- Exposing Controller HotReloadController-032b2250-3aac-4187-8c95-75412742b8f5
529
- Exposing Controller TimeController-ddf4753e-fc8a-413f-8cc2-b927dd449976
530
- Updating websocket server options
531
- Updating websocket server trusted certificates
532
- Updating websocket server options
533
- Updating websocket server trusted certificates
534
- Updating websocket server options
535
- Updating websocket server trusted certificates
536
- Trying to listening on 127.0.0.1:4231
537
- Started Listening on planquickly.com:4231 (127.0.0.1) after 5.54s
538
- Mounted on 127-0-0-1.planquickly.com:4231
539
- Exposing Controller RequireController-e2f811f3-14b8-4759-b0d6-73f14516cf1d
540
- Received TCP connection from 127.0.0.1:42105
541
- Received TCP header packet from 127.0.0.1:42105, have 1894 bytes so far, 1 packets
542
- Received TCP connection with SNI "127-0-0-1.planquickly.com". Have handlers for: planquickly.com, 127-0-0-1.planquickly.com
543
- HTTP server connection established 127.0.0.1:42105
544
- HTTP request (GET) https://127-0-0-1.planquickly.com:4231/?hot
545
- HTTP response 106KB (GET) https://127-0-0-1.planquickly.com:4231/?hot
546
- HTTP server socket closed for 127.0.0.1:42105
547
- Received TCP connection from 127.0.0.1:42106
548
- Received TCP header packet from 127.0.0.1:42106, have 1862 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:42106
551
- 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
552
- 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
553
- Received TCP connection from 127.0.0.1:42107
554
- Received TCP header packet from 127.0.0.1:42107, have 1894 bytes so far, 1 packets
555
- Received TCP connection with SNI "127-0-0-1.planquickly.com". Have handlers for: planquickly.com, 127-0-0-1.planquickly.com
556
- HTTP server connection established 127.0.0.1:42107
557
- HTTP server socket closed for 127.0.0.1:42106
558
- HTTP server socket closed for 127.0.0.1:42107
559
- Received TCP connection from 127.0.0.1:42108
560
- Received TCP header packet from 127.0.0.1:42108, have 1830 bytes so far, 1 packets
561
- Received TCP connection with SNI "127-0-0-1.planquickly.com". Have handlers for: planquickly.com, 127-0-0-1.planquickly.com
562
- HTTP server connection established 127.0.0.1:42108
563
- HTTP request (GET) https://127-0-0-1.planquickly.com:4231/node.cjs.map
564
- HTTP response 106KB (GET) https://127-0-0-1.planquickly.com:4231/node.cjs.map
565
- HTTP server socket closed for 127.0.0.1:42108
566
- Received TCP connection from 127.0.0.1:42110
567
- Received TCP header packet from 127.0.0.1:42110, have 1818 bytes so far, 1 packets
568
- Received TCP connection with SNI "127-0-0-1.planquickly.com". Have handlers for: planquickly.com, 127-0-0-1.planquickly.com
569
- HTTP server connection established 127.0.0.1:42110
570
- Received TCP connection from 127.0.0.1:42111
571
- Received TCP header packet from 127.0.0.1:42111, have 1830 bytes so far, 1 packets
572
- Received TCP connection with SNI "127-0-0-1.planquickly.com". Have handlers for: planquickly.com, 127-0-0-1.planquickly.com
573
- HTTP server connection established 127.0.0.1:42111
574
- Received websocket upgrade request for 127.0.0.1:42110
575
- Connection established to client:127.0.0.1:1744150129862.296:0.4118126921519041
576
- 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
577
- 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
578
- SIZE 171B EVALUATE HotReloadController-032b2250-3aac-4187-8c95-75412742b8f5.watchFiles at 1744150129869.296
579
- SIZE 174B EVALUATE ServerController-17ea53da-bbef-4c8b-9eb0-99e263464c6f.testSiteFunction at 1744150129872.296
580
- HTTP server socket closed for 127.0.0.1:42111
581
- SIZE 167B EVALUATE TimeController-ddf4753e-fc8a-413f-8cc2-b927dd449976.getTrueTime at 1744150129893.296
582
- SIZE 167B EVALUATE TimeController-ddf4753e-fc8a-413f-8cc2-b927dd449976.getTrueTime at 1744150129897.296
583
- SIZE 167B EVALUATE TimeController-ddf4753e-fc8a-413f-8cc2-b927dd449976.getTrueTime at 1744150129899.296
584
- SIZE 167B EVALUATE TimeController-ddf4753e-fc8a-413f-8cc2-b927dd449976.getTrueTime at 1744150139907.0776
585
- SIZE 167B EVALUATE TimeController-ddf4753e-fc8a-413f-8cc2-b927dd449976.getTrueTime at 1744150139909.0776
586
- SIZE 167B EVALUATE TimeController-ddf4753e-fc8a-413f-8cc2-b927dd449976.getTrueTime at 1744150139911.0776
587
- Hot reloading due to change: D:/repos/perspectanalytics/ai3/node_modules/socket-function/src/webSocketServer.ts
588
- - The upgrade request finishes, at least once: Received websocket upgrade
589
- - AND, we are receiving some calls, so... that appears to work.
590
- - Maybe the time calls never finish?
591
- - We added logging for when calls finish as well, so we can tell if all the TimeController calls timed out
592
- - ALSO, added more logging to see if the calls were from the same client (which WOULD be a bug, because
593
- the client shouldn't be calling us so often), or, different clients.
594
- - We DO receive more connections than http connections closed. But not that many more...
595
- */
596
- console.log(red(`Call to ${call.classGuid}.${call.functionName} at ${Date.now()}`));
555
+ call.isArgsCompressed = undefined;
597
556
  }
598
557
  if (SocketFunction.logMessages) {
599
558
  console.log(`SIZE\t${(formatNumberSuffixed(resultSize) + "B").padEnd(4, " ")}\tEVALUATE\t${call.classGuid}.${call.functionName} at ${Date.now()}, (${nodeId} / ${localNodeId})`);
@@ -614,9 +573,17 @@ export async function createCallFactory(
614
573
  let timeTaken = Date.now() - time;
615
574
  console.log(`DUR\t${(formatTime(timeTaken)).padEnd(6, " ")}\tFINISH\t${call.classGuid}.${call.functionName} at ${Date.now()}, (${nodeId} / ${localNodeId})`);
616
575
  }
617
- if (shouldCompressCall(call)) {
618
- response.result = await compressObj(response.result, sendStats) as any;
619
- response.isResultCompressed = true;
576
+ if (callFactory.receivedInitializeState?.supportsLZ4) {
577
+ let compressMode = shouldCompressCall(call);
578
+ if (compressMode !== false) {
579
+ response.result = await compressObjLZ4(response.result, sendStats);
580
+ response.isResultCompressed = "LZ4";
581
+ }
582
+ } else {
583
+ if (shouldCompressCall(call)) {
584
+ response.result = await compressObj(response.result, sendStats);
585
+ response.isResultCompressed = "zip";
586
+ }
620
587
  }
621
588
  } catch (e: any) {
622
589
  response = {
@@ -726,11 +693,19 @@ export async function createCallFactory(
726
693
  return;
727
694
  }
728
695
  if (message instanceof Buffer) {
729
- if (message.byteLength > 1000 * 1000 * 10) {
730
- console.log(`Received large packet ${formatNumber(message.byteLength)}B at ${Date.now()}`);
731
- }
732
696
  if (!pendingCall) {
733
- throw new Error(`Received data without size`);
697
+ throw new Error(`Received data without size ${message.byteLength}B, first 100 bytes: ${message.slice(0, 100).toString("hex")}`);
698
+ }
699
+ if (message.byteLength > 1000 * 1000 * 10 || pendingCall.bufferCount > 1000 && (pendingCall.buffers.length % 100 === 0)) {
700
+ if (pendingCall.buffers.length === 0) {
701
+ console.log(`Received large/many packets ${formatNumber(message.byteLength)}B (${pendingCall.buffers.length} / ${pendingCall.bufferCount}) at ${Date.now()}`);
702
+ } else {
703
+ let elapsed = Date.now() - (pendingCall.firstReceivedTime || 0);
704
+ console.log(`Received large/many packets ${formatNumber(message.byteLength)}B (${pendingCall.buffers.length} / ${pendingCall.bufferCount}) after ${formatTime(elapsed)}`);
705
+ }
706
+ }
707
+ if (pendingCall.buffers.length === 0) {
708
+ pendingCall.firstReceivedTime = Date.now();
734
709
  }
735
710
  pendingCall.buffers.push(message);
736
711
  if (pendingCall.buffers.length !== pendingCall.bufferCount) {
@@ -755,6 +730,20 @@ export async function createCallFactory(
755
730
  }
756
731
  }
757
732
 
733
+
734
+ if (!SocketFunction.LEGACY_INITIALIZE) {
735
+ let initState: InitializeState = {
736
+ supportsLZ4: true,
737
+ };
738
+ let initReturn: InternalReturnType = {
739
+ isReturn: true,
740
+ result: initState,
741
+ seqNum: INITIALIZE_STATE_SEQ_NUM,
742
+ };
743
+ let data = await SocketFunction.WIRE_SERIALIZER.serialize(initReturn);
744
+ await send(data);
745
+ }
746
+
758
747
  return callFactory;
759
748
  }
760
749
 
@@ -790,6 +779,9 @@ type CompressionStats = {
790
779
 
791
780
  const compressObj = measureWrap(async function wireCallCompress(obj: unknown, stats: CompressionStats): Promise<Buffer> {
792
781
  let buffers = await SocketFunction.WIRE_SERIALIZER.serialize(obj);
782
+ if (buffers.length > 1) {
783
+ throw new Error("Legacy CompressObj only supports single buffer");
784
+ }
793
785
  let lengthBuffer = Buffer.from((new Float64Array(buffers.map(x => x.length))).buffer);
794
786
  let buffer = Buffer.concat([lengthBuffer, ...buffers]);
795
787
  stats.uncompressedSize += buffer.length;
@@ -811,4 +803,179 @@ const decompressObj = measureWrap(async function wireCallDecompress(obj: Buffer,
811
803
  }
812
804
  let result = await SocketFunction.WIRE_SERIALIZER.deserialize(buffers);
813
805
  return result;
806
+ });
807
+
808
+ const compressObjLZ4 = measureWrap(async function wireCallCompressLZ4(obj: unknown, stats: CompressionStats): Promise<Buffer[]> {
809
+ let headerParts: number[];
810
+ let dataBuffers: Buffer[];
811
+
812
+ if (obj instanceof Buffer) {
813
+ headerParts = [1];
814
+ dataBuffers = [obj];
815
+ } else if (Array.isArray(obj) && obj.every((x: any) => x instanceof Buffer)) {
816
+ let bufferArray = obj as Buffer[];
817
+ const TARGET_SIZE = 50 * 1024 * 1024;
818
+ const MIN_INDIVIDUAL_SIZE = 10 * 1024 * 1024;
819
+ const MAX_UNSPLIT_SIZE = 100 * 1024 * 1024;
820
+
821
+ let outputBuffers: Buffer[] = [];
822
+ let outputDescriptors: number[][] = [];
823
+ let currentGroup: Buffer[] = [];
824
+ let currentGroupSize = 0;
825
+
826
+ function flushCurrentGroup() {
827
+ if (currentGroup.length > 0) {
828
+ outputBuffers.push(Buffer.concat(currentGroup));
829
+ outputDescriptors.push(currentGroup.map(b => b.length));
830
+ currentGroup = [];
831
+ currentGroupSize = 0;
832
+ }
833
+ }
834
+
835
+ for (let buf of bufferArray) {
836
+ if (buf.length >= MIN_INDIVIDUAL_SIZE) {
837
+ flushCurrentGroup();
838
+
839
+ if (buf.length > MAX_UNSPLIT_SIZE) {
840
+ let offset = 0;
841
+ while (offset < buf.length) {
842
+ let chunkSize = Math.min(TARGET_SIZE, buf.length - offset);
843
+ outputBuffers.push(buf.slice(offset, offset + chunkSize));
844
+ outputDescriptors.push([chunkSize]);
845
+ offset += chunkSize;
846
+ }
847
+ } else {
848
+ outputBuffers.push(buf);
849
+ outputDescriptors.push([buf.length]);
850
+ }
851
+ } else {
852
+ currentGroup.push(buf);
853
+ currentGroupSize += buf.length;
854
+
855
+ if (currentGroupSize >= TARGET_SIZE) {
856
+ flushCurrentGroup();
857
+ }
858
+ }
859
+ }
860
+
861
+ flushCurrentGroup();
862
+
863
+ headerParts = [2, outputBuffers.length];
864
+ for (let descriptor of outputDescriptors) {
865
+ headerParts.push(descriptor.length, ...descriptor);
866
+ }
867
+ dataBuffers = outputBuffers;
868
+ } else {
869
+ let buffers = await SocketFunction.WIRE_SERIALIZER.serialize(obj);
870
+ headerParts = [3, buffers.length];
871
+ dataBuffers = buffers;
872
+ }
873
+
874
+ let headerBuffer = Buffer.from((new Float64Array(headerParts)).buffer);
875
+ let allBuffers = [headerBuffer, ...dataBuffers];
876
+
877
+ stats.uncompressedSize += allBuffers.reduce((sum, buf) => sum + buf.length, 0);
878
+
879
+ let compressed: Buffer[] = [];
880
+ let startTime = Date.now();
881
+ let lastWarnTime = startTime;
882
+ let currentUncompressedSize = 0;
883
+ let currentCompressedSize = 0;
884
+
885
+ function logIfSlow(i: number) {
886
+ let now = Date.now();
887
+ if (now - lastWarnTime > 500) {
888
+ let elapsed = now - startTime;
889
+ console.log(`Slow LZ4 compress (${formatTime(elapsed)}: ${i + 1}/${allBuffers.length} buffers, ${formatNumber(currentUncompressedSize)}B => ${formatNumber(currentCompressedSize)}B`);
890
+ lastWarnTime = now;
891
+ }
892
+ }
893
+
894
+ for (let i = 0; i < allBuffers.length; i++) {
895
+ let buf = allBuffers[i];
896
+ currentUncompressedSize += buf.length;
897
+ let compressedBuf = LZ4.compress(buf);
898
+ compressed.push(compressedBuf);
899
+ currentCompressedSize += compressedBuf.length;
900
+
901
+ logIfSlow(i);
902
+ }
903
+ logIfSlow(allBuffers.length);
904
+
905
+ stats.compressedSize += currentCompressedSize;
906
+
907
+ return compressed;
908
+ });
909
+
910
+ const decompressObjLZ4 = measureWrap(async function wireCallDecompressLZ4(obj: Buffer[], stats: CompressionStats): Promise<unknown> {
911
+ stats.compressedSize += obj.reduce((sum, buf) => sum + buf.length, 0);
912
+
913
+ let decompressed: Buffer[] = [];
914
+ let startTime = Date.now();
915
+ let lastWarnTime = startTime;
916
+ let currentCompressedSize = 0;
917
+ let currentUncompressedSize = 0;
918
+ function logIfSlow(i: number) {
919
+ let now = Date.now();
920
+ if (now - lastWarnTime > 500) {
921
+ let elapsed = now - startTime;
922
+ console.log(`Slow LZ4 decompress (${formatTime(elapsed)}): ${i + 1}/${obj.length} buffers, ${formatNumber(currentCompressedSize)}B => ${formatNumber(currentUncompressedSize)}B`);
923
+ lastWarnTime = now;
924
+ }
925
+ }
926
+
927
+ for (let i = 0; i < obj.length; i++) {
928
+ let buf = obj[i];
929
+ currentCompressedSize += buf.length;
930
+ let decompressedBuf = LZ4.decompress(buf);
931
+ decompressed.push(decompressedBuf);
932
+ currentUncompressedSize += decompressedBuf.length;
933
+
934
+ logIfSlow(i);
935
+ }
936
+ logIfSlow(obj.length);
937
+
938
+ stats.uncompressedSize += currentUncompressedSize;
939
+
940
+ let headerBuffer = decompressed[0];
941
+ let dataBuffers = decompressed.slice(1);
942
+
943
+ let typeBuffer = headerBuffer.slice(0, 8);
944
+ let type = new Float64Array(typeBuffer.buffer, typeBuffer.byteOffset, 1)[0];
945
+
946
+ if (type === 1) {
947
+ return dataBuffers[0];
948
+ }
949
+
950
+ if (type === 2) {
951
+ let headerData = new Float64Array(headerBuffer.buffer, headerBuffer.byteOffset, headerBuffer.byteLength / 8);
952
+ let outputBufferCount = headerData[1];
953
+
954
+ let buffers: Buffer[] = [];
955
+ let headerIndex = 2;
956
+
957
+ for (let i = 0; i < outputBufferCount; i++) {
958
+ let inputBufferCount = headerData[headerIndex++];
959
+ let sizes: number[] = [];
960
+ for (let j = 0; j < inputBufferCount; j++) {
961
+ sizes.push(headerData[headerIndex++]);
962
+ }
963
+
964
+ let outputBuffer = dataBuffers[i];
965
+ let offset = 0;
966
+ for (let size of sizes) {
967
+ buffers.push(outputBuffer.slice(offset, offset + size));
968
+ offset += size;
969
+ }
970
+ }
971
+
972
+ return buffers;
973
+ }
974
+
975
+ if (type === 3) {
976
+ let result = await SocketFunction.WIRE_SERIALIZER.deserialize(dataBuffers);
977
+ return result;
978
+ }
979
+
980
+ throw new Error(`Unknown compression type ${type}`);
814
981
  });
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
  }
@@ -1,7 +1,7 @@
1
1
  /// <reference path="../hot/HotReloadController.d.ts" />
2
2
  import { CallerContext, CallType, ClientHookContext, FullCallType, FunctionFlags, HookContext, SocketExposedInterface, SocketExposedShape, SocketFunctionClientHook, SocketFunctionHook, SocketRegistered } from "../SocketFunctionTypes";
3
3
  export declare function getCallFlags(call: CallType): FunctionFlags | undefined;
4
- export declare function shouldCompressCall(call: CallType): boolean;
4
+ export declare function shouldCompressCall(call: CallType): boolean | "LZ4" | undefined;
5
5
  export declare function performLocalCall(config: {
6
6
  call: FullCallType;
7
7
  caller: CallerContext;
@@ -21,7 +21,7 @@ export function getCallFlags(call: CallType): FunctionFlags | undefined {
21
21
  return classes[call.classGuid]?.shape[call.functionName];
22
22
  }
23
23
  export function shouldCompressCall(call: CallType) {
24
- return !!classes[call.classGuid]?.shape[call.functionName]?.compress;
24
+ return classes[call.classGuid]?.shape[call.functionName]?.compress;
25
25
  }
26
26
 
27
27
  export async function performLocalCall(
@@ -0,0 +1,7 @@
1
+ /// <reference types="node" />
2
+ /// <reference types="node" />
3
+ export declare class LZ4 {
4
+ static compress(data: Buffer): Buffer;
5
+ static compressUntracked(data: Buffer): Buffer;
6
+ static decompress(data: Buffer): Buffer;
7
+ }
package/src/lz4/LZ4.ts ADDED
@@ -0,0 +1,32 @@
1
+ // NOTE: Even if we wanted to use the production version, we couldn't because it's not compatible with the client-side code, because they decided to do a file read to load in their WebAssembly.
2
+ import lz4_stream from "./lz4_wasm_nodejs";
3
+ import { measureFnc } from "../profiling/measure";
4
+ export class LZ4 {
5
+ @measureFnc
6
+ static compress(data: Buffer): Buffer {
7
+ return this.compressUntracked(data);
8
+ }
9
+ static compressUntracked(data: Buffer): Buffer {
10
+ try {
11
+ return Buffer.from(lz4_stream.compress(data));
12
+ } catch (e) {
13
+ // Rethrow non errors as properly wrapped errors
14
+ if (!(e && e instanceof Error)) {
15
+ throw new Error(`Error compressing LZ4: ${e}`);
16
+ }
17
+ throw e;
18
+ }
19
+ }
20
+ @measureFnc
21
+ static decompress(data: Buffer): Buffer {
22
+ try {
23
+ return Buffer.from(lz4_stream.decompress(data));
24
+ } catch (e) {
25
+ // Rethrow non errors as properly wrapped errors
26
+ if (!(e && e instanceof Error)) {
27
+ throw new Error(`Error decompressing LZ4: ${e}`);
28
+ }
29
+ throw e;
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,34 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+
4
+ /**
5
+ * Streaming LZ4 compressor (frame format with linked blocks).
6
+ * Concatenate all output chunks to form a complete LZ4 frame.
7
+ */
8
+ export class Lz4StreamCompressor {
9
+ free(): void;
10
+ [Symbol.dispose](): void;
11
+ compress(input: Uint8Array): Uint8Array;
12
+ constructor();
13
+ }
14
+
15
+ /**
16
+ * One-shot block compression with size prepended.
17
+ */
18
+ export function compress(input: Uint8Array): Uint8Array;
19
+
20
+ /**
21
+ * One-shot block decompression with size prepended.
22
+ */
23
+ export function decompress(input: Uint8Array): Uint8Array;
24
+
25
+ /**
26
+ * Decompress an LZ4 stream (frame format).
27
+ * Auto-injects end marker if missing. On error, returns partial data and sets a warning.
28
+ */
29
+ export function decompress_stream(input: Uint8Array): Uint8Array;
30
+
31
+ /**
32
+ * Get and clear the last warning from decompression.
33
+ */
34
+ export function get_last_warning(): string | undefined;