@orkestrel/process 0.0.5 → 0.0.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.
@@ -593,17 +593,41 @@ function waitForExit(child, timeout) {
593
593
  return settled.promise.finally(() => clearTimeout(timer));
594
594
  }
595
595
  /**
596
+ * Waits for one child process's streams to close, bounded by a deadline.
597
+ *
598
+ * @param child - The child boundary
599
+ * @param timeout - The milliseconds to wait before giving up
600
+ * @returns True when the host close arrived inside the deadline; false otherwise
601
+ *
602
+ * @example
603
+ * ```ts
604
+ * const closed = await waitForClose(child, 5_000)
605
+ * ```
606
+ */
607
+ function waitForClose(child, timeout) {
608
+ const closed = Promise.withResolvers();
609
+ const expired = Promise.withResolvers();
610
+ const timer = setTimeout(() => expired.resolve(false), timeout);
611
+ child.once("close", closed.resolve);
612
+ return Promise.race([closed.promise.then(() => true), expired.promise]).finally(() => {
613
+ clearTimeout(timer);
614
+ child.off("close", closed.resolve);
615
+ });
616
+ }
617
+ /**
596
618
  * Terminates one child process tree and reports whether its native exit was observed.
597
619
  *
598
620
  * @remarks
599
- * Windows ends the whole tree at once through {@link killTree}, falling back to a direct kill after
600
- * the utility fails, because the host has no cooperative termination to offer. A POSIX host signals
601
- * the process group `SIGTERM`, falls back to the direct child when no group owns its pid, waits
602
- * `grace`, then sends `SIGKILL` through the same route. No signal is initiated after the native exit
603
- * is observed; the window between initiating a signal and the host delivering it belongs to the
604
- * operating system. `confirm` bounds each awaited step rather than the call as a whole: on Windows
605
- * it bounds the `taskkill` call and then the final wait, and on a POSIX host `grace` bounds the
606
- * cooperative wait and `confirm` the final one. Never rejects.
621
+ * An already-exited child is returned on before any route to its pid runs, because the host has
622
+ * reaped that number and may have handed it to another process. Windows ends the whole tree at once
623
+ * through {@link killTree}, falling back to a direct kill after the utility fails, because the host
624
+ * has no cooperative termination to offer. A POSIX host signals the process group `SIGTERM`, falls
625
+ * back to the direct child when no group owns its pid, waits `grace`, then sends `SIGKILL` through
626
+ * the same route. No signal is initiated after the native exit is observed; the window between
627
+ * initiating a signal and the host delivering it belongs to the operating system. `confirm` bounds
628
+ * each awaited step rather than the call as a whole: on Windows it bounds the `taskkill` call and
629
+ * then the final wait, and on a POSIX host `grace` bounds the cooperative wait and `confirm` the
630
+ * final one. Never rejects.
607
631
  *
608
632
  * @param child - The child boundary
609
633
  * @param grace - The cooperative POSIX window in milliseconds between `SIGTERM` and `SIGKILL`
@@ -671,29 +695,23 @@ function buildExecuteResult(input) {
671
695
  //#endregion
672
696
  //#region src/server/Process.ts
673
697
  /**
674
- * One supervised child process with framed output, a bounded backlog, and bounded termination.
698
+ * Supervises one child while keeping every observation channel aligned at termination.
675
699
  *
676
700
  * @remarks
677
- * Standard output is framed through `readline` into a single-consumer stream: each line goes to
678
- * exactly one waiting iterator, so concurrent iterators split the output rather than each receiving
679
- * all of it. While no consumer has ever requested an iterator, stdout keeps draining so `exit` still
680
- * resolves, and retention stops at the `backlog` mark: a
681
- * consumer attaching later receives the retained head, a gap, then the live stream. After an iterator
682
- * has been requested, stdout pauses at the mark and resumes at half of it, so that consumer loses
683
- * nothing before termination and the child feels real backpressure. The mark is soft — the ordinary backlog can
684
- * overshoot it by the line that crossed it plus the rest of its delivered chunk. Termination never
685
- * reapplies backpressure, and retained lines are capped at twice `backlog`; later lines are dropped
686
- * and `truncated` reports the omission. Standard error is decoded and forwarded live as the `stderr`
687
- * event while a byte-bounded raw tail is retained as `evidence`. The typed `emitter` also carries a
688
- * child `error` cause on a spawn fault, a `protocol` {@link ProcessError} on a host-reported stdin
689
- * fault after the constructor input phase, and the terminal `exit`, alongside the `exit` promise.
690
- * A fault arising from the constructor-supplied `input` write or its closing `end` stays quiet
691
- * because the package initiated that sequence. `pid`,
692
- * `code`, and `signal` read the spawned child directly, so the native exit is readable before the
693
- * `exit` promise settles on stdio close. `stop` ends the whole tree and reports whether the native
694
- * exit was observed; `destroy` stops the child, destroys stdin so every pending `send` settles, and
695
- * destroys the emitter last. `destroy` resolving does not imply the child's stdio has closed, so
696
- * `exit` and `lines` can still be outstanding when a descendant holds an inherited pipe.
701
+ * The child's ending and the supervision's ending are distinct. `pid`, `code`, and `signal` read
702
+ * the host child directly, so they expose native exit as soon as the host records it. `settled`,
703
+ * `exit`, `evidence`, and `lines` reach one terminal moment after the read channels close or the
704
+ * bounded `drain` window cuts them off. That window is armed by the child's native exit and by a
705
+ * termination this package initiated, so every way a child can end reaches the moment. The terminal
706
+ * routine freezes `evidence` and ends `lines` before it destroys stdout, stderr, or the emitter, so
707
+ * the push channel never disappears while a pull channel can still change or wait indefinitely.
708
+ *
709
+ * Standard output is framed through `readline` into a single-consumer stream. Lines already framed
710
+ * and queued remain available through the terminal moment, while bytes arriving after a requested
711
+ * termination may be cut off. Standard error is forwarded through the live `stderr` event and
712
+ * retained as a byte-bounded raw tail until that same moment. `stop` ends the child tree and the
713
+ * observation surfaces. `destroy` performs that stop and then destroys the emitter after the
714
+ * frozen terminal state exists.
697
715
  *
698
716
  * @example
699
717
  * ```ts
@@ -712,6 +730,7 @@ var Process = class {
712
730
  #child;
713
731
  #reader;
714
732
  #grace;
733
+ #drain;
715
734
  #evidence;
716
735
  #backlog;
717
736
  #delivery;
@@ -729,12 +748,13 @@ var Process = class {
729
748
  #requested = false;
730
749
  #paused = false;
731
750
  #truncated = false;
732
- #terminating = false;
733
- #closed = false;
751
+ #stopping = false;
752
+ #settled = false;
734
753
  #ended = false;
735
754
  #input = 0;
736
755
  #failure;
737
- #stopping;
756
+ #waiting;
757
+ #termination;
738
758
  #ending;
739
759
  /**
740
760
  * Spawn one child process and begin stream capture.
@@ -746,6 +766,7 @@ var Process = class {
746
766
  const source = options.command;
747
767
  const workspace = options.workspace;
748
768
  const grace = options.grace;
769
+ const drain = options.drain;
749
770
  const evidence = options.evidence;
750
771
  const backlog = options.backlog;
751
772
  const delivery = options.delivery;
@@ -758,6 +779,7 @@ var Process = class {
758
779
  validateCommand(command);
759
780
  validateWorkspace(workspace);
760
781
  validateTimer(grace, "option 'grace'");
782
+ validateTimer(drain, "option 'drain'");
761
783
  validateTimer(delivery, "option 'delivery'");
762
784
  validateBytes(evidence, "option 'evidence'", 0);
763
785
  validateBytes(backlog, "option 'backlog'", 1);
@@ -766,6 +788,7 @@ var Process = class {
766
788
  ...error === void 0 ? {} : { error }
767
789
  });
768
790
  this.#grace = grace ?? _src_core.PROCESS_GRACE;
791
+ this.#drain = drain ?? _src_core.PROCESS_DRAIN;
769
792
  this.#evidence = evidence ?? _src_core.PROCESS_EVIDENCE;
770
793
  this.#backlog = backlog ?? _src_core.PROCESS_BACKLOG;
771
794
  this.#delivery = delivery ?? 0;
@@ -795,6 +818,7 @@ var Process = class {
795
818
  this.#reader.on("line", this.#push.bind(this));
796
819
  this.#reader.once("close", this.#finish.bind(this));
797
820
  this.#child.once("error", (cause) => this.#emitter.emit("error", cause));
821
+ this.#child.once("exit", this.#expire.bind(this));
798
822
  this.#child.once("close", this.#close.bind(this));
799
823
  this.#child.stdin.on("error", (cause) => this.#failInputStream(cause));
800
824
  this.#child.stderr.on("data", this.#retain.bind(this));
@@ -828,11 +852,11 @@ var Process = class {
828
852
  get emitter() {
829
853
  return this.#emitter;
830
854
  }
831
- /** The captured stdout lines, in arrival order, ending when the child's stdout closes. */
855
+ /** The captured stdout lines, in arrival order, ending after queued lines at the terminal moment. */
832
856
  get lines() {
833
857
  return this.#lines;
834
858
  }
835
- /** The decoded byte-bounded stderr tail. */
859
+ /** The decoded byte-bounded stderr tail, frozen at the terminal moment. */
836
860
  get evidence() {
837
861
  return this.#tail.toString("utf8");
838
862
  }
@@ -840,7 +864,15 @@ var Process = class {
840
864
  get truncated() {
841
865
  return this.#truncated;
842
866
  }
843
- /** The terminal child state, observed once from the close event. */
867
+ /** True after the terminal moment arrived and `exit` settled. */
868
+ get settled() {
869
+ return this.#settled;
870
+ }
871
+ /** True after termination began, including after the terminal moment. */
872
+ get stopping() {
873
+ return this.#stopping;
874
+ }
875
+ /** The terminal child state, observed once after stream close or the drain cutoff. */
844
876
  get exit() {
845
877
  return this.#exit.promise;
846
878
  }
@@ -863,7 +895,7 @@ var Process = class {
863
895
  */
864
896
  send(text) {
865
897
  const stdin = this.#child.stdin;
866
- if (this.#closed || this.#terminating || this.#failure !== void 0 || !stdin.writable) return Promise.resolve(false);
898
+ if (this.#settled || this.#stopping || this.#failure !== void 0 || !stdin.writable) return Promise.resolve(false);
867
899
  const settled = Promise.withResolvers();
868
900
  this.#writes.set(settled, void 0);
869
901
  try {
@@ -880,29 +912,32 @@ var Process = class {
880
912
  return settled.promise;
881
913
  }
882
914
  /**
883
- * Terminate the child process tree and await its observed exit.
915
+ * Terminates the child process tree and reaches the terminal observation moment.
884
916
  *
885
917
  * @remarks
886
- * Never rejects, and every call shares one termination.
918
+ * Never rejects, and every call shares one termination. After the termination sequence returns,
919
+ * confirmed or not, it waits at most `drain` for the read channels to close, then cuts them off
920
+ * and settles `exit` when they remain open. A cutoff reached before the native exit reports the
921
+ * `code` and `signal` the host had recorded by then, which is `null` for a child still running.
887
922
  *
888
923
  * @returns True when the child's native exit was observed; false when the confirmation deadline elapsed without it
889
924
  */
890
925
  stop() {
891
- if (this.#stopping !== void 0) return this.#stopping;
892
- this.#stopping = this.#kill();
893
- return this.#stopping;
926
+ if (this.#termination !== void 0) return this.#termination;
927
+ this.#termination = this.#kill();
928
+ return this.#termination;
894
929
  }
895
930
  /**
896
- * Stop the child, close its standard-input channel, and destroy the observation emitter.
931
+ * Stops the child and destroys the observation emitter after the terminal state freezes.
897
932
  *
898
933
  * @remarks
899
- * Always resolves, including when termination was never confirmed.
934
+ * Always resolves, including when termination was never confirmed. Every call shares one barrier.
900
935
  *
901
936
  * @returns The stable barrier shared by every call
902
937
  */
903
938
  destroy() {
904
939
  if (this.#ending !== void 0) return this.#ending;
905
- this.#ending = this.#teardown();
940
+ this.#ending = this.#end();
906
941
  return this.#ending;
907
942
  }
908
943
  #iterate() {
@@ -948,8 +983,8 @@ var Process = class {
948
983
  return;
949
984
  }
950
985
  const bytes = node_buffer.Buffer.byteLength(line) + 1;
951
- const limit = this.#terminating ? this.#backlog * 2 : this.#backlog;
952
- if ((!this.#requested || this.#terminating) && this.#pending + bytes > limit) {
986
+ const limit = this.#stopping ? this.#backlog * 2 : this.#backlog;
987
+ if ((!this.#requested || this.#stopping) && this.#pending + bytes > limit) {
953
988
  this.#truncated = true;
954
989
  return;
955
990
  }
@@ -958,14 +993,14 @@ var Process = class {
958
993
  this.#restrain();
959
994
  }
960
995
  #restrain() {
961
- if (this.#paused || this.#terminating || !this.#requested) return;
996
+ if (this.#paused || this.#stopping || !this.#requested) return;
962
997
  if (this.#pending < this.#backlog) return;
963
998
  this.#paused = true;
964
999
  this.#reader.pause();
965
1000
  }
966
1001
  #relieve() {
967
1002
  if (!this.#paused || this.#ended) return;
968
- if (!this.#terminating && this.#pending > this.#backlog / 2) return;
1003
+ if (!this.#stopping && this.#pending > this.#backlog / 2) return;
969
1004
  this.#paused = false;
970
1005
  this.#reader.resume();
971
1006
  }
@@ -980,24 +1015,40 @@ var Process = class {
980
1015
  this.#waiters.length = 0;
981
1016
  }
982
1017
  #retain(chunk) {
983
- if (!node_buffer.Buffer.isBuffer(chunk) || this.#closed) return;
1018
+ if (!node_buffer.Buffer.isBuffer(chunk) || this.#settled) return;
984
1019
  this.#tail = trimTail(node_buffer.Buffer.concat([this.#tail, chunk]), this.#evidence);
985
1020
  const text = this.#decoder.write(chunk);
986
1021
  if (text.length > 0) this.#emitter.emit("stderr", text);
987
1022
  }
988
- #close(code, signal) {
989
- if (this.#closed) return;
1023
+ #close() {
1024
+ if (this.#settled) return;
1025
+ this.#settle(true);
1026
+ }
1027
+ #expire() {
1028
+ if (this.#settled) return;
1029
+ this.#wait();
1030
+ }
1031
+ #wait() {
1032
+ this.#waiting ??= waitForClose(this.#child, this.#drain).then(() => {
1033
+ if (!this.#settled) this.#settle(false);
1034
+ });
1035
+ return this.#waiting;
1036
+ }
1037
+ #settle(drained) {
990
1038
  const suffix = this.#decoder.end();
991
1039
  if (suffix.length > 0) this.#emitter.emit("stderr", suffix);
992
- this.#closed = true;
1040
+ this.#settled = true;
993
1041
  this.#removeAbortListener();
994
1042
  this.#reader.close();
995
1043
  const exit = Object.freeze({
996
- code,
997
- signal
1044
+ code: this.code,
1045
+ signal: this.signal,
1046
+ drained
998
1047
  });
999
1048
  this.#exit.resolve(exit);
1000
1049
  this.#emitter.emit("exit", exit);
1050
+ this.#child.stdout.destroy();
1051
+ this.#child.stderr.destroy();
1001
1052
  }
1002
1053
  #terminate() {
1003
1054
  this.stop();
@@ -1034,7 +1085,7 @@ var Process = class {
1034
1085
  this.#failInputCallback(cause);
1035
1086
  }
1036
1087
  #failInputCallback(cause) {
1037
- if (this.#failure !== void 0 || this.#terminating) return;
1088
+ if (this.#failure !== void 0 || this.#stopping) return;
1038
1089
  this.#failure = cause;
1039
1090
  this.#settleWrites();
1040
1091
  this.#emitter.emit("error", new _src_core.ProcessError("The standard-input channel failed", {
@@ -1052,16 +1103,17 @@ var Process = class {
1052
1103
  }
1053
1104
  }
1054
1105
  async #kill() {
1055
- this.#terminating = true;
1106
+ this.#stopping = true;
1056
1107
  this.#capBacklog();
1057
1108
  this.#relieve();
1058
1109
  const confirmed = await stopChild(this.#child, this.#grace, _src_core.PROCESS_CONFIRMATION);
1059
1110
  this.#settleWrites();
1060
1111
  this.#child.stdin.destroy();
1112
+ if (!this.#settled) await this.#wait();
1113
+ if (!this.#settled) this.#settle(false);
1061
1114
  return confirmed;
1062
1115
  }
1063
- async #teardown() {
1064
- this.#removeAbortListener();
1116
+ async #end() {
1065
1117
  await this.stop();
1066
1118
  this.#emitter.destroy();
1067
1119
  }
@@ -1080,9 +1132,9 @@ var Process = class {
1080
1132
  * the child registered. `launch` throws a {@link createDuplicateError} when the id is already live
1081
1133
  * and a {@link createProtocolError} after `destroy` has begun, including when the caller's own option
1082
1134
  * getter begins that teardown mid-construction — a spawn fault surfaces through the returned child's
1083
- * `exit`, never from `launch`. `destroy` awaits every child's own teardown, which destroys each
1084
- * child's observation emitter and leaves every subscription on it silently inert, then destroys the
1085
- * registry emitter last.
1135
+ * `exit`, never from `launch`. `destroy` awaits every child's own teardown, including the teardown
1136
+ * of a child a lost race refused, which destroys each child's observation emitter and leaves every
1137
+ * subscription on it silently inert, then destroys the registry emitter last.
1086
1138
  *
1087
1139
  * @example
1088
1140
  * ```ts
@@ -1101,6 +1153,7 @@ var ProcessManager = class {
1101
1153
  #emitter;
1102
1154
  #children = /* @__PURE__ */ new Map();
1103
1155
  #ids = /* @__PURE__ */ new Set();
1156
+ #orphans = /* @__PURE__ */ new Set();
1104
1157
  #destroying = false;
1105
1158
  #ending;
1106
1159
  /**
@@ -1147,11 +1200,11 @@ var ProcessManager = class {
1147
1200
  * @remarks
1148
1201
  * {@link Process} reads every option before it spawns, so a caller's own option getter runs while
1149
1202
  * nothing has started and a throw from one strands no process. A getter that begins `destroy`
1150
- * without throwing is the one remaining race, and it leaves a bounded residual: the child is
1151
- * already spawned, so the launch is refused with a {@link createProtocolError} and that child is
1152
- * torn down asynchronously, bounded by `grace` plus the confirmation window. The `protocol`
1153
- * refusal throws synchronously before the `destroy` barrier settles. The barrier does not cover
1154
- * that child's asynchronous teardown.
1203
+ * without throwing is the one remaining race: the child is already spawned, so the launch is
1204
+ * refused with a {@link createProtocolError} and that child is destroyed rather than adopted, its
1205
+ * teardown bounded by `grace` plus the confirmation window. The `protocol` refusal throws
1206
+ * synchronously, and the `destroy` barrier covers that teardown, so the refused child reaches its
1207
+ * terminal moment before the barrier resolves.
1155
1208
  *
1156
1209
  * @param id - The registry key, unique among live children
1157
1210
  * @param options - The child construction options
@@ -1165,6 +1218,7 @@ var ProcessManager = class {
1165
1218
  const child = this.#construct(id, options);
1166
1219
  if (this.#destroying) {
1167
1220
  this.#ids.delete(id);
1221
+ this.#orphans.add(child);
1168
1222
  child.destroy();
1169
1223
  throw (0, _src_core.createProtocolError)(id);
1170
1224
  }
@@ -1221,6 +1275,11 @@ var ProcessManager = class {
1221
1275
  }
1222
1276
  async #teardown() {
1223
1277
  await Promise.allSettled([...this.#children.values()].map((child) => child.destroy()));
1278
+ while (this.#orphans.size > 0) {
1279
+ const orphans = [...this.#orphans];
1280
+ this.#orphans.clear();
1281
+ await Promise.allSettled(orphans.map((child) => child.destroy()));
1282
+ }
1224
1283
  this.#children.clear();
1225
1284
  this.#ids.clear();
1226
1285
  this.#emitter.destroy();
@@ -1412,6 +1471,7 @@ async function execute(command, options) {
1412
1471
  }));
1413
1472
  }, { once: true });
1414
1473
  terminate.signal.addEventListener("abort", () => {
1474
+ if (finish.signal.aborted) return;
1415
1475
  clearTimeout(timeoutTimer);
1416
1476
  cleanup.abort();
1417
1477
  stopChild(child, grace, _src_core.PROCESS_CONFIRMATION).then(() => {
@@ -1634,6 +1694,7 @@ exports.validateEnvironment = validateEnvironment;
1634
1694
  exports.validateText = validateText;
1635
1695
  exports.validateTimer = validateTimer;
1636
1696
  exports.validateWorkspace = validateWorkspace;
1697
+ exports.waitForClose = waitForClose;
1637
1698
  exports.waitForExit = waitForExit;
1638
1699
 
1639
1700
  //# sourceMappingURL=index.cjs.map