@nwire/supervisor 0.12.1 → 0.13.1

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.
Files changed (2) hide show
  1. package/dist/supervisor.js +28 -1
  2. package/package.json +1 -1
@@ -109,9 +109,28 @@ export class RunnerSupervisor extends EventEmitter {
109
109
  };
110
110
  child.stdout?.on("data", capture("stdout"));
111
111
  child.stderr?.on("data", capture("stderr"));
112
+ // A supervised child must never keep the supervisor's own process alive:
113
+ // unref the child handle and its stdio pipes so an in-flight or
114
+ // not-yet-stopped child can't block the parent (or a test runner's
115
+ // worker) from exiting. Data is still captured — unref only detaches the
116
+ // handles from the event-loop keep-alive set, it doesn't stop them.
117
+ child.unref();
118
+ // The piped stdio streams are Sockets at runtime (they carry `.unref`)
119
+ // but typed as the base `Readable`, which doesn't declare it.
120
+ const unref = (s) => {
121
+ s?.unref?.();
122
+ };
123
+ unref(child.stdout);
124
+ unref(child.stderr);
112
125
  child.on("exit", (code, signal) => {
113
126
  proc.exitCode = code;
114
127
  proc.signal = signal;
128
+ // Release the child's stdout/stderr pipes. Node keeps the underlying
129
+ // PipeWrap handles alive until the readable side is destroyed; without
130
+ // this, an exited child leaves open handles that keep the parent (or a
131
+ // test runner's worker) from exiting cleanly.
132
+ child.stdout?.destroy();
133
+ child.stderr?.destroy();
115
134
  // Distinguish clean stop (we asked for it) from a crash.
116
135
  if (proc.status === "stopping") {
117
136
  proc.status = "exited";
@@ -193,8 +212,16 @@ export class RunnerSupervisor extends EventEmitter {
193
212
  this.emit("status", id, "stopping", state.proc);
194
213
  state.child.kill("SIGTERM");
195
214
  const exited = new Promise((resolve) => state.child.once("exit", () => resolve()));
196
- const grace = new Promise((resolve) => setTimeout(() => resolve("escalate"), graceMs));
215
+ // Hold the timer handle so we can clear it the moment the child exits —
216
+ // an uncleared grace timer is a ref'd setTimeout that keeps the process
217
+ // (and a test runner's worker) alive for the full graceMs window.
218
+ let graceTimer;
219
+ const grace = new Promise((resolve) => {
220
+ graceTimer = setTimeout(() => resolve("escalate"), graceMs);
221
+ });
197
222
  const result = await Promise.race([exited.then(() => "exited"), grace]);
223
+ if (graceTimer)
224
+ clearTimeout(graceTimer);
198
225
  if (result === "escalate") {
199
226
  try {
200
227
  // Windows collapses SIGTERM and SIGKILL into the same hard
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nwire/supervisor",
3
- "version": "0.12.1",
3
+ "version": "0.13.1",
4
4
  "private": false,
5
5
  "description": "Spawns and watches Nwire wire processes for dev tooling. Used by `nwire dev` and Studio's vite dev server. Generic process supervisor with stdout/stderr ring buffer, health-check polling, SIGTERM-then-SIGKILL graceful stop.",
6
6
  "keywords": [