opencode-effect-enforcer 0.2.8 → 0.3.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/README.md +3 -3
- package/docs/effect-4.0.0-changelog.md +3213 -0
- package/docs/effect-4.0.0.md +110 -0
- package/guidance/effect-first-development.md +8 -6
- package/guidance/progressive-disclosure-guidance.md +15 -7
- package/package.json +2 -2
- package/patterns/avoid-any.md +2 -2
- package/patterns/avoid-direct-json.md +6 -6
- package/patterns/avoid-native-fetch.md +8 -6
- package/patterns/avoid-node-imports.md +2 -2
- package/patterns/avoid-non-null-assertion.md +2 -2
- package/patterns/avoid-object-type.md +2 -2
- package/patterns/avoid-platform-coupling.md +1 -1
- package/patterns/avoid-process-env.md +3 -4
- package/patterns/avoid-ts-ignore.md +1 -1
- package/patterns/context-tag-extends.md +11 -8
- package/patterns/effect-promise-vs-trypromise.md +6 -7
- package/patterns/prefer-arr-sort.md +1 -1
- package/patterns/prefer-effect-fn.md +21 -65
- package/patterns/prefer-schema-class.md +3 -3
- package/patterns/throw-in-effect-gen.md +1 -1
- package/patterns/use-clock-service.md +4 -0
- package/patterns/use-command-executor-service.md +2 -2
- package/patterns/use-http-client-service.md +8 -6
- package/patterns/use-random-service.md +6 -7
- package/skills/effect-ai-chat/SKILL.md +13 -7
- package/skills/effect-ai-language-model/SKILL.md +50 -21
- package/skills/effect-ai-prompt/SKILL.md +25 -14
- package/skills/effect-ai-provider/SKILL.md +50 -22
- package/skills/effect-ai-streaming/SKILL.md +27 -12
- package/skills/effect-ai-tool/SKILL.md +37 -28
- package/skills/effect-atom-rpc/SKILL.md +57 -36
- package/skills/effect-atom-state/SKILL.md +57 -19
- package/skills/effect-batching/SKILL.md +5 -3
- package/skills/effect-cache/SKILL.md +19 -7
- package/skills/effect-cli/SKILL.md +17 -8
- package/skills/effect-command-executor/SKILL.md +115 -64
- package/skills/effect-concurrency-testing/SKILL.md +26 -6
- package/skills/effect-config/SKILL.md +53 -2
- package/skills/effect-context-witness/SKILL.md +6 -6
- package/skills/effect-domain-modeling/SKILL.md +8 -1
- package/skills/effect-error-handling/SKILL.md +15 -2
- package/skills/effect-fiber/SKILL.md +20 -25
- package/skills/effect-filesystem/SKILL.md +69 -57
- package/skills/effect-http-api/SKILL.md +72 -22
- package/skills/effect-http-client/SKILL.md +25 -21
- package/skills/effect-http-server/SKILL.md +51 -21
- package/skills/effect-incremental-migration/SKILL.md +17 -8
- package/skills/effect-layer-design/SKILL.md +8 -0
- package/skills/effect-managed-runtime/SKILL.md +6 -0
- package/skills/effect-mcp-server/SKILL.md +64 -24
- package/skills/effect-observability/SKILL.md +61 -15
- package/skills/effect-parallelization/SKILL.md +24 -7
- package/skills/effect-path/SKILL.md +8 -2
- package/skills/effect-platform-abstraction/SKILL.md +88 -66
- package/skills/effect-platform-layers/SKILL.md +68 -67
- package/skills/effect-pubsub-event-bus/SKILL.md +56 -60
- package/skills/effect-react-composition/SKILL.md +19 -6
- package/skills/effect-rpc-api/SKILL.md +24 -24
- package/skills/effect-rpc-client/SKILL.md +33 -28
- package/skills/effect-rpc-cluster/SKILL.md +122 -78
- package/skills/effect-rpc-server/SKILL.md +56 -20
- package/skills/effect-scheduling/SKILL.md +29 -1
- package/skills/effect-schema-composition/SKILL.md +31 -13
- package/skills/effect-schema-v4/SKILL.md +94 -10
- package/skills/effect-scope/SKILL.md +13 -5
- package/skills/effect-service-implementation/SKILL.md +1 -1
- package/skills/effect-socket/SKILL.md +52 -8
- package/skills/effect-sql/SKILL.md +67 -33
- package/skills/effect-stream/SKILL.md +50 -5
- package/skills/effect-testing/SKILL.md +91 -2
- package/skills/effect-workflow/SKILL.md +76 -39
|
@@ -5,13 +5,16 @@ description: Spawn and manage child processes using Effect's ChildProcess module
|
|
|
5
5
|
|
|
6
6
|
# Child Process Execution with Effect v4
|
|
7
7
|
|
|
8
|
+
Targets **Effect 4.0.0**, checked against the `effect@4.0.0` source tag. Keep platform packages on the same version. The process API is marked `@stability unstable`, so check contracts again before a minor-version upgrade.
|
|
9
|
+
|
|
8
10
|
## Overview
|
|
9
11
|
|
|
10
|
-
Node process-group cleanup waits for the leader and descendants. Without
|
|
12
|
+
On POSIX, Node process-group cleanup waits for the leader and descendants. Without
|
|
11
13
|
`forceKillAfter`, cleanup waits up to one second without escalating. With it,
|
|
12
14
|
the group receives SIGKILL at the deadline, then a final bounded wait. Native
|
|
13
15
|
timers drive escalation even under TestClock. `exitCode` / `isRunning` describe
|
|
14
16
|
the leader, not all descendants; do not use virtual time alone to prove OS cleanup.
|
|
17
|
+
On Windows, Node uses `taskkill` for tree termination and waits for the leader.
|
|
15
18
|
|
|
16
19
|
The `ChildProcess` module provides type-safe, composable process execution with automatic resource cleanup via `Scope`. Commands are AST values — built first with `make` and `pipeTo`, then executed via the `ChildProcessSpawner` service.
|
|
17
20
|
|
|
@@ -23,12 +26,12 @@ The `ChildProcess` module provides type-safe, composable process execution with
|
|
|
23
26
|
- Streaming output from long-running processes
|
|
24
27
|
- Managing process lifecycles with scoped cleanup
|
|
25
28
|
|
|
26
|
-
**Note:** This skill covers child process execution
|
|
29
|
+
**Note:** This skill covers child process execution; use `effect/cli` for building CLI applications.
|
|
27
30
|
|
|
28
31
|
## Import Pattern
|
|
29
32
|
|
|
30
33
|
```typescript
|
|
31
|
-
import { ChildProcess, ChildProcessSpawner } from 'effect/
|
|
34
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/process';
|
|
32
35
|
```
|
|
33
36
|
|
|
34
37
|
Platform layer (Node.js):
|
|
@@ -41,8 +44,9 @@ import { NodeServices } from '@effect/platform-node';
|
|
|
41
44
|
|
|
42
45
|
### Template Literal Form
|
|
43
46
|
|
|
47
|
+
<!-- typecheck -->
|
|
44
48
|
```typescript
|
|
45
|
-
import { ChildProcess } from 'effect/
|
|
49
|
+
import { ChildProcess } from 'effect/process';
|
|
46
50
|
|
|
47
51
|
// Simple command — parsed by whitespace
|
|
48
52
|
const cmd = ChildProcess.make`echo hello world`;
|
|
@@ -58,8 +62,9 @@ const cmd3 = ChildProcess.make`oxfmt ${files}`;
|
|
|
58
62
|
|
|
59
63
|
### Options + Template Literal Form
|
|
60
64
|
|
|
65
|
+
<!-- typecheck -->
|
|
61
66
|
```typescript
|
|
62
|
-
import { ChildProcess } from 'effect/
|
|
67
|
+
import { ChildProcess } from 'effect/process';
|
|
63
68
|
|
|
64
69
|
// Options object returns a tagged template function
|
|
65
70
|
const cmd = ChildProcess.make({ cwd: '/tmp' })`ls -la`;
|
|
@@ -73,8 +78,9 @@ const cmd2 = ChildProcess.make({
|
|
|
73
78
|
|
|
74
79
|
### Array Form
|
|
75
80
|
|
|
81
|
+
<!-- typecheck -->
|
|
76
82
|
```typescript
|
|
77
|
-
import { ChildProcess } from 'effect/
|
|
83
|
+
import { ChildProcess } from 'effect/process';
|
|
78
84
|
|
|
79
85
|
// Explicit command + args array
|
|
80
86
|
const cmd = ChildProcess.make('git', ['status'], {
|
|
@@ -99,8 +105,9 @@ const cmd3 = ChildProcess.make('node', {
|
|
|
99
105
|
|
|
100
106
|
### Command Options
|
|
101
107
|
|
|
108
|
+
<!-- typecheck -->
|
|
102
109
|
```typescript
|
|
103
|
-
import { ChildProcess } from 'effect/
|
|
110
|
+
import { ChildProcess } from 'effect/process';
|
|
104
111
|
|
|
105
112
|
const cmd = ChildProcess.make('node', ['script.js'], {
|
|
106
113
|
// Working directory
|
|
@@ -141,8 +148,9 @@ The `fd3`/`fd4` names configure child-process stdio channels. Process handles ex
|
|
|
141
148
|
|
|
142
149
|
### Combinators
|
|
143
150
|
|
|
151
|
+
<!-- typecheck -->
|
|
144
152
|
```typescript
|
|
145
|
-
import { ChildProcess } from 'effect/
|
|
153
|
+
import { ChildProcess } from 'effect/process';
|
|
146
154
|
|
|
147
155
|
// Set cwd (applies to all commands in a pipeline)
|
|
148
156
|
const cmd = ChildProcess.make`ls -la`.pipe(ChildProcess.setCwd('/tmp'));
|
|
@@ -165,7 +173,7 @@ Commands are `Effect` values: `yield*` on a command evaluates through its `Effec
|
|
|
165
173
|
|
|
166
174
|
```typescript
|
|
167
175
|
import { Effect } from 'effect';
|
|
168
|
-
import { ChildProcessSpawner } from 'effect/
|
|
176
|
+
import { ChildProcessSpawner } from 'effect/process';
|
|
169
177
|
|
|
170
178
|
const program = Effect.gen(function* () {
|
|
171
179
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
@@ -175,9 +183,10 @@ const program = Effect.gen(function* () {
|
|
|
175
183
|
|
|
176
184
|
### Capture as String
|
|
177
185
|
|
|
186
|
+
<!-- typecheck -->
|
|
178
187
|
```typescript
|
|
179
188
|
import { Effect, String } from 'effect';
|
|
180
|
-
import { ChildProcess, ChildProcessSpawner } from 'effect/
|
|
189
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/process';
|
|
181
190
|
|
|
182
191
|
const program = Effect.gen(function* () {
|
|
183
192
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
@@ -190,39 +199,45 @@ const program = Effect.gen(function* () {
|
|
|
190
199
|
|
|
191
200
|
### Capture as Lines
|
|
192
201
|
|
|
202
|
+
<!-- typecheck -->
|
|
193
203
|
```typescript
|
|
194
204
|
import { Effect } from 'effect';
|
|
195
|
-
import
|
|
205
|
+
import * as Arr from 'effect/Array';
|
|
206
|
+
import * as Str from 'effect/String';
|
|
207
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/process';
|
|
196
208
|
|
|
197
209
|
const program = Effect.gen(function* () {
|
|
198
210
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
199
211
|
const files = yield* spawner.lines(
|
|
200
212
|
ChildProcess.make('git', ['diff', '--name-only', 'main...HEAD'])
|
|
201
213
|
);
|
|
202
|
-
const tsFiles =
|
|
214
|
+
const tsFiles = Arr.filter(files, Str.endsWith('.ts'));
|
|
203
215
|
});
|
|
204
216
|
```
|
|
205
217
|
|
|
206
218
|
### Progressive Output with Side Effects
|
|
207
219
|
|
|
220
|
+
<!-- typecheck -->
|
|
208
221
|
```typescript
|
|
209
222
|
import { Effect, Stream } from 'effect';
|
|
210
|
-
import { ChildProcess, ChildProcessSpawner } from 'effect/
|
|
223
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/process';
|
|
211
224
|
|
|
212
225
|
const program = Effect.gen(function* () {
|
|
213
226
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
214
|
-
let output = '';
|
|
215
227
|
const handle = yield* spawner.spawn(
|
|
216
228
|
ChildProcess.make('bun', ['test'], {
|
|
217
229
|
extendEnv: true,
|
|
218
230
|
stdin: 'ignore'
|
|
219
231
|
})
|
|
220
232
|
);
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
233
|
+
// Drain to EOF before leaving the scope, propagating read/progress failures.
|
|
234
|
+
const output = yield* handle.all.pipe(
|
|
235
|
+
Stream.decodeText(),
|
|
236
|
+
Stream.tap((chunk) => Effect.logDebug('Process output').pipe(
|
|
237
|
+
Effect.annotateLogs({ characters: chunk.length })
|
|
238
|
+
)),
|
|
239
|
+
Stream.mkString
|
|
240
|
+
);
|
|
226
241
|
const exitCode = yield* handle.exitCode;
|
|
227
242
|
return { output, exitCode };
|
|
228
243
|
}).pipe(Effect.scoped);
|
|
@@ -230,29 +245,41 @@ const program = Effect.gen(function* () {
|
|
|
230
245
|
|
|
231
246
|
Use this pattern when you need per-chunk side effects (progress reporting, streaming to UI) during command execution. `spawner.string`/`spawner.lines` cannot provide per-chunk callbacks.
|
|
232
247
|
|
|
248
|
+
Waiting for `handle.exitCode` alone does not drain output. If you fork a reader, join it before closing the scope and returning captured output. `Stream.mkString` buffers all text; for unbounded output, use `Stream.runForEach` without retaining it. Treat process output as potentially sensitive when choosing a progress callback.
|
|
249
|
+
|
|
233
250
|
### Get Exit Code
|
|
234
251
|
|
|
252
|
+
<!-- typecheck -->
|
|
235
253
|
```typescript
|
|
236
254
|
import { Effect } from 'effect';
|
|
237
|
-
import
|
|
255
|
+
import * as Schema from 'effect/Schema';
|
|
256
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/process';
|
|
257
|
+
|
|
258
|
+
class CommandFailed extends Schema.TaggedError<CommandFailed>()('CommandFailed', {
|
|
259
|
+
exitCode: Schema.Number
|
|
260
|
+
}) {}
|
|
238
261
|
|
|
239
262
|
const program = Effect.gen(function* () {
|
|
240
263
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
241
264
|
const exitCode = yield* spawner.exitCode(
|
|
242
|
-
ChildProcess.make('test', ['-f', 'package.json']
|
|
265
|
+
ChildProcess.make('test', ['-f', 'package.json'], {
|
|
266
|
+
stdin: 'ignore', stdout: 'ignore', stderr: 'ignore'
|
|
267
|
+
})
|
|
243
268
|
);
|
|
244
269
|
// exitCode: ExitCode (branded number)
|
|
245
270
|
if (exitCode !== ChildProcessSpawner.ExitCode(0)) {
|
|
246
|
-
yield* Effect.fail(new
|
|
271
|
+
return yield* Effect.fail(new CommandFailed({ exitCode }));
|
|
247
272
|
}
|
|
248
273
|
});
|
|
249
274
|
```
|
|
250
275
|
|
|
276
|
+
`string`, `lines`, and stream helpers collect output; they do **not** reject a nonzero exit code. Use a scoped handle when success requires both captured output and a checked status. When using `exitCode` alone, inherit or ignore stdout/stderr so an unread pipe cannot fill and stall a noisy process. Output helpers default to stdout; pass `{ includeStderr: true }` to consume combined output, or configure stderr as `inherit`/`ignore` when only stdout is needed.
|
|
277
|
+
|
|
251
278
|
### Stream Output (Long-Running Processes)
|
|
252
279
|
|
|
253
280
|
```typescript
|
|
254
281
|
import { Console, Effect, Stream } from 'effect';
|
|
255
|
-
import { ChildProcess, ChildProcessSpawner } from 'effect/
|
|
282
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/process';
|
|
256
283
|
|
|
257
284
|
const program = Effect.gen(function* () {
|
|
258
285
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
@@ -273,9 +300,15 @@ const program = Effect.gen(function* () {
|
|
|
273
300
|
|
|
274
301
|
Use `spawner.spawn` when you need the process handle for interactive control, streaming output while running, or checking exit codes.
|
|
275
302
|
|
|
303
|
+
<!-- typecheck -->
|
|
276
304
|
```typescript
|
|
277
305
|
import { Console, Effect, Stream } from 'effect';
|
|
278
|
-
import
|
|
306
|
+
import * as Schema from 'effect/Schema';
|
|
307
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/process';
|
|
308
|
+
|
|
309
|
+
class LintFailed extends Schema.TaggedError<LintFailed>()('LintFailed', {
|
|
310
|
+
exitCode: Schema.Number
|
|
311
|
+
}) {}
|
|
279
312
|
|
|
280
313
|
const program = Effect.gen(function* () {
|
|
281
314
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
@@ -298,7 +331,7 @@ const program = Effect.gen(function* () {
|
|
|
298
331
|
// Wait for exit
|
|
299
332
|
const exitCode = yield* handle.exitCode;
|
|
300
333
|
if (exitCode !== ChildProcessSpawner.ExitCode(0)) {
|
|
301
|
-
yield* Effect.fail(new
|
|
334
|
+
return yield* Effect.fail(new LintFailed({ exitCode }));
|
|
302
335
|
}
|
|
303
336
|
}).pipe(Effect.scoped); // <-- spawn requires Scope
|
|
304
337
|
```
|
|
@@ -310,7 +343,7 @@ const program = Effect.gen(function* () {
|
|
|
310
343
|
| `pid` | `ProcessId` | Process identifier (branded number) |
|
|
311
344
|
| `exitCode` | `Effect<ExitCode, PlatformError>` | Waits for exit, returns exit code |
|
|
312
345
|
| `isRunning` | `Effect<boolean, PlatformError>` | Check if still running |
|
|
313
|
-
| `kill(options?)` | `Effect<void, PlatformError>` | Kill with signal;
|
|
346
|
+
| `kill(options?)` | `Effect<void, PlatformError>` | Kill with signal; `forceKillAfter` bounds graceful waiting before escalation |
|
|
314
347
|
| `stdin` | `Sink<void, Uint8Array, never, PlatformError>` | Write to process stdin |
|
|
315
348
|
| `stdout` | `Stream<Uint8Array, PlatformError>` | Read process stdout |
|
|
316
349
|
| `stderr` | `Stream<Uint8Array, PlatformError>` | Read process stderr |
|
|
@@ -320,9 +353,10 @@ const program = Effect.gen(function* () {
|
|
|
320
353
|
|
|
321
354
|
## Piping Commands
|
|
322
355
|
|
|
356
|
+
<!-- typecheck -->
|
|
323
357
|
```typescript
|
|
324
358
|
import { Effect } from 'effect';
|
|
325
|
-
import { ChildProcess, ChildProcessSpawner } from 'effect/
|
|
359
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/process';
|
|
326
360
|
|
|
327
361
|
const program = Effect.gen(function* () {
|
|
328
362
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
@@ -371,12 +405,18 @@ const program = Effect.gen(function* () {
|
|
|
371
405
|
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer));
|
|
372
406
|
```
|
|
373
407
|
|
|
374
|
-
Or
|
|
408
|
+
Or compose the spawner with its FileSystem and Path dependencies:
|
|
375
409
|
|
|
376
410
|
```typescript
|
|
377
|
-
import { NodeChildProcessSpawner } from '@effect/platform-node';
|
|
411
|
+
import { NodeChildProcessSpawner, NodeFileSystem, NodePath } from '@effect/platform-node';
|
|
412
|
+
import { Effect, Layer } from 'effect';
|
|
413
|
+
import { ChildProcessSpawner } from 'effect/process';
|
|
378
414
|
|
|
379
|
-
const
|
|
415
|
+
declare const myEffect: Effect.Effect<void, never, ChildProcessSpawner.ChildProcessSpawner>;
|
|
416
|
+
const SpawnerLayer = NodeChildProcessSpawner.layer.pipe(
|
|
417
|
+
Layer.provide(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer))
|
|
418
|
+
);
|
|
419
|
+
const program = myEffect.pipe(Effect.provide(SpawnerLayer));
|
|
380
420
|
```
|
|
381
421
|
|
|
382
422
|
## Complete Example: DevTools Service
|
|
@@ -392,7 +432,8 @@ import {
|
|
|
392
432
|
Stream,
|
|
393
433
|
String
|
|
394
434
|
} from 'effect';
|
|
395
|
-
import
|
|
435
|
+
import * as Arr from 'effect/Array';
|
|
436
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/process';
|
|
396
437
|
|
|
397
438
|
class DevToolsError extends Schema.TaggedError<DevToolsError>()(
|
|
398
439
|
'DevToolsError',
|
|
@@ -442,7 +483,7 @@ class DevTools extends Context.Service<
|
|
|
442
483
|
.pipe(
|
|
443
484
|
Effect.mapError((cause) => new DevToolsError({ cause }))
|
|
444
485
|
);
|
|
445
|
-
return
|
|
486
|
+
return Arr.filter(files, String.endsWith('.ts'));
|
|
446
487
|
});
|
|
447
488
|
|
|
448
489
|
const recentCommitSubjects = spawner
|
|
@@ -463,7 +504,7 @@ class DevTools extends Context.Service<
|
|
|
463
504
|
const runLintFix = Effect.gen(function* () {
|
|
464
505
|
const handle = yield* spawner
|
|
465
506
|
.spawn(
|
|
466
|
-
ChildProcess.make('bun', ['lint'], {
|
|
507
|
+
ChildProcess.make('bun', ['lint', '--fix'], {
|
|
467
508
|
env: { FORCE_COLOR: '1' },
|
|
468
509
|
extendEnv: true,
|
|
469
510
|
stdin: 'ignore'
|
|
@@ -484,11 +525,9 @@ class DevTools extends Context.Service<
|
|
|
484
525
|
Effect.mapError((cause) => new DevToolsError({ cause }))
|
|
485
526
|
);
|
|
486
527
|
if (exitCode !== ChildProcessSpawner.ExitCode(0)) {
|
|
487
|
-
return yield* new DevToolsError({
|
|
488
|
-
cause:
|
|
489
|
-
|
|
490
|
-
)
|
|
491
|
-
});
|
|
528
|
+
return yield* Effect.fail(new DevToolsError({
|
|
529
|
+
cause: { command: 'bun lint --fix', exitCode }
|
|
530
|
+
}));
|
|
492
531
|
}
|
|
493
532
|
}).pipe(Effect.scoped);
|
|
494
533
|
|
|
@@ -499,7 +538,9 @@ class DevTools extends Context.Service<
|
|
|
499
538
|
runLintFix
|
|
500
539
|
});
|
|
501
540
|
})
|
|
502
|
-
)
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
static readonly defaultLayer = DevTools.layer.pipe(Layer.provide(NodeServices.layer));
|
|
503
544
|
}
|
|
504
545
|
```
|
|
505
546
|
|
|
@@ -516,14 +557,14 @@ const program = Effect.gen(function* () {
|
|
|
516
557
|
}).pipe(Effect.scoped);
|
|
517
558
|
```
|
|
518
559
|
|
|
519
|
-
### DON'T:
|
|
560
|
+
### DON'T: Run a scoped program without providing its Scope
|
|
520
561
|
|
|
521
562
|
```typescript
|
|
522
|
-
//
|
|
563
|
+
// This definition is valid and retains Scope in its requirements.
|
|
523
564
|
const program = Effect.gen(function* () {
|
|
524
565
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
525
566
|
const handle = yield* spawner.spawn(ChildProcess.make`my-server`);
|
|
526
|
-
//
|
|
567
|
+
// The caller must own/provide Scope before running this program.
|
|
527
568
|
});
|
|
528
569
|
```
|
|
529
570
|
|
|
@@ -571,22 +612,15 @@ class MyService extends Context.Service<
|
|
|
571
612
|
>()('MyService') {}
|
|
572
613
|
```
|
|
573
614
|
|
|
574
|
-
### DO: Check
|
|
615
|
+
### DO: Check the exit status explicitly
|
|
575
616
|
|
|
576
617
|
```typescript
|
|
577
618
|
if (exitCode !== ChildProcessSpawner.ExitCode(0)) {
|
|
578
|
-
yield* Effect.fail(new
|
|
619
|
+
return yield* Effect.fail(new CommandFailed({ exitCode }));
|
|
579
620
|
}
|
|
580
621
|
```
|
|
581
622
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
```typescript
|
|
585
|
-
// ❌ ExitCode is a branded type — direct number comparison may not work as expected
|
|
586
|
-
if (exitCode !== 0) {
|
|
587
|
-
/* ... */
|
|
588
|
-
}
|
|
589
|
-
```
|
|
623
|
+
`ExitCode` is a branded **number**, not a boxed runtime value. Both `exitCode !== 0` and comparison with `ChildProcessSpawner.ExitCode(0)` work. The constructor is useful when supplying an ExitCode to a typed API; the brand does not change numeric comparison semantics.
|
|
590
624
|
|
|
591
625
|
### DO: Use `handle.all` for interleaved stdout+stderr
|
|
592
626
|
|
|
@@ -610,9 +644,10 @@ yield* Stream.merge(handle.stdout, handle.all).pipe(Stream.runCollect);
|
|
|
610
644
|
|
|
611
645
|
Child process operations fail with `PlatformError`:
|
|
612
646
|
|
|
647
|
+
<!-- typecheck -->
|
|
613
648
|
```typescript
|
|
614
649
|
import { Effect } from 'effect';
|
|
615
|
-
import { ChildProcess, ChildProcessSpawner } from 'effect/
|
|
650
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/process';
|
|
616
651
|
|
|
617
652
|
const program = Effect.gen(function* () {
|
|
618
653
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
@@ -621,7 +656,9 @@ const program = Effect.gen(function* () {
|
|
|
621
656
|
.string(ChildProcess.make`non-existent-command`)
|
|
622
657
|
.pipe(
|
|
623
658
|
Effect.catchTag('PlatformError', (error) =>
|
|
624
|
-
|
|
659
|
+
error.reason._tag === 'NotFound'
|
|
660
|
+
? Effect.succeed('Optional command is unavailable')
|
|
661
|
+
: Effect.fail(error)
|
|
625
662
|
)
|
|
626
663
|
);
|
|
627
664
|
});
|
|
@@ -637,6 +674,7 @@ Keep process kill, timeout, and output-drain logic inside the process adapter th
|
|
|
637
674
|
|
|
638
675
|
Use `Effect.callback` to convert an `AbortSignal` (or any event-driven API) into an Effect. The cleanup Effect removes the listener, and the already-aborted edge case is handled first:
|
|
639
676
|
|
|
677
|
+
<!-- typecheck -->
|
|
640
678
|
```typescript
|
|
641
679
|
import { Effect } from 'effect';
|
|
642
680
|
|
|
@@ -651,16 +689,25 @@ const fromAbortSignal = (signal: AbortSignal) =>
|
|
|
651
689
|
|
|
652
690
|
### Abort / Timeout Multiplexing
|
|
653
691
|
|
|
654
|
-
Combine `Effect.raceAll` with discriminated result types to handle exit, abort, and timeout in a single expression:
|
|
692
|
+
Combine `Effect.raceAll` with discriminated result types to handle successful exit, abort, and timeout in a single expression. `raceAll` waits for the first success, so turn exit observation into an `Exit` value to preserve a failed exit-code read rather than hiding it behind the timeout:
|
|
655
693
|
|
|
694
|
+
<!-- typecheck -->
|
|
656
695
|
```typescript
|
|
657
|
-
import { Effect } from 'effect';
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
696
|
+
import { Duration, Effect } from 'effect';
|
|
697
|
+
import { ChildProcessSpawner } from 'effect/process';
|
|
698
|
+
|
|
699
|
+
declare const fromAbortSignal: (signal: AbortSignal) => Effect.Effect<void>;
|
|
700
|
+
|
|
701
|
+
// Call within the scope that owns the handle and its output reader.
|
|
702
|
+
const awaitOutcome = Effect.fn('Process.awaitOutcome')(function* (
|
|
703
|
+
handle: ChildProcessSpawner.ChildProcessHandle,
|
|
704
|
+
signal: AbortSignal,
|
|
705
|
+
timeout: Duration.Input
|
|
706
|
+
) {
|
|
707
|
+
const exit = yield* Effect.raceAll([
|
|
662
708
|
handle.exitCode.pipe(
|
|
663
|
-
Effect.
|
|
709
|
+
Effect.exit,
|
|
710
|
+
Effect.map((result) => ({ kind: 'exit' as const, result }))
|
|
664
711
|
),
|
|
665
712
|
fromAbortSignal(signal).pipe(
|
|
666
713
|
Effect.map(() => ({ kind: 'abort' as const }))
|
|
@@ -669,11 +716,15 @@ const exit =
|
|
|
669
716
|
Effect.map(() => ({ kind: 'timeout' as const }))
|
|
670
717
|
)
|
|
671
718
|
]);
|
|
672
|
-
if (exit.kind !== 'exit') {
|
|
673
|
-
|
|
674
|
-
}
|
|
719
|
+
if (exit.kind !== 'exit') {
|
|
720
|
+
yield* handle.kill({ forceKillAfter: '3 seconds' });
|
|
721
|
+
}
|
|
722
|
+
return exit;
|
|
723
|
+
});
|
|
675
724
|
```
|
|
676
725
|
|
|
726
|
+
Handle `exit.result` with `Exit.match` when `kind === 'exit'`; it includes observation failures. This race only chooses the control outcome: drain/join any output reader separately within the owning scope before returning captured output.
|
|
727
|
+
|
|
677
728
|
## Related Skills
|
|
678
729
|
|
|
679
730
|
- **effect-platform-abstraction**: FileSystem, Path, and other platform services
|
|
@@ -16,12 +16,15 @@ This skill provides patterns for testing Effect's concurrency primitives: fibers
|
|
|
16
16
|
| Simple fiber yield | `Effect.yieldNow` |
|
|
17
17
|
| Wait for subscriber ready | `Deferred.make()` + `Deferred.await` |
|
|
18
18
|
| Wait for stream element | `Latch.make()` + `Stream.tap(() => latch.open)` |
|
|
19
|
-
| Passive subscription registration |
|
|
19
|
+
| Passive subscription registration | acquire `PubSub.subscribe` before publishing, or expose a readiness signal |
|
|
20
20
|
| Time-dependent behavior | `TestClock.adjust` |
|
|
21
21
|
| Verify events published | `PubSub.subscribe` + `PubSub.takeUpTo` |
|
|
22
22
|
| Check fiber status | `fiber.pollUnsafe()` |
|
|
23
23
|
|
|
24
|
-
For `Stream.fromPubSub`
|
|
24
|
+
For `Stream.fromPubSub` registration, signal readiness after acquiring the actual
|
|
25
|
+
subscription, not merely before starting the stream. A scheduler yield is not a
|
|
26
|
+
general readiness guarantee; a sleep under `it.effect` also needs `TestClock`
|
|
27
|
+
advancement. Prefer a subscription-first test seam to arbitrary delays.
|
|
25
28
|
|
|
26
29
|
## Fiber Coordination Patterns
|
|
27
30
|
|
|
@@ -39,7 +42,7 @@ it.effect('fiber polling with yieldNow', () =>
|
|
|
39
42
|
|
|
40
43
|
const fiber = yield* latch.await.pipe(Effect.forkChild);
|
|
41
44
|
|
|
42
|
-
yield* Effect.yieldNow
|
|
45
|
+
yield* Effect.yieldNow;
|
|
43
46
|
|
|
44
47
|
expect(fiber.pollUnsafe()).toBeUndefined();
|
|
45
48
|
|
|
@@ -67,7 +70,7 @@ it.effect('latch coordination', () =>
|
|
|
67
70
|
return 'completed';
|
|
68
71
|
}).pipe(Effect.forkChild);
|
|
69
72
|
|
|
70
|
-
yield* Effect.yieldNow
|
|
73
|
+
yield* Effect.yieldNow;
|
|
71
74
|
expect(fiber.pollUnsafe()).toBeUndefined();
|
|
72
75
|
|
|
73
76
|
yield* latch.open;
|
|
@@ -436,6 +439,23 @@ it.effect('should run finalizers', () =>
|
|
|
436
439
|
|
|
437
440
|
## Interruption Testing
|
|
438
441
|
|
|
442
|
+
Test cancellation at the resource-owning boundary, using a `Deferred` to prove
|
|
443
|
+
work has started before interrupting it:
|
|
444
|
+
|
|
445
|
+
- Shared `ScopedCache` lookup: cancel one of two waiters and let the other
|
|
446
|
+
finish; then cancel the last waiter on another pending lookup and verify its
|
|
447
|
+
resource finalizer ran and a later `get` reacquires.
|
|
448
|
+
- `race` / `raceFirst` / iterable variants: assert loser cleanup is complete when
|
|
449
|
+
the race returns, including cancellation while contenders are starting.
|
|
450
|
+
- `Scope.close`: gate a finalizer, interrupt the closing fiber, release the gate,
|
|
451
|
+
and verify remaining finalizers still run.
|
|
452
|
+
- `ManagedRuntime.dispose`: verify request cleanup can use layer services before
|
|
453
|
+
those services are released.
|
|
454
|
+
- Queue batches: offer fewer than `takeN` requests, verify the taker remains
|
|
455
|
+
pending, then `end` or `fail` and assert the short final batch precedes the
|
|
456
|
+
terminal outcome. For PubSub, test the `end` value with `take`, not `takeUpTo`,
|
|
457
|
+
including a late subscriber and a full dropping buffer.
|
|
458
|
+
|
|
439
459
|
### Testing Fiber Interruption
|
|
440
460
|
|
|
441
461
|
```typescript
|
|
@@ -518,7 +538,7 @@ Effect.gen(function* () {
|
|
|
518
538
|
// GOOD - Use yieldNow for simple yielding
|
|
519
539
|
Effect.gen(function* () {
|
|
520
540
|
const fiber = yield* someEffect.pipe(Effect.forkChild);
|
|
521
|
-
yield* Effect.yieldNow
|
|
541
|
+
yield* Effect.yieldNow;
|
|
522
542
|
yield* Fiber.join(fiber);
|
|
523
543
|
});
|
|
524
544
|
```
|
|
@@ -538,7 +558,7 @@ while (fiber.pollUnsafe() === undefined) {
|
|
|
538
558
|
// GOOD - Yield between polls or use Fiber.await
|
|
539
559
|
Effect.gen(function* () {
|
|
540
560
|
while (fiber.pollUnsafe() === undefined) {
|
|
541
|
-
yield* Effect.yieldNow
|
|
561
|
+
yield* Effect.yieldNow;
|
|
542
562
|
}
|
|
543
563
|
});
|
|
544
564
|
|
|
@@ -89,9 +89,36 @@ const host = Config.String('HOST').pipe(
|
|
|
89
89
|
);
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
+
The fallback result replaces the original failure. If the fallback is absent,
|
|
93
|
+
an outer `withDefault` or `option` can recover it even when the primary input
|
|
94
|
+
was invalid. If the fallback fails, its error propagates. `Config.fail(error)`
|
|
95
|
+
has type `Config<never>` and does not widen a composed result to `unknown`.
|
|
96
|
+
|
|
97
|
+
### `Config.flatMap` — Select a Dependent Config
|
|
98
|
+
|
|
99
|
+
Use `map` for a pure value transformation, `mapEffect` for a transformation that
|
|
100
|
+
may fail with `ConfigError`, and `flatMap` when the parsed value selects another
|
|
101
|
+
`Config`. The selected config uses the same provider and nesting prefix; the
|
|
102
|
+
callback runs only after successful resolution of the first config.
|
|
103
|
+
|
|
104
|
+
<!-- typecheck -->
|
|
105
|
+
```ts
|
|
106
|
+
import { Config, ConfigProvider } from 'effect';
|
|
107
|
+
|
|
108
|
+
const host = Config.Literals(['development', 'production'], 'MODE').pipe(
|
|
109
|
+
Config.flatMap((mode) => Config.NonEmptyString(
|
|
110
|
+
mode === 'development' ? 'DEV_HOST' : 'PROD_HOST'
|
|
111
|
+
)),
|
|
112
|
+
Config.nested('APP')
|
|
113
|
+
);
|
|
114
|
+
const parsed = host.parse(ConfigProvider.fromUnknown({
|
|
115
|
+
APP: { MODE: 'production', PROD_HOST: 'api.example.com' }
|
|
116
|
+
}));
|
|
117
|
+
```
|
|
118
|
+
|
|
92
119
|
### `Config.all` — Combine Multiple Configs
|
|
93
120
|
|
|
94
|
-
Accepts a record or
|
|
121
|
+
Accepts a record, tuple, or iterable of configs:
|
|
95
122
|
|
|
96
123
|
```ts
|
|
97
124
|
// As a record
|
|
@@ -105,6 +132,28 @@ const appConfig = Config.all({
|
|
|
105
132
|
const pair = Config.all([Config.String('a'), Config.Int('b')]);
|
|
106
133
|
```
|
|
107
134
|
|
|
135
|
+
A group is absent when any child is absent and no child fails, even if other
|
|
136
|
+
children have supplied values. `Config.option(group)` then returns `None`;
|
|
137
|
+
`Config.withDefault(group, fallback)` replaces the **whole group**. Put defaults
|
|
138
|
+
on individual children to retain supplied siblings. Validation and source
|
|
139
|
+
errors still propagate. Use `Config.schema(Schema.Struct(...))` when an existing
|
|
140
|
+
but incomplete object must fail validation rather than default.
|
|
141
|
+
|
|
142
|
+
<!-- typecheck -->
|
|
143
|
+
```ts
|
|
144
|
+
import { Config, ConfigProvider, Effect } from 'effect';
|
|
145
|
+
|
|
146
|
+
const group = Config.all({
|
|
147
|
+
host: Config.String('HOST'),
|
|
148
|
+
port: Config.Int('PORT')
|
|
149
|
+
}).pipe(Config.withDefault({ host: 'localhost', port: 3000 }));
|
|
150
|
+
|
|
151
|
+
const result = Effect.runSync(group.parse(
|
|
152
|
+
ConfigProvider.fromUnknown({ HOST: 'supplied.example.com' })
|
|
153
|
+
));
|
|
154
|
+
// { host: 'localhost', port: 3000 } — the supplied host is replaced too.
|
|
155
|
+
```
|
|
156
|
+
|
|
108
157
|
### `Config.nested` — Scope Under a Prefix
|
|
109
158
|
|
|
110
159
|
Prepends a path segment to every key the inner config reads. With environment variables, nesting uses `_` as separator.
|
|
@@ -346,7 +395,9 @@ const defaults = ConfigProvider.fromUnknown({
|
|
|
346
395
|
const combined = ConfigProvider.orElse(envProvider, defaults);
|
|
347
396
|
```
|
|
348
397
|
|
|
349
|
-
At the `Config` level, `Config.orElse`
|
|
398
|
+
At the `Config` level, `Config.orElse` handles missing data and all `ConfigError`s;
|
|
399
|
+
its fallback determines whether an outer default or option can recover. Provider
|
|
400
|
+
fallback only chooses a source and does not perform that error recovery.
|
|
350
401
|
|
|
351
402
|
### `ConfigProvider.nested` — Prefix All Lookups
|
|
352
403
|
|
|
@@ -36,7 +36,7 @@ export const PaymentIntent = Schema.Struct({
|
|
|
36
36
|
Field **removed from schema**, only injected in code:
|
|
37
37
|
|
|
38
38
|
```typescript
|
|
39
|
-
import { Schema, Context, Effect
|
|
39
|
+
import { Schema, Context, Effect } from 'effect';
|
|
40
40
|
|
|
41
41
|
declare const generateId: () => string;
|
|
42
42
|
|
|
@@ -56,7 +56,7 @@ const createPaymentIntent = (amount: bigint) =>
|
|
|
56
56
|
|
|
57
57
|
// Use serial in business logic, logging, etc.
|
|
58
58
|
// but it's not part of the persisted data
|
|
59
|
-
yield*
|
|
59
|
+
yield* Effect.logInfo(`Creating payment intent ${serial}`);
|
|
60
60
|
|
|
61
61
|
return PaymentIntent.make({ id: generateId(), amount });
|
|
62
62
|
});
|
|
@@ -177,7 +177,7 @@ Good fits:
|
|
|
177
177
|
Witnesses are trivial to provide:
|
|
178
178
|
|
|
179
179
|
```typescript
|
|
180
|
-
import { Effect } from 'effect';
|
|
180
|
+
import { Context, Effect } from 'effect';
|
|
181
181
|
|
|
182
182
|
declare const myProgram: Effect.Effect<unknown, never, Serial>;
|
|
183
183
|
declare class Serial extends Context.Service<Serial, string>()('Serial') {}
|
|
@@ -188,7 +188,7 @@ const test = myProgram.pipe(Effect.provideService(Serial, 'test-serial-123'));
|
|
|
188
188
|
Capabilities need implementation:
|
|
189
189
|
|
|
190
190
|
```typescript
|
|
191
|
-
import { Effect } from 'effect';
|
|
191
|
+
import { Context, Effect } from 'effect';
|
|
192
192
|
|
|
193
193
|
declare const myProgram: Effect.Effect<unknown, never, SerialService>;
|
|
194
194
|
declare class SerialService extends Context.Service<
|
|
@@ -217,7 +217,7 @@ const test = myProgram.pipe(
|
|
|
217
217
|
- **Yes** → Keep in schema
|
|
218
218
|
|
|
219
219
|
```typescript
|
|
220
|
-
import { Schema, Context, Effect,
|
|
220
|
+
import { Schema, Context, Effect, Clock } from 'effect';
|
|
221
221
|
|
|
222
222
|
declare const LineItem: Schema.Schema<any>;
|
|
223
223
|
declare const generateId: () => string;
|
|
@@ -245,7 +245,7 @@ const createOrder = (items: Array<Schema.Schema.Type<typeof LineItem>>) =>
|
|
|
245
245
|
const requestId = yield* RequestId; // For logging
|
|
246
246
|
const timestamp = yield* Clock.currentTimeMillis; // For timestamp
|
|
247
247
|
|
|
248
|
-
yield*
|
|
248
|
+
yield* Effect.logInfo({
|
|
249
249
|
message: 'Creating order',
|
|
250
250
|
correlationId, // Used for tracing
|
|
251
251
|
requestId, // Used for logging
|