socket-function 1.1.4 → 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.
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 });
@@ -72,8 +73,8 @@ export class SocketFunction {
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
74
75
  public static readonly WIRE_SERIALIZER = {
75
- serialize: (obj: unknown): MaybePromise<Buffer[]> => [cborxInstance.encode(obj)],
76
- deserialize: (buffers: Buffer[]): MaybePromise<unknown> => cborxInstance.decode(buffers[0]),
76
+ serialize: measureWrap((obj: unknown): MaybePromise<Buffer[]> => [cborxInstance.encode(obj)], "WIRE_SERIALIZER|serialize"),
77
+ deserialize: measureWrap((buffers: Buffer[]): MaybePromise<unknown> => cborxInstance.decode(buffers[0]), "WIRE_SERIALIZER|deserialize"),
77
78
  };
78
79
 
79
80
  public static WIRE_WARN_TIME = 100;
package/index.d.ts CHANGED
@@ -565,7 +565,7 @@ declare module "socket-function/src/args" {
565
565
  }
566
566
 
567
567
  declare module "socket-function/src/batching" {
568
- import { AnyFunction } from "socket-function/src/types";
568
+ import { AnyFunction, MaybePromise } from "socket-function/src/types";
569
569
  export type DelayType = (number | "afterio" | "immediate" | "afterpromises" | "paintLoop" | "afterPaint");
570
570
  export declare function delay(delayTime: DelayType, immediateShortDelays?: "immediateShortDelays"): Promise<void>;
571
571
  export declare function batchFunctionNone<Arg, Result = void>(config: unknown, fnc: (arg: Arg[]) => (Promise<Result> | Result)): (arg: Arg) => Promise<Result>;
@@ -596,6 +596,13 @@ declare module "socket-function/src/batching" {
596
596
  minDelay?: number;
597
597
  maxDelay?: number;
598
598
  }): T;
599
+ export declare const safeLoop: typeof unblockLoop;
600
+ export declare const throttledLoop: typeof unblockLoop;
601
+ export declare function unblockLoop<T, R>(config: {
602
+ data: T[];
603
+ maxBlockingTime?: number;
604
+ backOffTime?: number;
605
+ } | T[], fnc: (item: T) => MaybePromise<R>): Promise<R[]>;
599
606
 
600
607
  }
601
608
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "socket-function",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "dependencies": {
@@ -269,7 +269,7 @@ export async function createCallFactory(
269
269
  let arg = originalArgs[0] as any;
270
270
  fncHack = `.${arg.DomainName}.${arg.ModuleId}.${arg.FunctionId}`;
271
271
  }
272
- 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()}`);
273
273
  }
274
274
  }
275
275
  // If sending OR resultPromise throws, we want to error out. This solves some issues with resultPromise
@@ -449,6 +449,7 @@ export async function createCallFactory(
449
449
 
450
450
  let pendingCall: MessageHeader & {
451
451
  buffers: Buffer[];
452
+ firstReceivedTime?: number;
452
453
  } | undefined;
453
454
 
454
455
  async function processPendingCall() {
@@ -523,7 +524,6 @@ export async function createCallFactory(
523
524
  if (call.isReturn) {
524
525
  if (!SocketFunction.LEGACY_INITIALIZE && call.seqNum === INITIALIZE_STATE_SEQ_NUM) {
525
526
  callFactory.receivedInitializeState = call.result as InitializeState;
526
- console.log(green(`Received initialize state: ${JSON.stringify(callFactory.receivedInitializeState)}`));
527
527
  return;
528
528
  }
529
529
  let callbackObj = pendingCalls.get(call.seqNum);
@@ -536,10 +536,10 @@ export async function createCallFactory(
536
536
  }
537
537
  if (SocketFunction.logMessages) {
538
538
  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})`);
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})`);
540
540
  }
541
541
  if (call.isResultCompressed === "LZ4") {
542
- call.result = await decompressObjLZ4(call.result as Buffer, receiveStats);
542
+ call.result = await decompressObjLZ4(call.result as Buffer[], receiveStats);
543
543
  call.isResultCompressed = undefined;
544
544
  } else if (call.isResultCompressed === "zip" || call.isResultCompressed === true) {
545
545
  call.result = await decompressObj(call.result as Buffer, receiveStats);
@@ -548,7 +548,7 @@ export async function createCallFactory(
548
548
  callbackObj.callback(call);
549
549
  } else {
550
550
  if (call.isArgsCompressed === "LZ4") {
551
- call.args = await decompressObjLZ4(call.args as any as Buffer, sendStats) as any;
551
+ call.args = await decompressObjLZ4(call.args as any as Buffer[], sendStats) as any;
552
552
  call.isArgsCompressed = undefined;
553
553
  } else if (call.isArgsCompressed === "zip" || call.isArgsCompressed === true) {
554
554
  call.args = await decompressObj(call.args as any as Buffer, sendStats) as any;
@@ -576,12 +576,12 @@ export async function createCallFactory(
576
576
  if (callFactory.receivedInitializeState?.supportsLZ4) {
577
577
  let compressMode = shouldCompressCall(call);
578
578
  if (compressMode !== false) {
579
- response.result = await compressObjLZ4(response.result, sendStats) as any;
579
+ response.result = await compressObjLZ4(response.result, sendStats);
580
580
  response.isResultCompressed = "LZ4";
581
581
  }
582
582
  } else {
583
583
  if (shouldCompressCall(call)) {
584
- response.result = await compressObj(response.result, sendStats) as any;
584
+ response.result = await compressObj(response.result, sendStats);
585
585
  response.isResultCompressed = "zip";
586
586
  }
587
587
  }
@@ -693,11 +693,19 @@ export async function createCallFactory(
693
693
  return;
694
694
  }
695
695
  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
696
  if (!pendingCall) {
700
- 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();
701
709
  }
702
710
  pendingCall.buffers.push(message);
703
711
  if (pendingCall.buffers.length !== pendingCall.bufferCount) {
@@ -771,6 +779,9 @@ type CompressionStats = {
771
779
 
772
780
  const compressObj = measureWrap(async function wireCallCompress(obj: unknown, stats: CompressionStats): Promise<Buffer> {
773
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
+ }
774
785
  let lengthBuffer = Buffer.from((new Float64Array(buffers.map(x => x.length))).buffer);
775
786
  let buffer = Buffer.concat([lengthBuffer, ...buffers]);
776
787
  stats.uncompressedSize += buffer.length;
@@ -794,27 +805,177 @@ const decompressObj = measureWrap(async function wireCallDecompress(obj: Buffer,
794
805
  return result;
795
806
  });
796
807
 
797
- const compressObjLZ4 = measureWrap(async function wireCallCompressLZ4(obj: unknown, stats: CompressionStats): Promise<Buffer> {
798
- let buffers = await SocketFunction.WIRE_SERIALIZER.serialize(obj);
799
- let lengthBuffer = Buffer.from((new Float64Array(buffers.map(x => x.length))).buffer);
800
- let buffer = Buffer.concat([lengthBuffer, ...buffers]);
801
- stats.uncompressedSize += buffer.length;
802
- let result = LZ4.compress(buffer);
803
- stats.compressedSize += result.length;
804
- return result;
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;
805
908
  });
806
909
 
807
- const decompressObjLZ4 = measureWrap(async function wireCallDecompressLZ4(obj: Buffer, stats: CompressionStats): Promise<unknown> {
808
- let buffer = LZ4.decompress(obj);
809
- stats.uncompressedSize += buffer.length - obj.length;
810
- let lengthBuffer = buffer.slice(0, 8);
811
- let lengths = new Float64Array(lengthBuffer.buffer, lengthBuffer.byteOffset, lengthBuffer.byteLength / 8);
812
- let buffers: Buffer[] = [];
813
- let offset = 8;
814
- for (let length of lengths) {
815
- buffers.push(buffer.slice(offset, offset + length));
816
- offset += length;
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
+ }
817
925
  }
818
- let result = await SocketFunction.WIRE_SERIALIZER.deserialize(buffers);
819
- return result;
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}`);
820
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
  }