@workglow/util 0.2.6 → 0.2.8

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
  }
@@ -1005,4 +1102,4 @@ export {
1005
1102
  ConsoleLogger
1006
1103
  };
1007
1104
 
1008
- //# debugId=BC54CE88961794D564756E2164756E21
1105
+ //# debugId=36266CB7AEE484F764756E2164756E21