@effectionx/process 0.6.1 → 0.7.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/CHANGELOG.md +269 -0
- package/dist/mod.d.ts +3 -0
- package/dist/mod.d.ts.map +1 -0
- package/{esm → dist}/src/daemon.d.ts +1 -1
- package/dist/src/daemon.d.ts.map +1 -0
- package/{script → dist}/src/exec/api.d.ts +2 -4
- package/dist/src/exec/api.d.ts.map +1 -0
- package/{script → dist}/src/exec/error.d.ts +1 -1
- package/dist/src/exec/error.d.ts.map +1 -0
- package/dist/src/exec/error.js +29 -0
- package/dist/src/exec/posix.d.ts +3 -0
- package/dist/src/exec/posix.d.ts.map +1 -0
- package/{esm → dist}/src/exec/posix.js +12 -13
- package/{esm → dist}/src/exec/win32.d.ts +1 -1
- package/dist/src/exec/win32.d.ts.map +1 -0
- package/{esm → dist}/src/exec/win32.js +20 -13
- package/{script → dist}/src/exec.d.ts +3 -3
- package/dist/src/exec.d.ts.map +1 -0
- package/{esm → dist}/src/exec.js +3 -5
- package/dist/src/helpers.d.ts +4 -0
- package/dist/src/helpers.d.ts.map +1 -0
- package/dist/src/helpers.js +10 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/mod.ts +2 -0
- package/package.json +25 -19
- package/src/daemon.ts +34 -0
- package/src/exec/api.ts +83 -0
- package/src/exec/error.ts +43 -0
- package/src/exec/posix.ts +133 -0
- package/src/exec/win32.ts +217 -0
- package/src/exec.ts +100 -0
- package/src/helpers.ts +12 -0
- package/test/daemon.test.ts +106 -0
- package/test/eventemitter.test.ts +82 -0
- package/test/exec.test.ts +481 -0
- package/test/fixtures/dump-args.js +13 -0
- package/test/fixtures/echo-server.ts +49 -0
- package/test/fixtures/hello-world-failed.js +5 -0
- package/test/fixtures/hello-world.js +5 -0
- package/test/helpers.ts +69 -0
- package/test/output-stream.test.ts +73 -0
- package/tsconfig.json +20 -0
- package/esm/mod.d.ts +0 -3
- package/esm/mod.d.ts.map +0 -1
- package/esm/package.json +0 -3
- package/esm/src/daemon.d.ts.map +0 -1
- package/esm/src/eventemitter.d.ts +0 -22
- package/esm/src/eventemitter.d.ts.map +0 -1
- package/esm/src/eventemitter.js +0 -40
- package/esm/src/exec/api.d.ts +0 -70
- package/esm/src/exec/api.d.ts.map +0 -1
- package/esm/src/exec/error.d.ts +0 -14
- package/esm/src/exec/error.d.ts.map +0 -1
- package/esm/src/exec/error.js +0 -54
- package/esm/src/exec/posix.d.ts +0 -3
- package/esm/src/exec/posix.d.ts.map +0 -1
- package/esm/src/exec/win32.d.ts.map +0 -1
- package/esm/src/exec.d.ts +0 -16
- package/esm/src/exec.d.ts.map +0 -1
- package/esm/src/helpers.d.ts +0 -12
- package/esm/src/helpers.d.ts.map +0 -1
- package/esm/src/helpers.js +0 -71
- package/script/mod.d.ts +0 -3
- package/script/mod.d.ts.map +0 -1
- package/script/mod.js +0 -20
- package/script/package.json +0 -3
- package/script/src/daemon.d.ts +0 -11
- package/script/src/daemon.d.ts.map +0 -1
- package/script/src/daemon.js +0 -23
- package/script/src/eventemitter.d.ts +0 -22
- package/script/src/eventemitter.d.ts.map +0 -1
- package/script/src/eventemitter.js +0 -44
- package/script/src/exec/api.d.ts.map +0 -1
- package/script/src/exec/api.js +0 -2
- package/script/src/exec/error.d.ts.map +0 -1
- package/script/src/exec/error.js +0 -59
- package/script/src/exec/posix.d.ts +0 -3
- package/script/src/exec/posix.d.ts.map +0 -1
- package/script/src/exec/posix.js +0 -117
- package/script/src/exec/win32.d.ts +0 -4
- package/script/src/exec/win32.d.ts.map +0 -1
- package/script/src/exec/win32.js +0 -177
- package/script/src/exec.d.ts.map +0 -1
- package/script/src/exec.js +0 -92
- package/script/src/helpers.d.ts +0 -12
- package/script/src/helpers.d.ts.map +0 -1
- package/script/src/helpers.js +0 -76
- /package/{esm → dist}/mod.js +0 -0
- /package/{esm → dist}/src/daemon.js +0 -0
- /package/{esm → dist}/src/exec/api.js +0 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { platform } from "node:os";
|
|
2
|
+
import { once } from "@effectionx/node/events";
|
|
3
|
+
import { fromReadable } from "@effectionx/node/stream";
|
|
4
|
+
// @ts-types="npm:@types/cross-spawn@6.0.6"
|
|
5
|
+
import { spawn as spawnProcess } from "cross-spawn";
|
|
6
|
+
import { ctrlc } from "ctrlc-windows";
|
|
7
|
+
import {
|
|
8
|
+
Err,
|
|
9
|
+
Ok,
|
|
10
|
+
type Result,
|
|
11
|
+
all,
|
|
12
|
+
createSignal,
|
|
13
|
+
resource,
|
|
14
|
+
spawn,
|
|
15
|
+
withResolvers,
|
|
16
|
+
} from "effection";
|
|
17
|
+
import type { CreateOSProcess, ExitStatus, Writable } from "./api.ts";
|
|
18
|
+
import { ExecError } from "./error.ts";
|
|
19
|
+
|
|
20
|
+
type ProcessResultValue = [number?, string?];
|
|
21
|
+
|
|
22
|
+
function* killTree(pid: number) {
|
|
23
|
+
try {
|
|
24
|
+
const killer = spawnProcess(
|
|
25
|
+
"cmd.exe",
|
|
26
|
+
["/c", "taskkill", "/PID", String(pid), "/T", "/F"],
|
|
27
|
+
{ windowsHide: true, stdio: "ignore" },
|
|
28
|
+
);
|
|
29
|
+
yield* once(killer, "close");
|
|
30
|
+
} catch (_) {
|
|
31
|
+
// best-effort; ignore errors
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const createWin32Process: CreateOSProcess = (command, options) => {
|
|
36
|
+
return resource(function* (provide) {
|
|
37
|
+
let processResult = withResolvers<Result<ProcessResultValue>>();
|
|
38
|
+
|
|
39
|
+
// Windows-specific process spawning with different options than POSIX
|
|
40
|
+
let childProcess = spawnProcess(command, options.arguments || [], {
|
|
41
|
+
// We lose exit information and events if this is detached in windows
|
|
42
|
+
// and it opens a window in windows+powershell.
|
|
43
|
+
detached: false,
|
|
44
|
+
// The `shell` option is passed to `cross-spawn` to control whether a shell is used.
|
|
45
|
+
// On Windows, `shell: true` is necessary to run command strings, as it uses
|
|
46
|
+
// `cmd.exe` to parse the command and find executables in the PATH.
|
|
47
|
+
// Using a boolean `true` was previously disabled, causing ENOENT errors for
|
|
48
|
+
// commands that were not a direct path to an executable.
|
|
49
|
+
shell: options.shell || false,
|
|
50
|
+
// With stdio as pipe, windows gets stuck where neither the child nor the
|
|
51
|
+
// parent wants to close the stream, so we call it ourselves in the exit event.
|
|
52
|
+
stdio: "pipe",
|
|
53
|
+
// Hide the child window so that killing it will not block the parent
|
|
54
|
+
// with a Terminate Batch Process (Y/n)
|
|
55
|
+
windowsHide: true,
|
|
56
|
+
env: options.env,
|
|
57
|
+
cwd: options.cwd,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
let { pid } = childProcess;
|
|
61
|
+
|
|
62
|
+
if (!childProcess.stdout || !childProcess.stderr) {
|
|
63
|
+
throw new Error("stdout and stderr must be available with stdio: pipe");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let io = {
|
|
67
|
+
stdout: yield* fromReadable(childProcess.stdout),
|
|
68
|
+
stderr: yield* fromReadable(childProcess.stderr),
|
|
69
|
+
stdoutDone: withResolvers<void>(),
|
|
70
|
+
stderrDone: withResolvers<void>(),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const stdout = createSignal<Uint8Array, void>();
|
|
74
|
+
const stderr = createSignal<Uint8Array, void>();
|
|
75
|
+
|
|
76
|
+
yield* spawn(function* () {
|
|
77
|
+
let next = yield* io.stdout.next();
|
|
78
|
+
while (!next.done) {
|
|
79
|
+
stdout.send(next.value);
|
|
80
|
+
next = yield* io.stdout.next();
|
|
81
|
+
}
|
|
82
|
+
stdout.close();
|
|
83
|
+
io.stdoutDone.resolve();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
yield* spawn(function* () {
|
|
87
|
+
let next = yield* io.stderr.next();
|
|
88
|
+
while (!next.done) {
|
|
89
|
+
stderr.send(next.value);
|
|
90
|
+
next = yield* io.stderr.next();
|
|
91
|
+
}
|
|
92
|
+
stderr.close();
|
|
93
|
+
io.stderrDone.resolve();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
let stdin: Writable<string> = {
|
|
97
|
+
send(data: string) {
|
|
98
|
+
childProcess.stdin.write(data);
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
yield* spawn(function* trapError() {
|
|
103
|
+
const [error] = yield* once<Error[]>(childProcess, "error");
|
|
104
|
+
processResult.resolve(Err(error));
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
yield* spawn(function* () {
|
|
108
|
+
let value = yield* once<ProcessResultValue>(childProcess, "close");
|
|
109
|
+
yield* all([io.stdoutDone.operation, io.stderrDone.operation]);
|
|
110
|
+
processResult.resolve(Ok(value));
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
function* join() {
|
|
114
|
+
let result = yield* processResult.operation;
|
|
115
|
+
if (result.ok) {
|
|
116
|
+
let [code, signal] = result.value;
|
|
117
|
+
return { command, options, code, signal } as ExitStatus;
|
|
118
|
+
}
|
|
119
|
+
throw result.error;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function* expect() {
|
|
123
|
+
let status = yield* join();
|
|
124
|
+
if (status.code !== 0) {
|
|
125
|
+
throw new ExecError(status, command, options);
|
|
126
|
+
}
|
|
127
|
+
return status;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Suppress EPIPE errors on stdin - these occur on Windows when the child
|
|
131
|
+
// process exits before we finish writing to it. This is expected during
|
|
132
|
+
// cleanup when we're killing the process.
|
|
133
|
+
childProcess.stdin.on("error", (err: Error & { code?: string }) => {
|
|
134
|
+
if (err.code !== "EPIPE") {
|
|
135
|
+
throw err;
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
yield* provide({
|
|
141
|
+
pid: pid as number,
|
|
142
|
+
stdin,
|
|
143
|
+
stdout,
|
|
144
|
+
stderr,
|
|
145
|
+
join,
|
|
146
|
+
expect,
|
|
147
|
+
});
|
|
148
|
+
} finally {
|
|
149
|
+
try {
|
|
150
|
+
// Only try to kill the process if it hasn't exited yet
|
|
151
|
+
if (
|
|
152
|
+
childProcess.exitCode === null &&
|
|
153
|
+
childProcess.signalCode === null
|
|
154
|
+
) {
|
|
155
|
+
if (typeof childProcess.pid === "undefined") {
|
|
156
|
+
// biome-ignore lint/correctness/noUnsafeFinally: Intentional error for missing PID
|
|
157
|
+
throw new Error("no pid for childProcess");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let stdinStream = childProcess.stdin;
|
|
161
|
+
|
|
162
|
+
// Try graceful shutdown with ctrlc
|
|
163
|
+
try {
|
|
164
|
+
ctrlc(childProcess.pid);
|
|
165
|
+
if (stdinStream.writable) {
|
|
166
|
+
try {
|
|
167
|
+
// Terminate batch process (Y/N)
|
|
168
|
+
stdinStream.write("Y\n");
|
|
169
|
+
} catch (_err) {
|
|
170
|
+
// not much we can do here
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
} catch (_err) {
|
|
174
|
+
// ctrlc might fail
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Close stdin to allow process to exit cleanly
|
|
178
|
+
try {
|
|
179
|
+
stdinStream.end();
|
|
180
|
+
} catch (_err) {
|
|
181
|
+
// stdin might already be closed
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// If process still hasn't exited, escalate
|
|
185
|
+
if (
|
|
186
|
+
childProcess.exitCode === null &&
|
|
187
|
+
childProcess.signalCode === null
|
|
188
|
+
) {
|
|
189
|
+
// Try regular kill first
|
|
190
|
+
try {
|
|
191
|
+
childProcess.kill();
|
|
192
|
+
} catch (_err) {
|
|
193
|
+
// process might already be dead
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// If still alive after kill, force-kill entire process tree
|
|
197
|
+
// This is necessary for bash on Windows where ctrlc doesn't work
|
|
198
|
+
// and child.kill() only kills the shell, leaving grandchildren alive
|
|
199
|
+
if (
|
|
200
|
+
childProcess.exitCode === null &&
|
|
201
|
+
childProcess.signalCode === null
|
|
202
|
+
) {
|
|
203
|
+
yield* killTree(childProcess.pid);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Wait for streams to finish
|
|
208
|
+
yield* all([io.stdoutDone.operation, io.stderrDone.operation]);
|
|
209
|
+
}
|
|
210
|
+
} catch (_e) {
|
|
211
|
+
// do nothing, process is probably already dead
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export const isWin32 = (): boolean => platform() === "win32";
|
package/src/exec.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import shellwords from "shellwords-ts";
|
|
2
|
+
|
|
3
|
+
import { type Operation, spawn } from "effection";
|
|
4
|
+
import type {
|
|
5
|
+
CreateOSProcess,
|
|
6
|
+
ExecOptions,
|
|
7
|
+
ExitStatus,
|
|
8
|
+
Process,
|
|
9
|
+
ProcessResult,
|
|
10
|
+
} from "./exec/api.ts";
|
|
11
|
+
import { createPosixProcess } from "./exec/posix.ts";
|
|
12
|
+
import { createWin32Process, isWin32 } from "./exec/win32.ts";
|
|
13
|
+
|
|
14
|
+
export * from "./exec/api.ts";
|
|
15
|
+
export * from "./exec/error.ts";
|
|
16
|
+
|
|
17
|
+
export interface Exec extends Operation<Process> {
|
|
18
|
+
join(): Operation<ProcessResult>;
|
|
19
|
+
expect(): Operation<ProcessResult>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const createProcess: CreateOSProcess = (cmd, opts) => {
|
|
23
|
+
if (isWin32()) {
|
|
24
|
+
return createWin32Process(cmd, opts);
|
|
25
|
+
}
|
|
26
|
+
return createPosixProcess(cmd, opts);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Execute `command` with `options`. You should use this operation for processes
|
|
31
|
+
* that have a finite lifetime and on which you may wish to synchronize on the
|
|
32
|
+
* exit status. If you want to start a process like a server that spins up and runs
|
|
33
|
+
* forever, consider using `daemon()`
|
|
34
|
+
*/
|
|
35
|
+
export function exec(command: string, options: ExecOptions = {}): Exec {
|
|
36
|
+
let [cmd, ...args] = options.shell ? [command] : shellwords.split(command);
|
|
37
|
+
let opts = { ...options, arguments: args.concat(options.arguments || []) };
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
*[Symbol.iterator]() {
|
|
41
|
+
return yield* createProcess(cmd, opts);
|
|
42
|
+
},
|
|
43
|
+
*join() {
|
|
44
|
+
const process = yield* createProcess(cmd, opts);
|
|
45
|
+
|
|
46
|
+
let stdout = "";
|
|
47
|
+
let stderr = "";
|
|
48
|
+
|
|
49
|
+
yield* spawn(function* () {
|
|
50
|
+
let subscription = yield* process.stdout;
|
|
51
|
+
let next = yield* subscription.next();
|
|
52
|
+
while (!next.done) {
|
|
53
|
+
stdout += next.value;
|
|
54
|
+
next = yield* subscription.next();
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
yield* spawn(function* () {
|
|
59
|
+
let subscription = yield* process.stderr;
|
|
60
|
+
let next = yield* subscription.next();
|
|
61
|
+
while (!next.done) {
|
|
62
|
+
stderr += next.value;
|
|
63
|
+
next = yield* subscription.next();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
let status: ExitStatus = yield* process.join();
|
|
68
|
+
|
|
69
|
+
return { ...status, stdout, stderr };
|
|
70
|
+
},
|
|
71
|
+
*expect() {
|
|
72
|
+
const process = yield* createProcess(cmd, opts);
|
|
73
|
+
|
|
74
|
+
let stdout = "";
|
|
75
|
+
let stderr = "";
|
|
76
|
+
|
|
77
|
+
yield* spawn(function* () {
|
|
78
|
+
let subscription = yield* process.stdout;
|
|
79
|
+
let next = yield* subscription.next();
|
|
80
|
+
while (!next.done) {
|
|
81
|
+
stdout += next.value;
|
|
82
|
+
next = yield* subscription.next();
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
yield* spawn(function* () {
|
|
87
|
+
let subscription = yield* process.stderr;
|
|
88
|
+
let next = yield* subscription.next();
|
|
89
|
+
while (!next.done) {
|
|
90
|
+
stderr += next.value;
|
|
91
|
+
next = yield* subscription.next();
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
let status: ExitStatus = yield* process.expect();
|
|
96
|
+
|
|
97
|
+
return { ...status, stdout, stderr };
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Err, Ok, type Operation, type Result, type Stream } from "effection";
|
|
2
|
+
|
|
3
|
+
export type OutputStream = Stream<Uint8Array, void>;
|
|
4
|
+
|
|
5
|
+
export function* box<T>(op: () => Operation<T>): Operation<Result<T>> {
|
|
6
|
+
try {
|
|
7
|
+
const value = yield* op();
|
|
8
|
+
return Ok(value);
|
|
9
|
+
} catch (e) {
|
|
10
|
+
return Err(e as Error);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { beforeEach, describe, it } from "@effectionx/bdd";
|
|
3
|
+
import { type Task, spawn, until, withResolvers } from "effection";
|
|
4
|
+
import { expect } from "expect";
|
|
5
|
+
|
|
6
|
+
import { lines } from "@effectionx/stream-helpers";
|
|
7
|
+
import { type Daemon, daemon } from "../mod.ts";
|
|
8
|
+
import { captureError, expectMatch, fetchText } from "./helpers.ts";
|
|
9
|
+
|
|
10
|
+
const SystemRoot = process.env.SystemRoot;
|
|
11
|
+
|
|
12
|
+
describe("daemon", () => {
|
|
13
|
+
let task: Task<void>;
|
|
14
|
+
let proc: Daemon;
|
|
15
|
+
|
|
16
|
+
describe("controlling from outside", () => {
|
|
17
|
+
beforeEach(function* () {
|
|
18
|
+
const result = withResolvers<Daemon>();
|
|
19
|
+
task = yield* spawn<void>(function* () {
|
|
20
|
+
let proc = yield* daemon("node", {
|
|
21
|
+
arguments: [
|
|
22
|
+
"--experimental-strip-types",
|
|
23
|
+
"./fixtures/echo-server.ts",
|
|
24
|
+
],
|
|
25
|
+
env: {
|
|
26
|
+
PORT: "29002",
|
|
27
|
+
PATH: process.env.PATH as string,
|
|
28
|
+
...(SystemRoot ? { SystemRoot } : {}),
|
|
29
|
+
},
|
|
30
|
+
cwd: import.meta.dirname,
|
|
31
|
+
});
|
|
32
|
+
result.resolve(proc);
|
|
33
|
+
yield* proc;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
proc = yield* result.operation;
|
|
37
|
+
|
|
38
|
+
yield* expectMatch(/listening/, lines()(proc.stdout));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("starts the given child", function* () {
|
|
42
|
+
const response = yield* fetchText("http://localhost:29002", {
|
|
43
|
+
method: "POST",
|
|
44
|
+
body: "hello",
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(response.status).toEqual(200);
|
|
48
|
+
expect(response.text).toEqual("hello");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("halting the daemon task", () => {
|
|
52
|
+
beforeEach(function* () {
|
|
53
|
+
yield* until(task.halt());
|
|
54
|
+
});
|
|
55
|
+
it("kills the process", function* () {
|
|
56
|
+
expect(
|
|
57
|
+
yield* captureError(
|
|
58
|
+
fetchText("http://localhost:29002", {
|
|
59
|
+
method: "POST",
|
|
60
|
+
body: "hello",
|
|
61
|
+
}),
|
|
62
|
+
),
|
|
63
|
+
).toMatchObject({
|
|
64
|
+
message: expect.stringContaining("FetchError"),
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("shutting down the daemon process prematurely", () => {
|
|
71
|
+
let task: Task<Error>;
|
|
72
|
+
beforeEach(function* () {
|
|
73
|
+
let proc = yield* daemon("node", {
|
|
74
|
+
arguments: ["--experimental-strip-types", "fixtures/echo-server.ts"],
|
|
75
|
+
env: {
|
|
76
|
+
PORT: "29001",
|
|
77
|
+
PATH: process.env.PATH as string,
|
|
78
|
+
...(SystemRoot ? { SystemRoot } : {}),
|
|
79
|
+
},
|
|
80
|
+
cwd: import.meta.dirname,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
task = yield* spawn(function* () {
|
|
84
|
+
try {
|
|
85
|
+
yield* proc;
|
|
86
|
+
} catch (e) {
|
|
87
|
+
return e as Error;
|
|
88
|
+
}
|
|
89
|
+
return new Error(`this shouldn't happen`);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
yield* expectMatch(/listening/, lines()(proc.stdout));
|
|
93
|
+
|
|
94
|
+
yield* fetchText("http://localhost:29001", {
|
|
95
|
+
method: "POST",
|
|
96
|
+
body: "exit",
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("throw an error because it was not expected to close", function* () {
|
|
101
|
+
yield* until(
|
|
102
|
+
expect(task).resolves.toHaveProperty("name", "DaemonExitError"),
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { describe, it } from "@effectionx/bdd";
|
|
3
|
+
import { spawn, withResolvers } from "effection";
|
|
4
|
+
import { expect } from "expect";
|
|
5
|
+
|
|
6
|
+
import { once } from "@effectionx/node/events";
|
|
7
|
+
|
|
8
|
+
describe("once", () => {
|
|
9
|
+
it("resolves with single argument as array", function* () {
|
|
10
|
+
expect.assertions(1);
|
|
11
|
+
const emitter = new EventEmitter();
|
|
12
|
+
const { resolve, operation } = withResolvers<[string]>();
|
|
13
|
+
|
|
14
|
+
yield* spawn(function* () {
|
|
15
|
+
resolve(yield* once<[string]>(emitter, "test"));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
yield* spawn(function* () {
|
|
19
|
+
emitter.emit("test", "hello");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
expect(yield* operation).toEqual(["hello"]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("resolves with multiple arguments as array", function* () {
|
|
26
|
+
expect.assertions(1);
|
|
27
|
+
const emitter = new EventEmitter();
|
|
28
|
+
|
|
29
|
+
let { resolve, operation } = withResolvers<[number, string]>();
|
|
30
|
+
|
|
31
|
+
yield* spawn(function* () {
|
|
32
|
+
resolve(yield* once<[number, string]>(emitter, "exit"));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
yield* spawn(function* () {
|
|
36
|
+
emitter.emit("exit", 42, "SIGTERM");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(yield* operation).toEqual([42, "SIGTERM"]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("only resolves once even with multiple emissions", function* () {
|
|
43
|
+
const emitter = new EventEmitter();
|
|
44
|
+
|
|
45
|
+
const { resolve, operation } = withResolvers<void>();
|
|
46
|
+
let results: string[][] = [];
|
|
47
|
+
|
|
48
|
+
yield* spawn(function* () {
|
|
49
|
+
results.push(yield* once<[string]>(emitter, "data"));
|
|
50
|
+
resolve();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
yield* spawn(function* () {
|
|
54
|
+
emitter.emit("data", "first");
|
|
55
|
+
emitter.emit("data", "second");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
yield* operation;
|
|
59
|
+
|
|
60
|
+
expect(results).toEqual([["first"]]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("removes listener after resolving", function* () {
|
|
64
|
+
expect.assertions(2);
|
|
65
|
+
const emitter = new EventEmitter();
|
|
66
|
+
|
|
67
|
+
const { resolve, operation } = withResolvers<void>();
|
|
68
|
+
|
|
69
|
+
yield* spawn(function* () {
|
|
70
|
+
yield* once<[string]>(emitter, "test");
|
|
71
|
+
resolve();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
yield* spawn(function* () {
|
|
75
|
+
expect(emitter.listenerCount("test")).toBe(1);
|
|
76
|
+
emitter.emit("test", "hello");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
yield* operation;
|
|
80
|
+
expect(emitter.listenerCount("test")).toBe(0);
|
|
81
|
+
});
|
|
82
|
+
});
|