Skip to content

Commit a52a9a0

Browse files
committed
♻️ Feed process stdio from native listeners instead of readable pumps
Attach data/close listeners to the child's stdio streams and error/close listeners to the child process itself, all in the same synchronous continuation as the spawn, and push chunks straight into signals. The Stdio middleware tasks now consume those signals, which removes the fromReadable subscriptions and the separate error/close watcher tasks. No behavior change: join()/expect() settle exactly as before on each platform, teardown ordering is untouched, and the public API is unchanged.
1 parent 87bc487 commit a52a9a0

2 files changed

Lines changed: 60 additions & 31 deletions

File tree

process/src/exec/posix.ts

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ import {
1313
withResolvers,
1414
} from "effection";
1515
import { unbox, useEvalScope } from "@effectionx/scope-eval";
16-
import { once } from "@effectionx/node/events";
17-
import { fromReadable } from "@effectionx/node/stream";
1816
import type {
1917
CreateOSProcess,
2018
ExecOptions,
@@ -58,9 +56,30 @@ export function* createPosixProcess(
5856
throw new Error("stdout and stderr must be available with stdio: pipe");
5957
}
6058

59+
// Native listeners attached in the same synchronous continuation as the
60+
// spawn: process events and stdio chunks cannot be missed, and no
61+
// readable pumps are needed.
62+
childProcess.once("error", (error) => {
63+
processResult.resolve(Err(error));
64+
});
65+
childProcess.once("close", (code, signal) => {
66+
processResult.resolve(Ok([code ?? undefined, signal ?? undefined]));
67+
});
68+
69+
let raw = {
70+
stdout: createSignal<Uint8Array, void>(),
71+
stderr: createSignal<Uint8Array, void>(),
72+
};
73+
childProcess.stdout.on("data", (chunk: Uint8Array) =>
74+
raw.stdout.send(chunk),
75+
);
76+
childProcess.stdout.once("close", () => raw.stdout.close());
77+
childProcess.stderr.on("data", (chunk: Uint8Array) =>
78+
raw.stderr.send(chunk),
79+
);
80+
childProcess.stderr.once("close", () => raw.stderr.close());
81+
6182
let io = {
62-
stdout: yield* fromReadable(childProcess.stdout),
63-
stderr: yield* fromReadable(childProcess.stderr),
6483
stdoutDone: withResolvers<void>(),
6584
stderrDone: withResolvers<void>(),
6685
};
@@ -69,22 +88,24 @@ export function* createPosixProcess(
6988
let stderr = createSignal<Uint8Array, void>();
7089

7190
yield* spawn(function* () {
72-
let next = yield* io.stdout.next();
91+
let subscription = yield* raw.stdout;
92+
let next = yield* subscription.next();
7393
while (!next.done) {
7494
yield* Stdio.operations.stdout(next.value);
7595
stdout.send(next.value);
76-
next = yield* io.stdout.next();
96+
next = yield* subscription.next();
7797
}
7898
stdout.close();
7999
io.stdoutDone.resolve();
80100
});
81101

82102
yield* spawn(function* () {
83-
let next = yield* io.stderr.next();
103+
let subscription = yield* raw.stderr;
104+
let next = yield* subscription.next();
84105
while (!next.done) {
85106
yield* Stdio.operations.stderr(next.value);
86107
stderr.send(next.value);
87-
next = yield* io.stderr.next();
108+
next = yield* subscription.next();
88109
}
89110
stderr.close();
90111
io.stderrDone.resolve();
@@ -96,16 +117,6 @@ export function* createPosixProcess(
96117
},
97118
};
98119

99-
yield* spawn(function* trapError() {
100-
let [error] = yield* once<[Error]>(childProcess, "error");
101-
processResult.resolve(Err(error));
102-
});
103-
104-
yield* spawn(function* () {
105-
let value = yield* once<ProcessResultValue>(childProcess, "close");
106-
processResult.resolve(Ok(value));
107-
});
108-
109120
function* join() {
110121
let result = yield* processResult.operation;
111122
if (result.ok) {

process/src/exec/win32.ts

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { platform } from "node:os";
22
import { once } from "@effectionx/node/events";
3-
import { fromReadable } from "@effectionx/node/stream";
43
// @ts-types="npm:@types/cross-spawn@6.0.6"
54
import { spawn as spawnProcess } from "cross-spawn";
65
import { ctrlc } from "ctrlc-windows";
@@ -76,9 +75,31 @@ export function* createWin32Process(
7675
throw new Error("stdout and stderr must be available with stdio: pipe");
7776
}
7877

78+
// Native listeners attached in the same synchronous continuation as the
79+
// spawn: process events and stdio chunks cannot be missed, and no
80+
// readable pumps are needed.
81+
let rawClose = withResolvers<ProcessResultValue>();
82+
childProcess.once("error", (error) => {
83+
processResult.resolve(Err(error));
84+
});
85+
childProcess.once("close", (code, signal) => {
86+
rawClose.resolve([code ?? undefined, signal ?? undefined]);
87+
});
88+
89+
let raw = {
90+
stdout: createSignal<Uint8Array, void>(),
91+
stderr: createSignal<Uint8Array, void>(),
92+
};
93+
childProcess.stdout.on("data", (chunk: Uint8Array) =>
94+
raw.stdout.send(chunk),
95+
);
96+
childProcess.stdout.once("close", () => raw.stdout.close());
97+
childProcess.stderr.on("data", (chunk: Uint8Array) =>
98+
raw.stderr.send(chunk),
99+
);
100+
childProcess.stderr.once("close", () => raw.stderr.close());
101+
79102
let io = {
80-
stdout: yield* fromReadable(childProcess.stdout),
81-
stderr: yield* fromReadable(childProcess.stderr),
82103
stdoutDone: withResolvers<void>(),
83104
stderrDone: withResolvers<void>(),
84105
};
@@ -87,22 +108,24 @@ export function* createWin32Process(
87108
const stderr = createSignal<Uint8Array, void>();
88109

89110
yield* spawn(function* () {
90-
let next = yield* io.stdout.next();
111+
let subscription = yield* raw.stdout;
112+
let next = yield* subscription.next();
91113
while (!next.done) {
92114
yield* Stdio.operations.stdout(next.value);
93115
stdout.send(next.value);
94-
next = yield* io.stdout.next();
116+
next = yield* subscription.next();
95117
}
96118
stdout.close();
97119
io.stdoutDone.resolve();
98120
});
99121

100122
yield* spawn(function* () {
101-
let next = yield* io.stderr.next();
123+
let subscription = yield* raw.stderr;
124+
let next = yield* subscription.next();
102125
while (!next.done) {
103126
yield* Stdio.operations.stderr(next.value);
104127
stderr.send(next.value);
105-
next = yield* io.stderr.next();
128+
next = yield* subscription.next();
106129
}
107130
stderr.close();
108131
io.stderrDone.resolve();
@@ -114,13 +137,8 @@ export function* createWin32Process(
114137
},
115138
};
116139

117-
yield* spawn(function* trapError() {
118-
const [error] = yield* once<Error[]>(childProcess, "error");
119-
processResult.resolve(Err(error));
120-
});
121-
122140
yield* spawn(function* () {
123-
let value = yield* once<ProcessResultValue>(childProcess, "close");
141+
let value = yield* rawClose.operation;
124142
// out of band with the finally block below compared to posix as
125143
// win32 is more sensitive to graceful shutdown timing that it is
126144
// worth waiting for stdout and stderr to close before resolving the process result

0 commit comments

Comments
 (0)