@effect/platform-node-shared 4.0.0-beta.7 → 4.0.0-beta.70

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 (66) hide show
  1. package/dist/NodeChildProcessSpawner.d.ts +3 -3
  2. package/dist/NodeChildProcessSpawner.d.ts.map +1 -1
  3. package/dist/NodeChildProcessSpawner.js +76 -17
  4. package/dist/NodeChildProcessSpawner.js.map +1 -1
  5. package/dist/NodeClusterSocket.d.ts +10 -4
  6. package/dist/NodeClusterSocket.d.ts.map +1 -1
  7. package/dist/NodeClusterSocket.js +33 -8
  8. package/dist/NodeClusterSocket.js.map +1 -1
  9. package/dist/NodeCrypto.d.ts +22 -0
  10. package/dist/NodeCrypto.d.ts.map +1 -0
  11. package/dist/NodeCrypto.js +50 -0
  12. package/dist/NodeCrypto.js.map +1 -0
  13. package/dist/NodeFileSystem.d.ts +5 -2
  14. package/dist/NodeFileSystem.d.ts.map +1 -1
  15. package/dist/NodeFileSystem.js +43 -26
  16. package/dist/NodeFileSystem.js.map +1 -1
  17. package/dist/NodePath.d.ts +15 -6
  18. package/dist/NodePath.d.ts.map +1 -1
  19. package/dist/NodePath.js +30 -7
  20. package/dist/NodePath.js.map +1 -1
  21. package/dist/NodeRuntime.d.ts +36 -7
  22. package/dist/NodeRuntime.d.ts.map +1 -1
  23. package/dist/NodeRuntime.js +8 -8
  24. package/dist/NodeRuntime.js.map +1 -1
  25. package/dist/NodeSink.d.ts +34 -4
  26. package/dist/NodeSink.d.ts.map +1 -1
  27. package/dist/NodeSink.js +23 -4
  28. package/dist/NodeSink.js.map +1 -1
  29. package/dist/NodeSocket.d.ts +40 -9
  30. package/dist/NodeSocket.d.ts.map +1 -1
  31. package/dist/NodeSocket.js +31 -15
  32. package/dist/NodeSocket.js.map +1 -1
  33. package/dist/NodeSocketServer.d.ts +24 -7
  34. package/dist/NodeSocketServer.d.ts.map +1 -1
  35. package/dist/NodeSocketServer.js +28 -11
  36. package/dist/NodeSocketServer.js.map +1 -1
  37. package/dist/NodeStdio.d.ts +6 -5
  38. package/dist/NodeStdio.d.ts.map +1 -1
  39. package/dist/NodeStdio.js +31 -7
  40. package/dist/NodeStdio.js.map +1 -1
  41. package/dist/NodeStream.d.ts +84 -20
  42. package/dist/NodeStream.d.ts.map +1 -1
  43. package/dist/NodeStream.js +76 -23
  44. package/dist/NodeStream.js.map +1 -1
  45. package/dist/NodeTerminal.d.ts +9 -2
  46. package/dist/NodeTerminal.d.ts.map +1 -1
  47. package/dist/NodeTerminal.js +13 -3
  48. package/dist/NodeTerminal.js.map +1 -1
  49. package/dist/index.d.ts +220 -0
  50. package/dist/index.d.ts.map +1 -0
  51. package/dist/index.js +221 -0
  52. package/dist/index.js.map +1 -0
  53. package/package.json +7 -6
  54. package/src/NodeChildProcessSpawner.ts +101 -27
  55. package/src/NodeClusterSocket.ts +33 -8
  56. package/src/NodeCrypto.ts +55 -0
  57. package/src/NodeFileSystem.ts +50 -28
  58. package/src/NodePath.ts +30 -7
  59. package/src/NodeRuntime.ts +38 -13
  60. package/src/NodeSink.ts +41 -4
  61. package/src/NodeSocket.ts +46 -16
  62. package/src/NodeSocketServer.ts +46 -12
  63. package/src/NodeStdio.ts +49 -23
  64. package/src/NodeStream.ts +97 -30
  65. package/src/NodeTerminal.ts +31 -4
  66. package/src/index.ts +233 -0
@@ -17,7 +17,13 @@ import * as Sink from "effect/Sink"
17
17
  import * as Stream from "effect/Stream"
18
18
  import * as ChildProcess from "effect/unstable/process/ChildProcess"
19
19
  import type { ChildProcessHandle } from "effect/unstable/process/ChildProcessSpawner"
20
- import { ChildProcessSpawner, ExitCode, makeHandle, ProcessId } from "effect/unstable/process/ChildProcessSpawner"
20
+ import {
21
+ ChildProcessSpawner,
22
+ ExitCode,
23
+ make as makeSpawner,
24
+ makeHandle,
25
+ ProcessId
26
+ } from "effect/unstable/process/ChildProcessSpawner"
21
27
  import * as NodeChildProcess from "node:child_process"
22
28
  import { handleErrnoException } from "./internal/utils.ts"
23
29
  import * as NodeSink from "./NodeSink.ts"
@@ -337,6 +343,23 @@ const make = Effect.gen(function*() {
337
343
  })
338
344
  }
339
345
 
346
+ const killProcessGroupOnExit = (
347
+ childProcess: NodeChildProcess.ChildProcess,
348
+ signal: NodeJS.Signals
349
+ ): void => {
350
+ if (globalThis.process.platform === "win32") {
351
+ NodeChildProcess.exec(`taskkill /pid ${childProcess.pid} /T /F`, () => {
352
+ // ignore errors during best-effort cleanup
353
+ })
354
+ return
355
+ }
356
+ try {
357
+ globalThis.process.kill(-childProcess.pid!, signal)
358
+ } catch {
359
+ // ignore errors during best-effort cleanup
360
+ }
361
+ }
362
+
340
363
  const killProcess = (
341
364
  command: ChildProcess.StandardCommand,
342
365
  childProcess: NodeChildProcess.ChildProcess,
@@ -368,7 +391,7 @@ const make = Effect.gen(function*() {
368
391
  ? kill(command, childProcess, killSignal)
369
392
  : Effect.timeoutOrElse(kill(command, childProcess, killSignal), {
370
393
  duration: options.forceKillAfter,
371
- onTimeout: () => kill(command, childProcess, "SIGKILL")
394
+ orElse: () => kill(command, childProcess, "SIGKILL")
372
395
  })
373
396
  }
374
397
 
@@ -413,6 +436,8 @@ const make = Effect.gen(function*() {
413
436
  const stdoutConfig = resolveOutputOption(cmd.options, "stdout")
414
437
  const stderrConfig = resolveOutputOption(cmd.options, "stderr")
415
438
  const resolvedAdditionalFds = resolveAdditionalFds(cmd.options)
439
+ let isReferenced = true
440
+ let cleanupOnNonZeroExit = false
416
441
 
417
442
  const cwd = yield* resolveWorkingDirectory(cmd.options)
418
443
  const env = resolveEnvironment(cmd.options)
@@ -438,6 +463,9 @@ const make = Effect.gen(function*() {
438
463
  }
439
464
  return yield* Effect.void
440
465
  }
466
+ if (!isReferenced) {
467
+ return yield* Effect.void
468
+ }
441
469
  // Process is still running, kill it
442
470
  return yield* killWithTimeout((command, childProcess, signal) =>
443
471
  Effect.catch(
@@ -452,6 +480,26 @@ const make = Effect.gen(function*() {
452
480
  )
453
481
 
454
482
  const pid = ProcessId(childProcess.pid!)
483
+ childProcess.on("exit", (code) => {
484
+ if (cleanupOnNonZeroExit && code !== 0 && Predicate.isNotNull(code)) {
485
+ killProcessGroupOnExit(childProcess, cmd.options.killSignal ?? "SIGTERM")
486
+ }
487
+ })
488
+ const reref = Effect.sync(() => {
489
+ if (!isReferenced) {
490
+ childProcess.ref()
491
+ isReferenced = true
492
+ cleanupOnNonZeroExit = false
493
+ }
494
+ })
495
+ const unref = Effect.sync(() => {
496
+ if (isReferenced) {
497
+ childProcess.unref()
498
+ isReferenced = false
499
+ cleanupOnNonZeroExit = true
500
+ }
501
+ return reref
502
+ })
455
503
  const stdin = yield* setupChildStdin(cmd, childProcess, stdinConfig)
456
504
  const { all, stderr, stdout } = setupChildOutputStreams(cmd, childProcess, stdoutConfig, stderrConfig)
457
505
  const { getInputFd, getOutputFd } = yield* setupAdditionalFds(cmd, childProcess, resolvedAdditionalFds)
@@ -489,14 +537,15 @@ const make = Effect.gen(function*() {
489
537
  stderr,
490
538
  all,
491
539
  getInputFd,
492
- getOutputFd
540
+ getOutputFd,
541
+ unref
493
542
  })
494
543
  }
495
544
  case "PipedCommand": {
496
545
  const { commands, pipeOptions } = flattenCommand(cmd)
497
546
  const [root, ...pipeline] = commands
498
547
 
499
- let handle = spawnCommand(root)
548
+ const handles = [yield* spawnCommand(root)]
500
549
 
501
550
  for (let i = 0; i < pipeline.length; i++) {
502
551
  const command = pipeline[i]
@@ -505,7 +554,7 @@ const make = Effect.gen(function*() {
505
554
 
506
555
  // Get the appropriate stream from the source based on `from` option
507
556
  const sourceStream = Stream.unwrap(
508
- Effect.map(handle, (h) => getSourceStream(h, options.from))
557
+ Effect.succeed(getSourceStream(handles[handles.length - 1], options.from))
509
558
  )
510
559
 
511
560
  // Determine where to pipe: stdin or custom fd
@@ -513,48 +562,73 @@ const make = Effect.gen(function*() {
513
562
 
514
563
  if (toOption === "stdin") {
515
564
  // Pipe to stdin (default behavior)
516
- handle = spawnCommand(ChildProcess.make(command.command, command.args, {
517
- ...command.options,
518
- stdin: { ...stdinConfig, stream: sourceStream }
519
- }))
565
+ handles.push(
566
+ yield* spawnCommand(ChildProcess.make(command.command, command.args, {
567
+ ...command.options,
568
+ stdin: { ...stdinConfig, stream: sourceStream }
569
+ }))
570
+ )
520
571
  } else {
521
572
  // Pipe to custom fd (fd3, fd4, etc.)
522
573
  const fd = ChildProcess.parseFdName(toOption)
523
574
  if (Predicate.isNotUndefined(fd)) {
524
575
  const fdName = ChildProcess.fdName(fd) as `fd${number}`
525
576
  const existingFds = command.options.additionalFds ?? {}
526
- handle = spawnCommand(ChildProcess.make(command.command, command.args, {
527
- ...command.options,
528
- additionalFds: {
529
- ...existingFds,
530
- [fdName]: { type: "input" as const, stream: sourceStream }
531
- }
532
- }))
577
+ handles.push(
578
+ yield* spawnCommand(ChildProcess.make(command.command, command.args, {
579
+ ...command.options,
580
+ additionalFds: {
581
+ ...existingFds,
582
+ [fdName]: { type: "input" as const, stream: sourceStream }
583
+ }
584
+ }))
585
+ )
533
586
  } else {
534
587
  // Invalid fd name, fall back to stdin
535
- handle = spawnCommand(ChildProcess.make(command.command, command.args, {
536
- ...command.options,
537
- stdin: { ...stdinConfig, stream: sourceStream }
538
- }))
588
+ handles.push(
589
+ yield* spawnCommand(ChildProcess.make(command.command, command.args, {
590
+ ...command.options,
591
+ stdin: { ...stdinConfig, stream: sourceStream }
592
+ }))
593
+ )
539
594
  }
540
595
  }
541
596
  }
542
597
 
543
- return yield* handle
598
+ const handle = handles[handles.length - 1]
599
+ const unref = Effect.gen(function*() {
600
+ const rerefs: Array<Effect.Effect<void, PlatformError.PlatformError>> = []
601
+ for (const handle of handles) {
602
+ rerefs.push(yield* handle.unref)
603
+ }
604
+ return Effect.forEach([...rerefs].reverse(), (reref) => reref, { discard: true })
605
+ })
606
+
607
+ return makeHandle({
608
+ pid: handle.pid,
609
+ exitCode: handle.exitCode,
610
+ isRunning: handle.isRunning,
611
+ kill: handle.kill,
612
+ stdin: handle.stdin,
613
+ stdout: handle.stdout,
614
+ stderr: handle.stderr,
615
+ all: handle.all,
616
+ getInputFd: handle.getInputFd,
617
+ getOutputFd: handle.getOutputFd,
618
+ unref
619
+ })
544
620
  }
545
621
  }
546
622
  })
547
623
 
548
- return ChildProcessSpawner.of({
549
- spawn: spawnCommand
550
- })
624
+ return makeSpawner(spawnCommand)
551
625
  })
552
626
 
553
627
  /**
554
628
  * Layer providing the `NodeChildProcessSpawner` implementation.
555
629
  *
630
+ * @category layers
556
631
  * @since 4.0.0
557
- * @category Layers
558
632
  */
559
633
  export const layer: Layer.Layer<
560
634
  ChildProcessSpawner,
@@ -569,8 +643,8 @@ export const layer: Layer.Layer<
569
643
  /**
570
644
  * Result of flattening a pipeline of commands.
571
645
  *
646
+ * @category models
572
647
  * @since 4.0.0
573
- * @category Models
574
648
  */
575
649
  export interface FlattenedPipeline {
576
650
  readonly commands: Arr.NonEmptyReadonlyArray<ChildProcess.StandardCommand>
@@ -581,8 +655,8 @@ export interface FlattenedPipeline {
581
655
  * Flattens a `Command` into an array of `StandardCommand`s along with pipe
582
656
  * options for each connection.
583
657
  *
658
+ * @category utils
584
659
  * @since 4.0.0
585
- * @category Utilities
586
660
  */
587
661
  export const flattenCommand = (
588
662
  command: ChildProcess.Command
@@ -1,8 +1,27 @@
1
1
  /**
2
- * @since 1.0.0
2
+ * Node TCP socket integration for Effect Cluster runner communication.
3
+ *
4
+ * This module provides the shared Node layers used by socket-based cluster
5
+ * transports: a client protocol that opens TCP sockets to runner addresses and
6
+ * a socket server that listens for incoming runner RPC traffic. It is useful
7
+ * when wiring Node or Node-compatible cluster runners, sharing the same socket
8
+ * implementation across platform packages, or building tests and deployments
9
+ * that need direct runner-to-runner RPC over TCP rather than HTTP.
10
+ *
11
+ * Cluster runners must advertise an address that peers can reach while the
12
+ * server may listen on a different address via `runnerListenAddress`, which is
13
+ * common behind containers, port mappings, or Kubernetes services. Serialization
14
+ * is supplied by the surrounding layer, and gossip, shard discovery, health
15
+ * checks, and storage-backed delivery are coordinated by the cluster services
16
+ * that use this transport. Keep those responsibilities separate when debugging:
17
+ * a reachable socket does not by itself guarantee that runner membership,
18
+ * shard ownership, or persisted message notification is current.
19
+ *
20
+ * @since 4.0.0
3
21
  */
4
22
  import * as Effect from "effect/Effect"
5
23
  import * as Layer from "effect/Layer"
24
+ import * as Option from "effect/Option"
6
25
  import * as Runners from "effect/unstable/cluster/Runners"
7
26
  import * as ShardingConfig from "effect/unstable/cluster/ShardingConfig"
8
27
  import * as RpcClient from "effect/unstable/rpc/RpcClient"
@@ -13,8 +32,11 @@ import * as NodeSocket from "./NodeSocket.ts"
13
32
  import * as NodeSocketServer from "./NodeSocketServer.ts"
14
33
 
15
34
  /**
16
- * @since 1.0.0
17
- * @category Layers
35
+ * Provides the cluster `RpcClientProtocol` by opening TCP sockets to runner
36
+ * addresses and using the current RPC serialization service.
37
+ *
38
+ * @category layers
39
+ * @since 4.0.0
18
40
  */
19
41
  export const layerClientProtocol: Layer.Layer<
20
42
  Runners.RpcClientProtocol,
@@ -39,8 +61,11 @@ export const layerClientProtocol: Layer.Layer<
39
61
  )
40
62
 
41
63
  /**
42
- * @since 1.0.0
43
- * @category Layers
64
+ * Provides the socket server used by cluster runners, listening on
65
+ * `ShardingConfig.runnerListenAddress` or `runnerAddress`.
66
+ *
67
+ * @category layers
68
+ * @since 4.0.0
44
69
  */
45
70
  export const layerSocketServer: Layer.Layer<
46
71
  SocketServer.SocketServer,
@@ -48,9 +73,9 @@ export const layerSocketServer: Layer.Layer<
48
73
  ShardingConfig.ShardingConfig
49
74
  > = Effect.gen(function*() {
50
75
  const config = yield* ShardingConfig.ShardingConfig
51
- const listenAddress = config.runnerListenAddress ?? config.runnerAddress
52
- if (listenAddress === undefined) {
76
+ const listenAddress = Option.orElse(config.runnerListenAddress, () => config.runnerAddress)
77
+ if (Option.isNone(listenAddress)) {
53
78
  return yield* Effect.die("layerSocketServer: ShardingConfig.runnerListenAddress is None")
54
79
  }
55
- return NodeSocketServer.layer(listenAddress)
80
+ return NodeSocketServer.layer(listenAddress.value)
56
81
  }).pipe(Layer.unwrap)
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Node.js implementation of the Crypto service.
3
+ *
4
+ * @since 1.0.0
5
+ */
6
+ import * as EffectCrypto from "effect/Crypto"
7
+ import * as Effect from "effect/Effect"
8
+ import * as Layer from "effect/Layer"
9
+ import * as PlatformError from "effect/PlatformError"
10
+ import * as NodeCrypto from "node:crypto"
11
+
12
+ const toHashAlgorithm = (algorithm: EffectCrypto.DigestAlgorithm): string => {
13
+ switch (algorithm) {
14
+ case "SHA-1":
15
+ return "sha1"
16
+ case "SHA-256":
17
+ return "sha256"
18
+ case "SHA-384":
19
+ return "sha384"
20
+ case "SHA-512":
21
+ return "sha512"
22
+ }
23
+ }
24
+
25
+ const digest: EffectCrypto.Crypto["digest"] = (algorithm, data) =>
26
+ Effect.try({
27
+ try: () => Uint8Array.from(NodeCrypto.createHash(toHashAlgorithm(algorithm)).update(data).digest()),
28
+ catch: (cause) =>
29
+ PlatformError.systemError({
30
+ module: "Crypto",
31
+ method: "digest",
32
+ _tag: "Unknown",
33
+ description: "Could not compute digest",
34
+ cause
35
+ })
36
+ })
37
+
38
+ /**
39
+ * The default Node.js Crypto service implementation.
40
+ *
41
+ * @category constructors
42
+ * @since 1.0.0
43
+ */
44
+ export const make: EffectCrypto.Crypto = EffectCrypto.make({
45
+ randomBytes: NodeCrypto.randomBytes,
46
+ digest
47
+ })
48
+
49
+ /**
50
+ * A layer that provides the Node.js Crypto service implementation.
51
+ *
52
+ * @category layers
53
+ * @since 1.0.0
54
+ */
55
+ export const layer: Layer.Layer<EffectCrypto.Crypto> = Layer.succeed(EffectCrypto.Crypto, make)
@@ -1,5 +1,23 @@
1
1
  /**
2
- * @since 1.0.0
2
+ * Shared Node-compatible implementation of Effect's `FileSystem` service.
3
+ *
4
+ * This module adapts Node's `node:fs`, `node:os`, and `node:path` APIs into a
5
+ * layer that can be provided to Effect programs running on Node-compatible
6
+ * runtimes. It is used by platform packages to provide file and directory I/O,
7
+ * permissions, links, metadata, temporary files and directories, and file
8
+ * watching through the `FileSystem` service.
9
+ *
10
+ * Paths are passed to Node filesystem APIs, so relative paths are resolved by
11
+ * the current working directory and platform path rules still apply. Node
12
+ * filesystem failures are translated into `PlatformError` values, while invalid
13
+ * arguments become `BadArgument` failures. Open files are scoped resources with
14
+ * tracked read and write positions; append mode lets the operating system choose
15
+ * the write offset. File watching is exposed as a stream and follows
16
+ * `node:fs.watch` semantics unless a `WatchBackend` is provided, so recursive
17
+ * support, event coalescing, and reported paths can vary by runtime and
18
+ * platform.
19
+ *
20
+ * @since 4.0.0
3
21
  */
4
22
  import * as Cause from "effect/Cause"
5
23
  import * as Effect from "effect/Effect"
@@ -232,7 +250,7 @@ const makeFile = (() => {
232
250
  readonly fd: FileSystem.File.Descriptor
233
251
  private readonly append: boolean
234
252
 
235
- private position: bigint = 0n
253
+ private position: bigint = BigInt(0)
236
254
 
237
255
  constructor(
238
256
  fd: FileSystem.File.Descriptor,
@@ -285,19 +303,19 @@ const makeFile = (() => {
285
303
  const position = this.position
286
304
  return Effect.map(
287
305
  nodeReadAlloc(this.fd, { buffer, position }),
288
- (bytesRead): Buffer | undefined => {
306
+ (bytesRead): Option.Option<Buffer> => {
289
307
  if (bytesRead === 0) {
290
- return undefined
308
+ return Option.none()
291
309
  }
292
310
 
293
311
  this.position = position + BigInt(bytesRead)
294
312
  if (bytesRead === sizeNumber) {
295
- return buffer
313
+ return Option.some(buffer)
296
314
  }
297
315
 
298
316
  const dst = Buffer.allocUnsafeSlow(bytesRead)
299
317
  buffer.copy(dst, 0, 0, bytesRead)
300
- return dst
318
+ return Option.some(dst)
301
319
  }
302
320
  )
303
321
  })
@@ -468,19 +486,19 @@ const makeFileInfo = (stat: NFS.Stats): FileSystem.File.Info => ({
468
486
  stat.isSocket() ?
469
487
  "Socket" :
470
488
  "Unknown",
471
- mtime: stat.mtime,
472
- atime: stat.atime,
473
- birthtime: stat.birthtime,
489
+ mtime: Option.fromNullishOr(stat.mtime),
490
+ atime: Option.fromNullishOr(stat.atime),
491
+ birthtime: Option.fromNullishOr(stat.birthtime),
474
492
  dev: stat.dev,
475
- rdev: stat.rdev,
476
- ino: stat.ino,
493
+ rdev: Option.fromNullishOr(stat.rdev),
494
+ ino: Option.fromNullishOr(stat.ino),
477
495
  mode: stat.mode,
478
- nlink: stat.nlink,
479
- uid: stat.uid,
480
- gid: stat.gid,
496
+ nlink: Option.fromNullishOr(stat.nlink),
497
+ uid: Option.fromNullishOr(stat.uid),
498
+ gid: Option.fromNullishOr(stat.gid),
481
499
  size: FileSystem.Size(stat.size),
482
- blksize: FileSystem.Size(stat.blksize),
483
- blocks: stat.blocks
500
+ blksize: stat.blksize !== undefined ? Option.some(FileSystem.Size(stat.blksize)) : Option.none(),
501
+ blocks: Option.fromNullishOr(stat.blocks)
484
502
  })
485
503
  const stat = (() => {
486
504
  const nodeStat = effectify(
@@ -531,7 +549,9 @@ const watchNode = (path: string) =>
531
549
  Stream.callback<FileSystem.WatchEvent, Error.PlatformError>((queue) =>
532
550
  Effect.acquireRelease(
533
551
  Effect.sync(() => {
534
- const watcher = NFS.watch(path, {}, (event, path) => {
552
+ const watcher = NFS.watch(path, {
553
+ recursive: true
554
+ }, (event, path) => {
535
555
  if (!path) return
536
556
  switch (event) {
537
557
  case "rename": {
@@ -570,15 +590,14 @@ const watchNode = (path: string) =>
570
590
  )
571
591
  )
572
592
 
573
- const watch = (backend: FileSystem.WatchBackend["Service"] | undefined, path: string) =>
593
+ const watch = (backend: Option.Option<FileSystem.WatchBackend["Service"]>, path: string) =>
574
594
  stat(path).pipe(
575
- Effect.map((stat) => {
576
- if (backend) {
577
- const stream = backend.register(path, stat)
578
- if (stream) return stream
579
- }
580
- return watchNode(path)
581
- }),
595
+ Effect.map((stat) =>
596
+ backend.pipe(
597
+ Option.flatMap((_) => _.register(path, stat)),
598
+ Option.getOrElse(() => watchNode(path))
599
+ )
600
+ ),
582
601
  Stream.unwrap
583
602
  )
584
603
 
@@ -628,13 +647,16 @@ const makeFileSystem = Effect.map(Effect.serviceOption(FileSystem.WatchBackend),
628
647
  truncate,
629
648
  utimes,
630
649
  watch(path) {
631
- return watch(Option.getOrUndefined(backend), path)
650
+ return watch(backend, path)
632
651
  },
633
652
  writeFile
634
653
  }))
635
654
 
636
655
  /**
637
- * @since 1.0.0
638
- * @category Layers
656
+ * Provides the `FileSystem` service backed by Node filesystem APIs, including
657
+ * file operations, directory operations, links, metadata, and file watching.
658
+ *
659
+ * @category layers
660
+ * @since 4.0.0
639
661
  */
640
662
  export const layer: Layer.Layer<FileSystem.FileSystem> = Layer.effect(FileSystem.FileSystem)(makeFileSystem)
package/src/NodePath.ts CHANGED
@@ -1,5 +1,19 @@
1
1
  /**
2
- * @since 1.0.0
2
+ * Shared Node-compatible implementation of Effect's `Path` service.
3
+ *
4
+ * This module adapts Node's `node:path` and `node:url` APIs into layers that
5
+ * can be provided to Effect programs needing path manipulation, such as
6
+ * resolving configuration files, building file system locations, parsing
7
+ * names and extensions, or converting between file paths and `file:` URLs.
8
+ *
9
+ * The default layer follows the host platform semantics exposed by
10
+ * `node:path`, while `layerPosix` and `layerWin32` provide stable POSIX or
11
+ * Windows behavior regardless of the current runtime. Path operations are
12
+ * syntactic and do not check whether files exist; separators, drive letters,
13
+ * UNC paths, and URL encoding rules can also differ by platform. Invalid
14
+ * file URL conversions are reported through `BadArgument`.
15
+ *
16
+ * @since 4.0.0
3
17
  */
4
18
  import * as Effect from "effect/Effect"
5
19
  import * as Layer from "effect/Layer"
@@ -31,8 +45,11 @@ const toFileUrl = (path: string): Effect.Effect<URL, BadArgument> =>
31
45
  })
32
46
 
33
47
  /**
34
- * @since 1.0.0
35
- * @category Layers
48
+ * Provides the `Path` service using Node's POSIX path implementation plus
49
+ * file URL conversion helpers.
50
+ *
51
+ * @category layers
52
+ * @since 4.0.0
36
53
  */
37
54
  export const layerPosix: Layer.Layer<Path> = Layer.succeed(Path)({
38
55
  [TypeId]: TypeId,
@@ -42,8 +59,11 @@ export const layerPosix: Layer.Layer<Path> = Layer.succeed(Path)({
42
59
  })
43
60
 
44
61
  /**
45
- * @since 1.0.0
46
- * @category Layers
62
+ * Provides the `Path` service using Node's Windows path implementation plus
63
+ * file URL conversion helpers.
64
+ *
65
+ * @category layers
66
+ * @since 4.0.0
47
67
  */
48
68
  export const layerWin32: Layer.Layer<Path> = Layer.succeed(Path)({
49
69
  [TypeId]: TypeId,
@@ -53,8 +73,11 @@ export const layerWin32: Layer.Layer<Path> = Layer.succeed(Path)({
53
73
  })
54
74
 
55
75
  /**
56
- * @since 1.0.0
57
- * @category Layers
76
+ * Provides the default `Path` service using the host platform's Node path
77
+ * implementation plus file URL conversion helpers.
78
+ *
79
+ * @category layers
80
+ * @since 4.0.0
58
81
  */
59
82
  export const layer: Layer.Layer<Path> = Layer.succeed(Path)({
60
83
  [TypeId]: TypeId,
@@ -1,17 +1,42 @@
1
1
  /**
2
- * @since 1.0.0
2
+ * Shared runtime helpers for running Effect programs as Node-compatible
3
+ * process entry points.
4
+ *
5
+ * This module provides the common `runMain` implementation used by
6
+ * Node-compatible platform packages. It is intended for CLIs, scripts,
7
+ * workers, servers, and other process-oriented programs that should run an
8
+ * Effect as their main fiber while still following Node process conventions.
9
+ *
10
+ * The runner installs `SIGINT` and `SIGTERM` handlers for the lifetime of the
11
+ * main fiber, translating those process signals into fiber interruption so
12
+ * Effect finalizers and the configured teardown can run. When the fiber exits,
13
+ * the signal listeners are removed and teardown determines the exit code. Clean
14
+ * success lets the Node event loop drain naturally instead of forcing
15
+ * `process.exit(0)`, while signal-triggered or non-zero exits call
16
+ * `process.exit` after teardown, so long-running resources should be modeled
17
+ * in the Effect scope and finalized explicitly.
18
+ *
19
+ * @since 4.0.0
3
20
  */
4
21
  import type { Effect } from "effect/Effect"
5
22
  import * as Runtime from "effect/Runtime"
6
23
 
7
24
  /**
8
- * @since 1.0.0
9
- * @category Run main
25
+ * Runs an Effect as the Node process main program, interrupting the fiber on
26
+ * `SIGINT` or `SIGTERM` and invoking the configured teardown to determine the
27
+ * process exit code.
28
+ *
29
+ * @category running
30
+ * @since 4.0.0
10
31
  */
11
32
  export const runMain: {
12
33
  /**
13
- * @since 1.0.0
14
- * @category Run main
34
+ * Runs an Effect as the Node process main program, interrupting the fiber on
35
+ * `SIGINT` or `SIGTERM` and invoking the configured teardown to determine the
36
+ * process exit code.
37
+ *
38
+ * @category running
39
+ * @since 4.0.0
15
40
  */
16
41
  (
17
42
  options?: {
@@ -20,8 +45,12 @@ export const runMain: {
20
45
  }
21
46
  ): <E, A>(effect: Effect<A, E>) => void
22
47
  /**
23
- * @since 1.0.0
24
- * @category Run main
48
+ * Runs an Effect as the Node process main program, interrupting the fiber on
49
+ * `SIGINT` or `SIGTERM` and invoking the configured teardown to determine the
50
+ * process exit code.
51
+ *
52
+ * @category running
53
+ * @since 4.0.0
25
54
  */
26
55
  <E, A>(
27
56
  effect: Effect<A, E>,
@@ -37,10 +66,8 @@ export const runMain: {
37
66
  let receivedSignal = false
38
67
 
39
68
  fiber.addObserver((exit) => {
40
- if (!receivedSignal) {
41
- process.removeListener("SIGINT", onSigint)
42
- process.removeListener("SIGTERM", onSigint)
43
- }
69
+ process.removeListener("SIGINT", onSigint)
70
+ process.removeListener("SIGTERM", onSigint)
44
71
  teardown(exit, (code) => {
45
72
  if (receivedSignal || code !== 0) {
46
73
  process.exit(code)
@@ -50,8 +77,6 @@ export const runMain: {
50
77
 
51
78
  function onSigint() {
52
79
  receivedSignal = true
53
- process.removeListener("SIGINT", onSigint)
54
- process.removeListener("SIGTERM", onSigint)
55
80
  fiber.interruptUnsafe(fiber.id)
56
81
  }
57
82