@executablemd/runtime 0.11.0 → 0.12.0

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.
package/esm/launcher.js CHANGED
@@ -27,7 +27,8 @@
27
27
  * document help and inspection free of any of this.
28
28
  */
29
29
  import { createApi } from "@effectionx/context-api";
30
- import { ensure, race, resource, scoped, until, withResolvers } from "effection";
30
+ import { ensure, race, resource, scoped, until } from "effection";
31
+ import { once } from "@effectionx/node/events";
31
32
  import { spawn as spawnChild } from "node:child_process";
32
33
  import process from "node:process";
33
34
  export const NATIVE_LAUNCHER_UNAVAILABLE = "no native launcher is installed — this host does not hand a native agent UI " +
@@ -141,8 +142,6 @@ function runForeground(request) {
141
142
  if (command === undefined) {
142
143
  throw new Error("native launch: command must not be empty");
143
144
  }
144
- const settled = withResolvers();
145
- const failed = withResolvers();
146
145
  let child;
147
146
  // Interrupt, then insist. A cancelled document may not continue — or
148
147
  // finish tearing down — while a child still holds the terminal, so this
@@ -153,28 +152,42 @@ function runForeground(request) {
153
152
  // `inherit` is the whole point: the child reads this terminal and draws on
154
153
  // it directly, so nothing between it and the person using it can buffer,
155
154
  // reorder, capture or journal what passes.
156
- child = spawnChild(command, args, {
155
+ const started = spawnChild(command, args, {
157
156
  cwd: request.cwd,
158
157
  env: request.env,
159
158
  stdio: "inherit",
160
159
  });
161
- child.once("error", (error) => failed.reject(error));
162
- child.once("exit", (code, signal) => {
163
- const outcome = {};
164
- if (code !== null) {
165
- outcome.exitCode = code;
166
- }
167
- if (signal !== null) {
168
- outcome.signal = signal;
169
- }
170
- settled.resolve(outcome);
171
- });
172
- return yield* race([settled.operation, failed.operation]);
160
+ child = started;
161
+ // Raced inline, in the same synchronous run as the spawn, so both arms are
162
+ // attached before the child can report anything — a spawned race attaches
163
+ // a turn later. Whichever loses is halted, which is what detaches it.
164
+ return yield* race([
165
+ (function* () {
166
+ const [code, signal] = yield* once(started, "exit");
167
+ const outcome = {};
168
+ if (code !== null) {
169
+ outcome.exitCode = code;
170
+ }
171
+ if (signal !== null) {
172
+ outcome.signal = signal;
173
+ }
174
+ return outcome;
175
+ })(),
176
+ (function* () {
177
+ const [error] = yield* once(started, "error");
178
+ throw error;
179
+ })(),
180
+ ]);
173
181
  });
174
182
  }
175
183
  /**
176
184
  * End one foreground child and wait for it to be gone.
177
185
  *
186
+ * Exported for `packages/runtime/tests/native-launcher.test.ts` and not from
187
+ * `mod.ts`: the listener this installs belongs to a bounded Promise, and the
188
+ * only way to observe that it is released on every settlement path is to hold
189
+ * the child.
190
+ *
178
191
  * Deliberately one promise rather than an Effection race: this runs while the
179
192
  * scope is already being dismantled, and the cheapest correct thing to do
180
193
  * there is to wait on the process's own events instead of starting more
@@ -184,7 +197,7 @@ function runForeground(request) {
184
197
  * is spent — a native UI holding the terminal is not something a cancelled run
185
198
  * can afford to wait on indefinitely.
186
199
  */
187
- function reap(child) {
200
+ export function reap(child) {
188
201
  const pid = child.pid;
189
202
  if (pid === undefined || child.exitCode !== null || child.signalCode !== null) {
190
203
  return Promise.resolve();
@@ -206,6 +219,10 @@ function reap(child) {
206
219
  clearInterval(poll);
207
220
  clearTimeout(escalation);
208
221
  clearTimeout(deadline);
222
+ // The one funnel every settlement goes through — the exit event, the
223
+ // reachability poll, the escalation deadline, and the refusal that
224
+ // rejects — so the handler comes off however this ends.
225
+ child.off("exit", onExit);
209
226
  // Deno's `node:child_process` stops reporting a child's exit once a
210
227
  // signal that child ignored has been delivered, and holds the runtime
211
228
  // open on the handle it will now never settle. Dropping the reference is
@@ -223,7 +240,8 @@ function reap(child) {
223
240
  }
224
241
  resolve();
225
242
  };
226
- child.once("exit", () => done());
243
+ const onExit = () => done();
244
+ child.on("exit", onExit);
227
245
  // Reachability rather than the exit event, because that is the fact this
228
246
  // has to establish and the event is not dependable across runtimes here.
229
247
  const poll = setInterval(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@executablemd/runtime",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Runtime host APIs for executable.md documents.",
5
5
  "homepage": "https://executable.md",
6
6
  "repository": {
@@ -38,9 +38,9 @@
38
38
  "@effectionx/context-api": "0.6.0",
39
39
  "@effectionx/fetch": "0.2.1",
40
40
  "@effectionx/fs": "0.3.0",
41
+ "@effectionx/node": "0.2.5",
41
42
  "@effectionx/process": "0.8.1",
42
- "effection": "4.1.0",
43
- "@effectionx/node": "0.2.4"
43
+ "effection": "4.1.0"
44
44
  },
45
45
  "_generatedBy": "dnt@dev"
46
46
  }
@@ -28,6 +28,7 @@
28
28
  */
29
29
  import { type Api } from "@effectionx/context-api";
30
30
  import type { Operation } from "effection";
31
+ import type { ChildProcess } from "node:child_process";
31
32
  /**
32
33
  * What a provider asks the host to run.
33
34
  *
@@ -86,6 +87,24 @@ interface ForegroundLauncherOptions {
86
87
  * own its exit status, or continue after the UI closes.
87
88
  */
88
89
  export declare function installForegroundLauncher(options?: ForegroundLauncherOptions): Operation<void>;
90
+ /**
91
+ * End one foreground child and wait for it to be gone.
92
+ *
93
+ * Exported for `packages/runtime/tests/native-launcher.test.ts` and not from
94
+ * `mod.ts`: the listener this installs belongs to a bounded Promise, and the
95
+ * only way to observe that it is released on every settlement path is to hold
96
+ * the child.
97
+ *
98
+ * Deliberately one promise rather than an Effection race: this runs while the
99
+ * scope is already being dismantled, and the cheapest correct thing to do
100
+ * there is to wait on the process's own events instead of starting more
101
+ * structured work beside them.
102
+ *
103
+ * A child that ignores the interrupt is killed outright once the grace period
104
+ * is spent — a native UI holding the terminal is not something a cancelled run
105
+ * can afford to wait on indefinitely.
106
+ */
107
+ export declare function reap(child: ChildProcess): Promise<void>;
89
108
  /**
90
109
  * A launcher a host installs when it has no terminal to give away, and no
91
110
  * intention of starting a native UI.