@workglow/util 0.2.5 → 0.2.7

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.
@@ -534,22 +534,28 @@ class WorkerManager {
534
534
  workerFunctions = new Map;
535
535
  workerStreamFunctions = new Map;
536
536
  workerReactiveFunctions = new Map;
537
- lazyFactories = new Map;
537
+ workerFactories = new Map;
538
+ idleTimeouts = new Map;
538
539
  lazyInitPromises = new Map;
539
- registerWorker(name, workerOrFactory) {
540
+ activeCallCounts = new Map;
541
+ idleTimers = new Map;
542
+ terminationPromises = new Map;
543
+ registerWorker(name, workerOrFactory, options) {
540
544
  if (this.workers.has(name)) {
541
545
  throw new Error(`Worker ${name} is already registered.`);
542
546
  }
543
- if (this.lazyFactories.has(name)) {
547
+ if (this.workerFactories.has(name)) {
544
548
  throw new Error(`Worker ${name} is already registered.`);
545
549
  }
550
+ this.idleTimeouts.set(name, options?.idleTimeoutMs ?? 0);
546
551
  if (typeof workerOrFactory === "function") {
547
- this.lazyFactories.set(name, workerOrFactory);
552
+ this.workerFactories.set(name, workerOrFactory);
548
553
  } else {
549
554
  this.attachWorkerInstance(name, workerOrFactory);
550
555
  }
551
556
  }
552
557
  attachWorkerInstance(name, worker) {
558
+ this.clearIdleTimer(name);
553
559
  this.workers.set(name, worker);
554
560
  worker.addEventListener("error", (event) => {
555
561
  console.error("Worker Error:", event.message, "at", event.filename, "line:", event.lineno);
@@ -586,23 +592,27 @@ class WorkerManager {
586
592
  this.readyWorkers.set(name, readyPromise);
587
593
  }
588
594
  async ensureWorkerReady(name) {
595
+ await this.terminationPromises.get(name);
589
596
  if (this.workers.has(name)) {
590
597
  await this.readyWorkers.get(name);
591
598
  return;
592
599
  }
593
- const factory = this.lazyFactories.get(name);
600
+ const factory = this.workerFactories.get(name);
594
601
  if (!factory) {
595
602
  throw new Error(`Worker ${name} not found.`);
596
603
  }
597
604
  let init = this.lazyInitPromises.get(name);
598
605
  if (!init) {
599
606
  init = (async () => {
607
+ let worker;
600
608
  try {
601
- const f = this.lazyFactories.get(name);
602
- this.lazyFactories.delete(name);
603
- const worker = f();
609
+ const f = this.workerFactories.get(name);
610
+ worker = f();
604
611
  this.attachWorkerInstance(name, worker);
605
612
  await this.readyWorkers.get(name);
613
+ } catch (error) {
614
+ await this.cleanupFailedInitialization(name, worker);
615
+ throw error;
606
616
  } finally {
607
617
  this.lazyInitPromises.delete(name);
608
618
  }
@@ -617,105 +627,187 @@ class WorkerManager {
617
627
  throw new Error(`Worker ${name} not found.`);
618
628
  return worker;
619
629
  }
630
+ beginWorkerActivity(name) {
631
+ this.clearIdleTimer(name);
632
+ this.activeCallCounts.set(name, (this.activeCallCounts.get(name) ?? 0) + 1);
633
+ }
634
+ endWorkerActivity(name) {
635
+ const nextCount = (this.activeCallCounts.get(name) ?? 0) - 1;
636
+ if (nextCount > 0) {
637
+ this.activeCallCounts.set(name, nextCount);
638
+ return;
639
+ }
640
+ this.activeCallCounts.delete(name);
641
+ this.scheduleIdleTermination(name);
642
+ }
643
+ clearIdleTimer(name) {
644
+ const timer = this.idleTimers.get(name);
645
+ if (timer !== undefined) {
646
+ clearTimeout(timer);
647
+ this.idleTimers.delete(name);
648
+ }
649
+ }
650
+ scheduleIdleTermination(name) {
651
+ this.clearIdleTimer(name);
652
+ const idleTimeoutMs = this.idleTimeouts.get(name) ?? 0;
653
+ if (idleTimeoutMs <= 0 || !this.workerFactories.has(name) || !this.workers.has(name)) {
654
+ return;
655
+ }
656
+ const timer = setTimeout(() => {
657
+ this.idleTimers.delete(name);
658
+ if ((this.activeCallCounts.get(name) ?? 0) > 0 || !this.workers.has(name)) {
659
+ return;
660
+ }
661
+ this.terminateWorkerInstance(name).catch((error) => {
662
+ getLogger().warn(`Worker ${name} idle termination failed.`, { error });
663
+ });
664
+ }, idleTimeoutMs);
665
+ this.idleTimers.set(name, timer);
666
+ }
667
+ async cleanupFailedInitialization(name, worker) {
668
+ this.clearIdleTimer(name);
669
+ if (worker !== undefined && this.workers.get(name) === worker) {
670
+ this.workers.delete(name);
671
+ }
672
+ this.readyWorkers.delete(name);
673
+ this.workerFunctions.delete(name);
674
+ this.workerStreamFunctions.delete(name);
675
+ this.workerReactiveFunctions.delete(name);
676
+ this.activeCallCounts.delete(name);
677
+ if (worker && "terminate" in worker && typeof worker.terminate === "function") {
678
+ try {
679
+ await worker.terminate();
680
+ } catch {}
681
+ }
682
+ }
683
+ async terminateWorkerInstance(name) {
684
+ const existing = this.terminationPromises.get(name);
685
+ if (existing) {
686
+ await existing;
687
+ return;
688
+ }
689
+ const termination = (async () => {
690
+ this.clearIdleTimer(name);
691
+ const worker = this.workers.get(name);
692
+ this.workers.delete(name);
693
+ this.readyWorkers.delete(name);
694
+ this.workerFunctions.delete(name);
695
+ this.workerStreamFunctions.delete(name);
696
+ this.workerReactiveFunctions.delete(name);
697
+ this.activeCallCounts.delete(name);
698
+ try {
699
+ if (worker && "terminate" in worker && typeof worker.terminate === "function") {
700
+ await worker.terminate();
701
+ }
702
+ } catch {}
703
+ })();
704
+ this.terminationPromises.set(name, termination);
705
+ try {
706
+ await termination;
707
+ } finally {
708
+ this.terminationPromises.delete(name);
709
+ }
710
+ }
620
711
  async callWorkerFunction(workerName, functionName, args, options) {
621
712
  await this.ensureWorkerReady(workerName);
622
713
  const worker = this.workers.get(workerName);
623
714
  if (!worker)
624
715
  throw new Error(`Worker ${workerName} not found.`);
625
- const knownFunctions = this.workerFunctions.get(workerName);
626
- if (knownFunctions && !knownFunctions.has(functionName)) {
627
- throw new Error(`Function "${functionName}" is not registered on worker "${workerName}".`);
628
- }
629
- return new Promise((resolve, reject) => {
630
- const requestId = crypto.randomUUID();
631
- const handleMessage = (event) => {
632
- const { id, type, data } = event.data;
633
- if (id !== requestId)
634
- return;
635
- if (type === "progress" && options?.onProgress) {
636
- options.onProgress(data.progress, data.message, data.details);
637
- getLogger().debug(`Worker ${workerName} function ${functionName} progress: ${data.progress}, `, { data });
638
- } else if (type === "complete") {
639
- cleanup();
640
- getLogger().debug(`Worker ${workerName} function ${functionName} complete.`, { data });
641
- resolve(data);
642
- } else if (type === "error") {
643
- cleanup();
644
- getLogger().debug(`Worker ${workerName} function ${functionName} error.`, { data });
645
- const err = typeof data === "object" && data !== null ? Object.assign(new Error(data.message ?? String(data)), {
646
- name: data.name ?? "Error"
647
- }) : new Error(String(data));
648
- reject(err);
649
- }
650
- };
651
- const handleAbort = () => {
652
- worker.postMessage({ id: requestId, type: "abort" });
653
- getLogger().info(`Worker ${workerName} function ${functionName} aborted.`);
654
- };
655
- const cleanup = () => {
656
- worker.removeEventListener("message", handleMessage);
657
- options?.signal?.removeEventListener("abort", handleAbort);
658
- };
659
- worker.addEventListener("message", handleMessage);
660
- if (options?.signal) {
661
- options.signal.addEventListener("abort", handleAbort, { once: true });
716
+ this.beginWorkerActivity(workerName);
717
+ try {
718
+ const knownFunctions = this.workerFunctions.get(workerName);
719
+ if (knownFunctions && !knownFunctions.has(functionName)) {
720
+ throw new Error(`Function "${functionName}" is not registered on worker "${workerName}".`);
662
721
  }
663
- const message = { id: requestId, type: "call", functionName, args };
664
- worker.postMessage(message);
665
- getLogger().info(`Worker ${workerName} function ${functionName} called.`);
666
- });
722
+ return await new Promise((resolve, reject) => {
723
+ const requestId = crypto.randomUUID();
724
+ const handleMessage = (event) => {
725
+ const { id, type, data } = event.data;
726
+ if (id !== requestId)
727
+ return;
728
+ if (type === "progress" && options?.onProgress) {
729
+ options.onProgress(data.progress, data.message, data.details);
730
+ getLogger().debug(`Worker ${workerName} function ${functionName} progress: ${data.progress}, `, { data });
731
+ } else if (type === "complete") {
732
+ cleanup();
733
+ getLogger().debug(`Worker ${workerName} function ${functionName} complete.`, { data });
734
+ resolve(data);
735
+ } else if (type === "error") {
736
+ cleanup();
737
+ getLogger().debug(`Worker ${workerName} function ${functionName} error.`, { data });
738
+ const err = typeof data === "object" && data !== null ? Object.assign(new Error(data.message ?? String(data)), {
739
+ name: data.name ?? "Error"
740
+ }) : new Error(String(data));
741
+ reject(err);
742
+ }
743
+ };
744
+ const handleAbort = () => {
745
+ worker.postMessage({ id: requestId, type: "abort" });
746
+ getLogger().info(`Worker ${workerName} function ${functionName} aborted.`);
747
+ };
748
+ const cleanup = () => {
749
+ worker.removeEventListener("message", handleMessage);
750
+ options?.signal?.removeEventListener("abort", handleAbort);
751
+ };
752
+ worker.addEventListener("message", handleMessage);
753
+ if (options?.signal) {
754
+ options.signal.addEventListener("abort", handleAbort, { once: true });
755
+ }
756
+ const message = { id: requestId, type: "call", functionName, args };
757
+ worker.postMessage(message);
758
+ getLogger().info(`Worker ${workerName} function ${functionName} called.`);
759
+ });
760
+ } finally {
761
+ this.endWorkerActivity(workerName);
762
+ }
667
763
  }
668
764
  async callWorkerReactiveFunction(workerName, functionName, args) {
669
765
  await this.ensureWorkerReady(workerName);
670
766
  const worker = this.workers.get(workerName);
671
767
  if (!worker)
672
768
  return;
673
- const knownReactive = this.workerReactiveFunctions.get(workerName);
674
- if (knownReactive && !knownReactive.has(functionName))
675
- return;
676
- return new Promise((resolve) => {
677
- const requestId = crypto.randomUUID();
678
- const handleMessage = (event) => {
679
- const { id, type, data } = event.data;
680
- if (id !== requestId)
681
- return;
682
- if (type === "complete") {
683
- cleanup();
684
- resolve(data);
685
- } else if (type === "error") {
686
- cleanup();
687
- getLogger().warn(`Worker ${workerName} reactive function ${functionName} error:`, {
688
- error: data
689
- });
690
- resolve(undefined);
691
- }
692
- };
693
- const cleanup = () => {
694
- worker.removeEventListener("message", handleMessage);
695
- };
696
- worker.addEventListener("message", handleMessage);
697
- const message = { id: requestId, type: "call", functionName, args, reactive: true };
698
- worker.postMessage(message);
699
- getLogger().info(`Worker ${workerName} reactive function ${functionName} called.`);
700
- });
769
+ this.beginWorkerActivity(workerName);
770
+ try {
771
+ const knownReactive = this.workerReactiveFunctions.get(workerName);
772
+ if (knownReactive && !knownReactive.has(functionName))
773
+ return;
774
+ return await new Promise((resolve) => {
775
+ const requestId = crypto.randomUUID();
776
+ const handleMessage = (event) => {
777
+ const { id, type, data } = event.data;
778
+ if (id !== requestId)
779
+ return;
780
+ if (type === "complete") {
781
+ cleanup();
782
+ resolve(data);
783
+ } else if (type === "error") {
784
+ cleanup();
785
+ getLogger().warn(`Worker ${workerName} reactive function ${functionName} error:`, {
786
+ error: data
787
+ });
788
+ resolve(undefined);
789
+ }
790
+ };
791
+ const cleanup = () => {
792
+ worker.removeEventListener("message", handleMessage);
793
+ };
794
+ worker.addEventListener("message", handleMessage);
795
+ const message = { id: requestId, type: "call", functionName, args, reactive: true };
796
+ worker.postMessage(message);
797
+ getLogger().info(`Worker ${workerName} reactive function ${functionName} called.`);
798
+ });
799
+ } finally {
800
+ this.endWorkerActivity(workerName);
801
+ }
701
802
  }
702
803
  async terminateWorker(name) {
703
- const worker = this.workers.get(name);
704
- this.workers.delete(name);
705
- this.readyWorkers.delete(name);
706
- this.workerFunctions.delete(name);
707
- this.workerStreamFunctions.delete(name);
708
- this.workerReactiveFunctions.delete(name);
709
- this.lazyFactories.delete(name);
804
+ await this.terminateWorkerInstance(name);
805
+ this.workerFactories.delete(name);
806
+ this.idleTimeouts.delete(name);
710
807
  this.lazyInitPromises.delete(name);
711
- try {
712
- if (worker && "terminate" in worker && typeof worker.terminate === "function") {
713
- await worker.terminate();
714
- }
715
- } catch {}
716
808
  }
717
809
  async dispose() {
718
- const names = [...this.workers.keys(), ...this.lazyFactories.keys()];
810
+ const names = [...new Set([...this.workers.keys(), ...this.workerFactories.keys()])];
719
811
  for (const name of names) {
720
812
  await this.terminateWorker(name);
721
813
  }
@@ -728,80 +820,85 @@ class WorkerManager {
728
820
  const worker = this.workers.get(workerName);
729
821
  if (!worker)
730
822
  throw new Error(`Worker ${workerName} not found.`);
731
- const knownStream = this.workerStreamFunctions.get(workerName);
732
- const knownFns = this.workerFunctions.get(workerName);
733
- if (knownStream && knownFns && !knownStream.has(functionName) && !knownFns.has(functionName)) {
734
- throw new Error(`Function "${functionName}" is not registered on worker "${workerName}".`);
735
- }
736
- const requestId = crypto.randomUUID();
737
- const queue = [];
738
- let waiting = null;
739
- const notify = () => {
740
- if (waiting) {
741
- const resolve = waiting;
742
- waiting = null;
743
- resolve();
744
- }
745
- };
746
- const handleMessage = (event) => {
747
- const { id, type, data } = event.data;
748
- if (id !== requestId)
749
- return;
750
- if (type === "stream_chunk") {
751
- queue.push({ kind: "event", data });
752
- notify();
753
- } else if (type === "complete") {
754
- queue.push({ kind: "done" });
755
- notify();
756
- } else if (type === "error") {
757
- queue.push({ kind: "error", error: new Error(data) });
758
- notify();
823
+ this.beginWorkerActivity(workerName);
824
+ try {
825
+ const knownStream = this.workerStreamFunctions.get(workerName);
826
+ const knownFns = this.workerFunctions.get(workerName);
827
+ if (knownStream && knownFns && !knownStream.has(functionName) && !knownFns.has(functionName)) {
828
+ throw new Error(`Function "${functionName}" is not registered on worker "${workerName}".`);
759
829
  }
760
- };
761
- const handleAbort = () => {
762
- worker.postMessage({ id: requestId, type: "abort" });
763
- getLogger().info(`Worker ${workerName} stream function ${functionName} aborted.`);
764
- };
765
- const cleanup = () => {
766
- worker.removeEventListener("message", handleMessage);
767
- options?.signal?.removeEventListener("abort", handleAbort);
768
- };
769
- worker.addEventListener("message", handleMessage);
770
- if (options?.signal) {
771
- if (options.signal.aborted) {
772
- cleanup();
773
- throw new Error("Operation aborted");
830
+ const requestId = crypto.randomUUID();
831
+ const queue = [];
832
+ let waiting = null;
833
+ const notify = () => {
834
+ if (waiting) {
835
+ const resolve = waiting;
836
+ waiting = null;
837
+ resolve();
838
+ }
839
+ };
840
+ const handleMessage = (event) => {
841
+ const { id, type, data } = event.data;
842
+ if (id !== requestId)
843
+ return;
844
+ if (type === "stream_chunk") {
845
+ queue.push({ kind: "event", data });
846
+ notify();
847
+ } else if (type === "complete") {
848
+ queue.push({ kind: "done" });
849
+ notify();
850
+ } else if (type === "error") {
851
+ queue.push({ kind: "error", error: new Error(data) });
852
+ notify();
853
+ }
854
+ };
855
+ const handleAbort = () => {
856
+ worker.postMessage({ id: requestId, type: "abort" });
857
+ getLogger().info(`Worker ${workerName} stream function ${functionName} aborted.`);
858
+ };
859
+ const cleanup = () => {
860
+ worker.removeEventListener("message", handleMessage);
861
+ options?.signal?.removeEventListener("abort", handleAbort);
862
+ };
863
+ worker.addEventListener("message", handleMessage);
864
+ if (options?.signal) {
865
+ if (options.signal.aborted) {
866
+ cleanup();
867
+ throw new Error("Operation aborted");
868
+ }
869
+ options.signal.addEventListener("abort", handleAbort, { once: true });
774
870
  }
775
- options.signal.addEventListener("abort", handleAbort, { once: true });
776
- }
777
- const message = { id: requestId, type: "call", functionName, args, stream: true };
778
- worker.postMessage(message);
779
- getLogger().info(`Worker ${workerName} stream function ${functionName} called.`);
780
- let completedNormally = false;
781
- try {
782
- while (true) {
783
- while (queue.length > 0) {
784
- const item = queue.shift();
785
- if (item.kind === "event") {
786
- yield item.data;
787
- } else if (item.kind === "done") {
788
- completedNormally = true;
789
- return;
790
- } else if (item.kind === "error") {
791
- completedNormally = true;
792
- throw item.error;
871
+ const message = { id: requestId, type: "call", functionName, args, stream: true };
872
+ worker.postMessage(message);
873
+ getLogger().info(`Worker ${workerName} stream function ${functionName} called.`);
874
+ let completedNormally = false;
875
+ try {
876
+ while (true) {
877
+ while (queue.length > 0) {
878
+ const item = queue.shift();
879
+ if (item.kind === "event") {
880
+ yield item.data;
881
+ } else if (item.kind === "done") {
882
+ completedNormally = true;
883
+ return;
884
+ } else if (item.kind === "error") {
885
+ completedNormally = true;
886
+ throw item.error;
887
+ }
793
888
  }
889
+ await new Promise((resolve) => {
890
+ waiting = resolve;
891
+ });
892
+ }
893
+ } finally {
894
+ if (!completedNormally) {
895
+ worker.postMessage({ id: requestId, type: "abort" });
896
+ getLogger().info(`Worker ${workerName} stream function ${functionName} aborted.`);
794
897
  }
795
- await new Promise((resolve) => {
796
- waiting = resolve;
797
- });
898
+ cleanup();
798
899
  }
799
900
  } finally {
800
- if (!completedNormally) {
801
- worker.postMessage({ id: requestId, type: "abort" });
802
- getLogger().info(`Worker ${workerName} stream function ${functionName} aborted.`);
803
- }
804
- cleanup();
901
+ this.endWorkerActivity(workerName);
805
902
  }
806
903
  }
807
904
  }
@@ -841,7 +938,6 @@ function repairJson(text) {
841
938
  const stack = [];
842
939
  let inString = false;
843
940
  let escaped = false;
844
- let lastSafeEnd = 0;
845
941
  while (i < len) {
846
942
  const ch = text[i];
847
943
  if (escaped) {
@@ -861,7 +957,6 @@ function repairJson(text) {
861
957
  inString = false;
862
958
  result += ch;
863
959
  i++;
864
- lastSafeEnd = result.length;
865
960
  continue;
866
961
  }
867
962
  result += ch;
@@ -889,7 +984,6 @@ function repairJson(text) {
889
984
  stack.pop();
890
985
  result += ch;
891
986
  i++;
892
- lastSafeEnd = result.length;
893
987
  } else {
894
988
  return closeStack(result, stack);
895
989
  }
@@ -899,7 +993,6 @@ function repairJson(text) {
899
993
  stack.pop();
900
994
  result += ch;
901
995
  i++;
902
- lastSafeEnd = result.length;
903
996
  } else {
904
997
  return closeStack(result, stack);
905
998
  }
@@ -964,11 +1057,7 @@ function cleanTrailing(text) {
964
1057
  return s;
965
1058
  }
966
1059
  function closeStack(text, stack) {
967
- let result = text;
968
- for (let i = stack.length - 1;i >= 0; i--) {
969
- result += stack[i];
970
- }
971
- return result;
1060
+ return text + [...stack].reverse().join("");
972
1061
  }
973
1062
  // src/worker/Worker.node.ts
974
1063
  import { Worker as NodeWorker, isMainThread, parentPort } from "worker_threads";
@@ -1033,4 +1122,4 @@ export {
1033
1122
  ConsoleLogger
1034
1123
  };
1035
1124
 
1036
- //# debugId=D50270F8EE79338864756E2164756E21
1125
+ //# debugId=4F1AE43983D857E464756E2164756E21