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.
@@ -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.4",
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"
@@ -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) + "B").padEnd(4, " ")}\tREMOTE CALL\t${call.classGuid}.${call.functionName}${fncHack} at ${Date.now()}`);
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) as any;
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) as any;
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 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;
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
- 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;
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
- let result = await SocketFunction.WIRE_SERIALIZER.deserialize(buffers);
819
- return result;
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
  }