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