@effect/platform-node-shared 4.0.0-beta.1 → 4.0.0-beta.100

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 (70) hide show
  1. package/README.md +3 -3
  2. package/dist/NodeChildProcessSpawner.d.ts +19 -5
  3. package/dist/NodeChildProcessSpawner.d.ts.map +1 -1
  4. package/dist/NodeChildProcessSpawner.js +105 -118
  5. package/dist/NodeChildProcessSpawner.js.map +1 -1
  6. package/dist/NodeClusterSocket.d.ts +10 -4
  7. package/dist/NodeClusterSocket.d.ts.map +1 -1
  8. package/dist/NodeClusterSocket.js +22 -8
  9. package/dist/NodeClusterSocket.js.map +1 -1
  10. package/dist/NodeCrypto.d.ts +27 -0
  11. package/dist/NodeCrypto.d.ts.map +1 -0
  12. package/dist/NodeCrypto.js +55 -0
  13. package/dist/NodeCrypto.js.map +1 -0
  14. package/dist/NodeFileSystem.d.ts +5 -2
  15. package/dist/NodeFileSystem.d.ts.map +1 -1
  16. package/dist/NodeFileSystem.js +44 -28
  17. package/dist/NodeFileSystem.js.map +1 -1
  18. package/dist/NodePath.d.ts +15 -6
  19. package/dist/NodePath.d.ts.map +1 -1
  20. package/dist/NodePath.js +23 -7
  21. package/dist/NodePath.js.map +1 -1
  22. package/dist/NodeRuntime.d.ts +26 -7
  23. package/dist/NodeRuntime.d.ts.map +1 -1
  24. package/dist/NodeRuntime.js +8 -8
  25. package/dist/NodeRuntime.js.map +1 -1
  26. package/dist/NodeSink.d.ts +33 -4
  27. package/dist/NodeSink.d.ts.map +1 -1
  28. package/dist/NodeSink.js +31 -4
  29. package/dist/NodeSink.js.map +1 -1
  30. package/dist/NodeSocket.d.ts +43 -12
  31. package/dist/NodeSocket.d.ts.map +1 -1
  32. package/dist/NodeSocket.js +41 -17
  33. package/dist/NodeSocket.js.map +1 -1
  34. package/dist/NodeSocketServer.d.ts +25 -8
  35. package/dist/NodeSocketServer.d.ts.map +1 -1
  36. package/dist/NodeSocketServer.js +29 -12
  37. package/dist/NodeSocketServer.js.map +1 -1
  38. package/dist/NodeStdio.d.ts +6 -5
  39. package/dist/NodeStdio.d.ts.map +1 -1
  40. package/dist/NodeStdio.js +26 -10
  41. package/dist/NodeStdio.js.map +1 -1
  42. package/dist/NodeStream.d.ts +76 -20
  43. package/dist/NodeStream.d.ts.map +1 -1
  44. package/dist/NodeStream.js +74 -25
  45. package/dist/NodeStream.js.map +1 -1
  46. package/dist/NodeTerminal.d.ts +9 -2
  47. package/dist/NodeTerminal.d.ts.map +1 -1
  48. package/dist/NodeTerminal.js +13 -3
  49. package/dist/NodeTerminal.js.map +1 -1
  50. package/dist/index.d.ts +52 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +53 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/internal/utils.js +1 -1
  55. package/dist/internal/utils.js.map +1 -1
  56. package/package.json +9 -9
  57. package/src/NodeChildProcessSpawner.ts +157 -148
  58. package/src/NodeClusterSocket.ts +22 -8
  59. package/src/NodeCrypto.ts +60 -0
  60. package/src/NodeFileSystem.ts +58 -30
  61. package/src/NodePath.ts +23 -7
  62. package/src/NodeRuntime.ts +28 -13
  63. package/src/NodeSink.ts +40 -4
  64. package/src/NodeSocket.ts +51 -20
  65. package/src/NodeSocketServer.ts +39 -13
  66. package/src/NodeStdio.ts +42 -24
  67. package/src/NodeStream.ts +97 -33
  68. package/src/NodeTerminal.ts +22 -4
  69. package/src/index.ts +65 -0
  70. package/src/internal/utils.ts +4 -4
@@ -1,5 +1,19 @@
1
1
  /**
2
- * Node.js implementation of `ChildProcessSpawner`.
2
+ * Shared Node.js implementation of the child process spawner service.
3
+ *
4
+ * This module adapts `node:child_process.spawn` to the Effect
5
+ * `ChildProcessSpawner` service. Provide {@link layer} to run `ChildProcess`
6
+ * commands in Node-compatible runtimes: commands get scoped process handles
7
+ * with stdin sinks, stdout and stderr streams, exit-code waiting,
8
+ * interruption-time cleanup, process killing, and custom file-descriptor pipes.
9
+ *
10
+ * The implementation sits below the command-building API. It validates and
11
+ * resolves `cwd` through the Effect `FileSystem` and `Path` services,
12
+ * translates Node errno failures to `PlatformError`, and uses scopes to
13
+ * terminate referenced children when the owning effect is interrupted or
14
+ * finalized. Pipelines are flattened by {@link flattenCommand} and spawned one
15
+ * process at a time, wiring the selected source stream (`stdout`, `stderr`,
16
+ * `all`, or `fdN`) to the destination `stdin` or `fdN`.
3
17
  *
4
18
  * @since 4.0.0
5
19
  */
@@ -17,9 +31,15 @@ import * as Sink from "effect/Sink"
17
31
  import * as Stream from "effect/Stream"
18
32
  import * as ChildProcess from "effect/unstable/process/ChildProcess"
19
33
  import type { ChildProcessHandle } from "effect/unstable/process/ChildProcessSpawner"
20
- import { ChildProcessSpawner, ExitCode, makeHandle, ProcessId } from "effect/unstable/process/ChildProcessSpawner"
34
+ import {
35
+ ChildProcessSpawner,
36
+ ExitCode,
37
+ make as makeSpawner,
38
+ makeHandle,
39
+ ProcessId
40
+ } from "effect/unstable/process/ChildProcessSpawner"
21
41
  import * as NodeChildProcess from "node:child_process"
22
- import * as NodeJSStream from "node:stream"
42
+ import { PassThrough } from "node:stream"
23
43
  import { handleErrnoException } from "./internal/utils.ts"
24
44
  import * as NodeSink from "./NodeSink.ts"
25
45
  import * as NodeStream from "./NodeStream.ts"
@@ -181,15 +201,15 @@ const make = Effect.gen(function*() {
181
201
  case "input": {
182
202
  // Create a sink to write to for input file descriptors
183
203
  let sink: Sink.Sink<void, Uint8Array, never, PlatformError.PlatformError> = Sink.drain
184
- if (Predicate.isNotNullish(nodeStream) && "write" in nodeStream) {
204
+ if (nodeStream && "write" in nodeStream) {
185
205
  sink = NodeSink.fromWritable({
186
- evaluate: () => nodeStream as NodeJS.WritableStream,
206
+ evaluate: () => nodeStream,
187
207
  onError: (error) => toPlatformError(`fromWritable(fd${fd})`, toError(error), command)
188
208
  })
189
209
  }
190
210
 
191
211
  // If user provided a stream, pipe it into the sink
192
- if (Predicate.isNotUndefined(config.stream)) {
212
+ if (config.stream) {
193
213
  yield* Effect.forkScoped(Stream.run(config.stream, sink))
194
214
  }
195
215
 
@@ -200,15 +220,18 @@ const make = Effect.gen(function*() {
200
220
  case "output": {
201
221
  // Create a stream to read from for output file descriptors
202
222
  let stream: Stream.Stream<Uint8Array, PlatformError.PlatformError> = Stream.empty
203
- if (Predicate.isNotNull(nodeStream) && Predicate.isNotUndefined(nodeStream) && "read" in nodeStream) {
223
+ if (nodeStream && "read" in nodeStream) {
224
+ const passThrough = new PassThrough()
225
+ nodeStream.on("error", (error) => passThrough.destroy(error))
226
+ nodeStream.pipe(passThrough)
204
227
  stream = NodeStream.fromReadable({
205
- evaluate: () => nodeStream as NodeJS.ReadableStream,
228
+ evaluate: () => passThrough,
206
229
  onError: (error) => toPlatformError(`fromReadable(fd${fd})`, toError(error), command)
207
230
  })
208
231
  }
209
232
 
210
233
  // If user provided a sink, transduce the stream through it
211
- if (Predicate.isNotUndefined(config.sink)) {
234
+ if (config.sink) {
212
235
  stream = Stream.transduce(stream, config.sink)
213
236
  }
214
237
 
@@ -251,35 +274,6 @@ const make = Effect.gen(function*() {
251
274
  return Effect.succeed(sink)
252
275
  })
253
276
 
254
- /**
255
- * Given that `NodeStream.fromReadable` uses `.read` to read data from the
256
- * provided `Readable` stream, consumers would race to read data from the
257
- * `handle.stdout` and `handle.stderr` streams if they were also simultaneously
258
- * reading from the `handle.all` stream.
259
- *
260
- * To solve this, we leverage the fact that NodeJS `Readable` streams can be
261
- * piped to multiple destinations simultaneously. The logic for the solution
262
- * is as follows:
263
- *
264
- * 1. Pipe each original stream to two `PassThrough` streams:
265
- * - One dedicated PassThrough for individual access (.stdout / .stderr)
266
- * - One shared PassThrough for combined access (.all)
267
- * 2. Create Effect streams from the PassThrough streams (not the originals)
268
- *
269
- * **Diagram**
270
- *
271
- * ┌─────────────┐
272
- * ┌────►│ passthrough │────► Effect stdout Stream
273
- * │ └─────────────┘
274
- * childProcess.stdout ────┤
275
- * │ ┌─────────────┐
276
- * └────►│ passthrough │────► Effect all Stream
277
- * ┌────►│ │
278
- * childProcess.stderr ────┤ └─────────────┘
279
- * │ ┌─────────────┐
280
- * └────►│ passthrough │────► Effect stderr Stream
281
- * └─────────────┘
282
- */
283
277
  const setupChildOutputStreams = (
284
278
  command: ChildProcess.StandardCommand,
285
279
  childProcess: NodeChildProcess.ChildProcess,
@@ -290,85 +284,37 @@ const make = Effect.gen(function*() {
290
284
  stderr: Stream.Stream<Uint8Array, PlatformError.PlatformError>
291
285
  all: Stream.Stream<Uint8Array, PlatformError.PlatformError>
292
286
  } => {
293
- const nodeStdout = childProcess.stdout
294
- const nodeStderr = childProcess.stderr
295
-
296
- // Create PassThrough streams for individual access to stdout and stderr.
297
- // We pipe the original Node.js streams to these PassThroughs so that
298
- // the data can be consumed by both the individual streams AND the
299
- // combined stream (.all) simultaneously.
300
- const stdoutPassThrough = Predicate.isNotNull(nodeStdout)
301
- ? new NodeJSStream.PassThrough()
302
- : null
303
- const stderrPassThrough = Predicate.isNotNull(nodeStderr)
304
- ? new NodeJSStream.PassThrough()
305
- : null
306
-
307
- // Create PassThrough for combined output (.all)
308
- const combinedPassThrough = new NodeJSStream.PassThrough()
309
-
310
- // Track stream endings for the combined stream
311
- const totalStreams = (Predicate.isNotNull(nodeStdout) ? 1 : 0) +
312
- (Predicate.isNotNull(nodeStderr) ? 1 : 0)
313
-
314
- let endedCount = 0
315
- const onStreamEnd = () => {
316
- endedCount++
317
- if (endedCount >= totalStreams) {
318
- combinedPassThrough.end()
319
- }
320
- }
321
-
322
- // Pipe stdout to both its own PassThrough and the combined PassThrough
323
- if (Predicate.isNotNull(nodeStdout) && Predicate.isNotNull(stdoutPassThrough)) {
324
- nodeStdout.pipe(stdoutPassThrough)
325
- nodeStdout.pipe(combinedPassThrough, { end: false })
326
- nodeStdout.once("end", onStreamEnd)
327
- }
328
-
329
- // Pipe stderr to both its own PassThrough and the combined PassThrough
330
- if (Predicate.isNotNull(nodeStderr) && Predicate.isNotNull(stderrPassThrough)) {
331
- nodeStderr.pipe(stderrPassThrough)
332
- nodeStderr.pipe(combinedPassThrough, { end: false })
333
- nodeStderr.once("end", onStreamEnd)
334
- }
335
-
336
- // Handle edge case: no streams available
337
- if (totalStreams === 0) {
338
- combinedPassThrough.end()
339
- }
287
+ let stdout = childProcess.stdout ?
288
+ (() => {
289
+ const passThrough = new PassThrough()
290
+ childProcess.stdout!.on("error", (error) => passThrough.destroy(error))
291
+ childProcess.stdout!.pipe(passThrough)
292
+ return NodeStream.fromReadable({
293
+ evaluate: () => passThrough,
294
+ onError: (error) => toPlatformError("fromReadable(stdout)", toError(error), command)
295
+ })
296
+ })() :
297
+ Stream.empty
298
+ let stderr = childProcess.stderr ?
299
+ (() => {
300
+ const passThrough = new PassThrough()
301
+ childProcess.stderr!.on("error", (error) => passThrough.destroy(error))
302
+ childProcess.stderr!.pipe(passThrough)
303
+ return NodeStream.fromReadable({
304
+ evaluate: () => passThrough,
305
+ onError: (error) => toPlatformError("fromReadable(stderr)", toError(error), command)
306
+ })
307
+ })() :
308
+ Stream.empty
340
309
 
341
- // Create Effect stream for stdout from its PassThrough
342
- let stdout: Stream.Stream<Uint8Array, PlatformError.PlatformError> = Stream.empty
343
- if (Predicate.isNotNull(stdoutPassThrough)) {
344
- stdout = NodeStream.fromReadable({
345
- evaluate: () => stdoutPassThrough,
346
- onError: (error) => toPlatformError("fromReadable(stdout)", toError(error), command)
347
- })
348
- // Apply user-provided Sink if configured
349
- if (Sink.isSink(stdoutConfig.stream)) {
350
- stdout = Stream.transduce(stdout, stdoutConfig.stream)
351
- }
310
+ if (Sink.isSink(stdoutConfig.stream)) {
311
+ stdout = Stream.transduce(stdout, stdoutConfig.stream)
352
312
  }
353
-
354
- // Create Effect stream for stderr from its PassThrough
355
- let stderr: Stream.Stream<Uint8Array, PlatformError.PlatformError> = Stream.empty
356
- if (Predicate.isNotNull(stderrPassThrough)) {
357
- stderr = NodeStream.fromReadable({
358
- evaluate: () => stderrPassThrough,
359
- onError: (error) => toPlatformError("fromReadable(stderr)", toError(error), command)
360
- })
361
- // Apply user-provided Sink if configured
362
- if (Sink.isSink(stderrConfig.stream)) {
363
- stderr = Stream.transduce(stderr, stderrConfig.stream)
364
- }
313
+ if (Sink.isSink(stderrConfig.stream)) {
314
+ stderr = Stream.transduce(stderr, stderrConfig.stream)
365
315
  }
366
316
 
367
- // Create Effect stream for combined output from the combined PassThrough
368
- const all: Stream.Stream<Uint8Array, PlatformError.PlatformError> = NodeStream.fromReadable({
369
- evaluate: () => combinedPassThrough,
370
- onError: (error) => toPlatformError("fromReadable(all)", toError(error), command)
371
- })
317
+ const all = Stream.merge(stdout, stderr)
372
318
 
373
319
  return { stdout, stderr, all }
374
320
  }
@@ -425,6 +371,23 @@ const make = Effect.gen(function*() {
425
371
  })
426
372
  }
427
373
 
374
+ const killProcessGroupOnExit = (
375
+ childProcess: NodeChildProcess.ChildProcess,
376
+ signal: NodeJS.Signals
377
+ ): void => {
378
+ if (globalThis.process.platform === "win32") {
379
+ NodeChildProcess.exec(`taskkill /pid ${childProcess.pid} /T /F`, () => {
380
+ // ignore errors during best-effort cleanup
381
+ })
382
+ return
383
+ }
384
+ try {
385
+ globalThis.process.kill(-childProcess.pid!, signal)
386
+ } catch {
387
+ // ignore errors during best-effort cleanup
388
+ }
389
+ }
390
+
428
391
  const killProcess = (
429
392
  command: ChildProcess.StandardCommand,
430
393
  childProcess: NodeChildProcess.ChildProcess,
@@ -456,7 +419,7 @@ const make = Effect.gen(function*() {
456
419
  ? kill(command, childProcess, killSignal)
457
420
  : Effect.timeoutOrElse(kill(command, childProcess, killSignal), {
458
421
  duration: options.forceKillAfter,
459
- onTimeout: () => kill(command, childProcess, "SIGKILL")
422
+ orElse: () => kill(command, childProcess, "SIGKILL")
460
423
  })
461
424
  }
462
425
 
@@ -501,6 +464,7 @@ const make = Effect.gen(function*() {
501
464
  const stdoutConfig = resolveOutputOption(cmd.options, "stdout")
502
465
  const stderrConfig = resolveOutputOption(cmd.options, "stderr")
503
466
  const resolvedAdditionalFds = resolveAdditionalFds(cmd.options)
467
+ let isReferenced = true
504
468
 
505
469
  const cwd = yield* resolveWorkingDirectory(cmd.options)
506
470
  const env = resolveEnvironment(cmd.options)
@@ -526,20 +490,40 @@ const make = Effect.gen(function*() {
526
490
  }
527
491
  return yield* Effect.void
528
492
  }
493
+ if (!isReferenced) {
494
+ return yield* Effect.void
495
+ }
529
496
  // Process is still running, kill it
530
497
  return yield* killWithTimeout((command, childProcess, signal) =>
531
- Effect.catch(
532
- killProcessGroup(command, childProcess, signal),
533
- () => killProcess(command, childProcess, signal)
498
+ killProcessGroup(command, childProcess, signal).pipe(
499
+ Effect.catch(() => killProcess(command, childProcess, signal)),
500
+ Effect.andThen(Deferred.await(exitSignal))
534
501
  )
535
502
  ).pipe(
536
- Effect.andThen(Deferred.await(exitSignal)),
537
503
  Effect.ignore
538
504
  )
539
505
  })
540
506
  )
541
507
 
542
508
  const pid = ProcessId(childProcess.pid!)
509
+ childProcess.on("exit", (code) => {
510
+ if (code !== 0 && Predicate.isNotNull(code)) {
511
+ killProcessGroupOnExit(childProcess, cmd.options.killSignal ?? "SIGTERM")
512
+ }
513
+ })
514
+ const reref = Effect.sync(() => {
515
+ if (!isReferenced) {
516
+ childProcess.ref()
517
+ isReferenced = true
518
+ }
519
+ })
520
+ const unref = Effect.sync(() => {
521
+ if (isReferenced) {
522
+ childProcess.unref()
523
+ isReferenced = false
524
+ }
525
+ return reref
526
+ })
543
527
  const stdin = yield* setupChildStdin(cmd, childProcess, stdinConfig)
544
528
  const { all, stderr, stdout } = setupChildOutputStreams(cmd, childProcess, stdoutConfig, stderrConfig)
545
529
  const { getInputFd, getOutputFd } = yield* setupAdditionalFds(cmd, childProcess, resolvedAdditionalFds)
@@ -557,12 +541,11 @@ const make = Effect.gen(function*() {
557
541
  const kill = (options?: ChildProcess.KillOptions | undefined) => {
558
542
  const killWithTimeout = withTimeout(childProcess, cmd, options)
559
543
  return killWithTimeout((command, childProcess, signal) =>
560
- Effect.catch(
561
- killProcessGroup(command, childProcess, signal),
562
- () => killProcess(command, childProcess, signal)
544
+ killProcessGroup(command, childProcess, signal).pipe(
545
+ Effect.catch(() => killProcess(command, childProcess, signal)),
546
+ Effect.andThen(Deferred.await(exitSignal))
563
547
  )
564
548
  ).pipe(
565
- Effect.andThen(Deferred.await(exitSignal)),
566
549
  Effect.asVoid
567
550
  )
568
551
  }
@@ -577,14 +560,15 @@ const make = Effect.gen(function*() {
577
560
  stderr,
578
561
  all,
579
562
  getInputFd,
580
- getOutputFd
563
+ getOutputFd,
564
+ unref
581
565
  })
582
566
  }
583
567
  case "PipedCommand": {
584
568
  const { commands, pipeOptions } = flattenCommand(cmd)
585
569
  const [root, ...pipeline] = commands
586
570
 
587
- let handle = spawnCommand(root)
571
+ const handles = [yield* spawnCommand(root)]
588
572
 
589
573
  for (let i = 0; i < pipeline.length; i++) {
590
574
  const command = pipeline[i]
@@ -593,7 +577,7 @@ const make = Effect.gen(function*() {
593
577
 
594
578
  // Get the appropriate stream from the source based on `from` option
595
579
  const sourceStream = Stream.unwrap(
596
- Effect.map(handle, (h) => getSourceStream(h, options.from))
580
+ Effect.succeed(getSourceStream(handles[handles.length - 1], options.from))
597
581
  )
598
582
 
599
583
  // Determine where to pipe: stdin or custom fd
@@ -601,48 +585,73 @@ const make = Effect.gen(function*() {
601
585
 
602
586
  if (toOption === "stdin") {
603
587
  // Pipe to stdin (default behavior)
604
- handle = spawnCommand(ChildProcess.make(command.command, command.args, {
605
- ...command.options,
606
- stdin: { ...stdinConfig, stream: sourceStream }
607
- }))
588
+ handles.push(
589
+ yield* spawnCommand(ChildProcess.make(command.command, command.args, {
590
+ ...command.options,
591
+ stdin: { ...stdinConfig, stream: sourceStream }
592
+ }))
593
+ )
608
594
  } else {
609
595
  // Pipe to custom fd (fd3, fd4, etc.)
610
596
  const fd = ChildProcess.parseFdName(toOption)
611
597
  if (Predicate.isNotUndefined(fd)) {
612
598
  const fdName = ChildProcess.fdName(fd) as `fd${number}`
613
599
  const existingFds = command.options.additionalFds ?? {}
614
- handle = spawnCommand(ChildProcess.make(command.command, command.args, {
615
- ...command.options,
616
- additionalFds: {
617
- ...existingFds,
618
- [fdName]: { type: "input" as const, stream: sourceStream }
619
- }
620
- }))
600
+ handles.push(
601
+ yield* spawnCommand(ChildProcess.make(command.command, command.args, {
602
+ ...command.options,
603
+ additionalFds: {
604
+ ...existingFds,
605
+ [fdName]: { type: "input" as const, stream: sourceStream }
606
+ }
607
+ }))
608
+ )
621
609
  } else {
622
610
  // Invalid fd name, fall back to stdin
623
- handle = spawnCommand(ChildProcess.make(command.command, command.args, {
624
- ...command.options,
625
- stdin: { ...stdinConfig, stream: sourceStream }
626
- }))
611
+ handles.push(
612
+ yield* spawnCommand(ChildProcess.make(command.command, command.args, {
613
+ ...command.options,
614
+ stdin: { ...stdinConfig, stream: sourceStream }
615
+ }))
616
+ )
627
617
  }
628
618
  }
629
619
  }
630
620
 
631
- return yield* handle
621
+ const handle = handles[handles.length - 1]
622
+ const unref = Effect.gen(function*() {
623
+ const rerefs: Array<Effect.Effect<void, PlatformError.PlatformError>> = []
624
+ for (const handle of handles) {
625
+ rerefs.push(yield* handle.unref)
626
+ }
627
+ return Effect.forEach([...rerefs].reverse(), (reref) => reref, { discard: true })
628
+ })
629
+
630
+ return makeHandle({
631
+ pid: handle.pid,
632
+ exitCode: handle.exitCode,
633
+ isRunning: handle.isRunning,
634
+ kill: handle.kill,
635
+ stdin: handle.stdin,
636
+ stdout: handle.stdout,
637
+ stderr: handle.stderr,
638
+ all: handle.all,
639
+ getInputFd: handle.getInputFd,
640
+ getOutputFd: handle.getOutputFd,
641
+ unref
642
+ })
632
643
  }
633
644
  }
634
645
  })
635
646
 
636
- return ChildProcessSpawner.of({
637
- spawn: spawnCommand
638
- })
647
+ return makeSpawner(spawnCommand)
639
648
  })
640
649
 
641
650
  /**
642
- * Layer providing the `NodeChildProcessSpawner` implementation.
651
+ * Layer that provides the `NodeChildProcessSpawner` implementation.
643
652
  *
653
+ * @category layers
644
654
  * @since 4.0.0
645
- * @category Layers
646
655
  */
647
656
  export const layer: Layer.Layer<
648
657
  ChildProcessSpawner,
@@ -657,8 +666,8 @@ export const layer: Layer.Layer<
657
666
  /**
658
667
  * Result of flattening a pipeline of commands.
659
668
  *
669
+ * @category models
660
670
  * @since 4.0.0
661
- * @category Models
662
671
  */
663
672
  export interface FlattenedPipeline {
664
673
  readonly commands: Arr.NonEmptyReadonlyArray<ChildProcess.StandardCommand>
@@ -669,8 +678,8 @@ export interface FlattenedPipeline {
669
678
  * Flattens a `Command` into an array of `StandardCommand`s along with pipe
670
679
  * options for each connection.
671
680
  *
681
+ * @category transforming
672
682
  * @since 4.0.0
673
- * @category Utilities
674
683
  */
675
684
  export const flattenCommand = (
676
685
  command: ChildProcess.Command
@@ -1,8 +1,16 @@
1
1
  /**
2
- * @since 1.0.0
2
+ * Node TCP socket transport for Effect Cluster runner-to-runner RPC.
3
+ *
4
+ * This module provides the shared Node layers used by socket-based cluster
5
+ * transports. `layerClientProtocol` opens TCP sockets to peer runner addresses
6
+ * and wraps them in the current RPC serialization protocol. `layerSocketServer`
7
+ * exposes the socket server that receives incoming runner RPC traffic.
8
+ *
9
+ * @since 4.0.0
3
10
  */
4
11
  import * as Effect from "effect/Effect"
5
12
  import * as Layer from "effect/Layer"
13
+ import * as Option from "effect/Option"
6
14
  import * as Runners from "effect/unstable/cluster/Runners"
7
15
  import * as ShardingConfig from "effect/unstable/cluster/ShardingConfig"
8
16
  import * as RpcClient from "effect/unstable/rpc/RpcClient"
@@ -13,8 +21,11 @@ import * as NodeSocket from "./NodeSocket.ts"
13
21
  import * as NodeSocketServer from "./NodeSocketServer.ts"
14
22
 
15
23
  /**
16
- * @since 1.0.0
17
- * @category Layers
24
+ * Provides the cluster `RpcClientProtocol` by opening TCP sockets to runner
25
+ * addresses and using the current RPC serialization service.
26
+ *
27
+ * @category layers
28
+ * @since 4.0.0
18
29
  */
19
30
  export const layerClientProtocol: Layer.Layer<
20
31
  Runners.RpcClientProtocol,
@@ -39,8 +50,11 @@ export const layerClientProtocol: Layer.Layer<
39
50
  )
40
51
 
41
52
  /**
42
- * @since 1.0.0
43
- * @category Layers
53
+ * Provides the socket server used by cluster runners, listening on
54
+ * `ShardingConfig.runnerListenAddress` or `runnerAddress`.
55
+ *
56
+ * @category layers
57
+ * @since 4.0.0
44
58
  */
45
59
  export const layerSocketServer: Layer.Layer<
46
60
  SocketServer.SocketServer,
@@ -48,9 +62,9 @@ export const layerSocketServer: Layer.Layer<
48
62
  ShardingConfig.ShardingConfig
49
63
  > = Effect.gen(function*() {
50
64
  const config = yield* ShardingConfig.ShardingConfig
51
- const listenAddress = config.runnerListenAddress ?? config.runnerAddress
52
- if (listenAddress === undefined) {
65
+ const listenAddress = Option.orElse(config.runnerListenAddress, () => config.runnerAddress)
66
+ if (Option.isNone(listenAddress)) {
53
67
  return yield* Effect.die("layerSocketServer: ShardingConfig.runnerListenAddress is None")
54
68
  }
55
- return NodeSocketServer.layer(listenAddress)
69
+ return NodeSocketServer.layer(listenAddress.value)
56
70
  }).pipe(Layer.unwrap)
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Node-compatible implementation of Effect's `Crypto` service.
3
+ *
4
+ * This module builds the service from `node:crypto`, using `randomBytes` for
5
+ * random data and `createHash` for supported digest algorithms. It exports
6
+ * `make` as the concrete service value and `layer` for providing it through
7
+ * Effect context.
8
+ *
9
+ * @since 1.0.0
10
+ */
11
+ import * as EffectCrypto from "effect/Crypto"
12
+ import * as Effect from "effect/Effect"
13
+ import * as Layer from "effect/Layer"
14
+ import * as PlatformError from "effect/PlatformError"
15
+ import * as NodeCrypto from "node:crypto"
16
+
17
+ const toHashAlgorithm = (algorithm: EffectCrypto.DigestAlgorithm): string => {
18
+ switch (algorithm) {
19
+ case "SHA-1":
20
+ return "sha1"
21
+ case "SHA-256":
22
+ return "sha256"
23
+ case "SHA-384":
24
+ return "sha384"
25
+ case "SHA-512":
26
+ return "sha512"
27
+ }
28
+ }
29
+
30
+ const digest: EffectCrypto.Crypto["digest"] = (algorithm, data) =>
31
+ Effect.try({
32
+ try: () => Uint8Array.from(NodeCrypto.createHash(toHashAlgorithm(algorithm)).update(data).digest()),
33
+ catch: (cause) =>
34
+ PlatformError.systemError({
35
+ module: "Crypto",
36
+ method: "digest",
37
+ _tag: "Unknown",
38
+ description: "Could not compute digest",
39
+ cause
40
+ })
41
+ })
42
+
43
+ /**
44
+ * The default Node.js Crypto service implementation.
45
+ *
46
+ * @category constructors
47
+ * @since 1.0.0
48
+ */
49
+ export const make: EffectCrypto.Crypto = EffectCrypto.make({
50
+ randomBytes: NodeCrypto.randomBytes,
51
+ digest
52
+ })
53
+
54
+ /**
55
+ * Layer that provides the Node.js Crypto service implementation.
56
+ *
57
+ * @category layers
58
+ * @since 1.0.0
59
+ */
60
+ export const layer: Layer.Layer<EffectCrypto.Crypto> = Layer.succeed(EffectCrypto.Crypto, make)