@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.
- package/dist/NodeChildProcessSpawner.d.ts +3 -3
- package/dist/NodeChildProcessSpawner.d.ts.map +1 -1
- package/dist/NodeChildProcessSpawner.js +76 -17
- package/dist/NodeChildProcessSpawner.js.map +1 -1
- package/dist/NodeClusterSocket.d.ts +10 -4
- package/dist/NodeClusterSocket.d.ts.map +1 -1
- package/dist/NodeClusterSocket.js +33 -8
- package/dist/NodeClusterSocket.js.map +1 -1
- package/dist/NodeCrypto.d.ts +22 -0
- package/dist/NodeCrypto.d.ts.map +1 -0
- package/dist/NodeCrypto.js +50 -0
- package/dist/NodeCrypto.js.map +1 -0
- package/dist/NodeFileSystem.d.ts +5 -2
- package/dist/NodeFileSystem.d.ts.map +1 -1
- package/dist/NodeFileSystem.js +43 -26
- package/dist/NodeFileSystem.js.map +1 -1
- package/dist/NodePath.d.ts +15 -6
- package/dist/NodePath.d.ts.map +1 -1
- package/dist/NodePath.js +30 -7
- package/dist/NodePath.js.map +1 -1
- package/dist/NodeRuntime.d.ts +36 -7
- package/dist/NodeRuntime.d.ts.map +1 -1
- package/dist/NodeRuntime.js +8 -8
- package/dist/NodeRuntime.js.map +1 -1
- package/dist/NodeSink.d.ts +34 -4
- package/dist/NodeSink.d.ts.map +1 -1
- package/dist/NodeSink.js +23 -4
- package/dist/NodeSink.js.map +1 -1
- package/dist/NodeSocket.d.ts +40 -9
- package/dist/NodeSocket.d.ts.map +1 -1
- package/dist/NodeSocket.js +31 -15
- package/dist/NodeSocket.js.map +1 -1
- package/dist/NodeSocketServer.d.ts +24 -7
- package/dist/NodeSocketServer.d.ts.map +1 -1
- package/dist/NodeSocketServer.js +28 -11
- package/dist/NodeSocketServer.js.map +1 -1
- package/dist/NodeStdio.d.ts +6 -5
- package/dist/NodeStdio.d.ts.map +1 -1
- package/dist/NodeStdio.js +31 -7
- package/dist/NodeStdio.js.map +1 -1
- package/dist/NodeStream.d.ts +84 -20
- package/dist/NodeStream.d.ts.map +1 -1
- package/dist/NodeStream.js +76 -23
- package/dist/NodeStream.js.map +1 -1
- package/dist/NodeTerminal.d.ts +9 -2
- package/dist/NodeTerminal.d.ts.map +1 -1
- package/dist/NodeTerminal.js +13 -3
- package/dist/NodeTerminal.js.map +1 -1
- package/dist/index.d.ts +220 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +221 -0
- package/dist/index.js.map +1 -0
- package/package.json +7 -6
- package/src/NodeChildProcessSpawner.ts +101 -27
- package/src/NodeClusterSocket.ts +33 -8
- package/src/NodeCrypto.ts +55 -0
- package/src/NodeFileSystem.ts +50 -28
- package/src/NodePath.ts +30 -7
- package/src/NodeRuntime.ts +38 -13
- package/src/NodeSink.ts +41 -4
- package/src/NodeSocket.ts +46 -16
- package/src/NodeSocketServer.ts +46 -12
- package/src/NodeStdio.ts +49 -23
- package/src/NodeStream.ts +97 -30
- package/src/NodeTerminal.ts +31 -4
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
|
|
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
|
|
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
|
package/src/NodeClusterSocket.ts
CHANGED
|
@@ -1,8 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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
|
-
*
|
|
17
|
-
*
|
|
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
|
-
*
|
|
43
|
-
*
|
|
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
|
|
52
|
-
if (listenAddress
|
|
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)
|
package/src/NodeFileSystem.ts
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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 =
|
|
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
|
|
306
|
+
(bytesRead): Option.Option<Buffer> => {
|
|
289
307
|
if (bytesRead === 0) {
|
|
290
|
-
return
|
|
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, {
|
|
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"]
|
|
593
|
+
const watch = (backend: Option.Option<FileSystem.WatchBackend["Service"]>, path: string) =>
|
|
574
594
|
stat(path).pipe(
|
|
575
|
-
Effect.map((stat) =>
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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(
|
|
650
|
+
return watch(backend, path)
|
|
632
651
|
},
|
|
633
652
|
writeFile
|
|
634
653
|
}))
|
|
635
654
|
|
|
636
655
|
/**
|
|
637
|
-
*
|
|
638
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
35
|
-
*
|
|
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
|
-
*
|
|
46
|
-
*
|
|
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
|
-
*
|
|
57
|
-
*
|
|
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,
|
package/src/NodeRuntime.ts
CHANGED
|
@@ -1,17 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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
|
-
*
|
|
9
|
-
*
|
|
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
|
-
*
|
|
14
|
-
*
|
|
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
|
-
*
|
|
24
|
-
*
|
|
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
|
-
|
|
41
|
-
|
|
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
|
|