@graftland/plugin-remote 0.0.0-stage → 0.2.0-beta.2
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/LICENSE +202 -0
- package/README.md +76 -2
- package/README.zh-CN.md +71 -0
- package/README.zh-TW.md +71 -0
- package/dist/Auth-BElTC9_X.js +6 -0
- package/dist/Auth.d.ts +49 -0
- package/dist/Auth.d.ts.map +1 -0
- package/dist/Auth.js +8 -0
- package/dist/Auth.js.map +1 -0
- package/dist/Bootstrap-MCwivGN1.js +9 -0
- package/dist/ConfigDirectory-BCC6q2R4.js +5 -0
- package/dist/Contract-CcpSIJm4.js +121 -0
- package/dist/Contract.d.ts +71 -0
- package/dist/Contract.d.ts.map +1 -0
- package/dist/Contract.js +1 -0
- package/dist/Contract.js.map +1 -0
- package/dist/Errors-oPsTLY7l.js +45 -0
- package/dist/Errors.d.ts +67 -0
- package/dist/Errors.d.ts.map +1 -0
- package/dist/Errors.js +47 -0
- package/dist/Errors.js.map +1 -0
- package/dist/FailureGroup-BcH0XOBi.js +53 -0
- package/dist/Framing-zaYdOJnT.js +61 -0
- package/dist/Graft-BsYq3F-J.js +985 -0
- package/dist/Instance.d.ts +7 -0
- package/dist/Instance.d.ts.map +1 -0
- package/dist/Instance.js +14 -0
- package/dist/Instance.js.map +1 -0
- package/dist/Packages-B1TPk8pa.js +111 -0
- package/dist/Peer-IJgFK6mD.js +342 -0
- package/dist/ProcessObservation-DEGrEMfP.js +38 -0
- package/dist/ServeLifecycle-CJM0gG5B.js +24 -0
- package/dist/Testing.d.ts +26 -0
- package/dist/Testing.d.ts.map +1 -0
- package/dist/Testing.js +69 -0
- package/dist/Testing.js.map +1 -0
- package/dist/Transport-DULzpsgR.js +6 -0
- package/dist/Transport.d.ts +39 -0
- package/dist/Transport.d.ts.map +1 -0
- package/dist/Transport.js +8 -0
- package/dist/Transport.js.map +1 -0
- package/dist/WatchdogProtocol-BZmVxrBP.js +17 -0
- package/dist/_virtual/_rolldown/runtime.js +12 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +28275 -0
- package/dist/bin.js.map +1 -0
- package/dist/childRuntime-DSqtFN9o.js +188 -0
- package/dist/cleanup-BPWr-5U9.js +13 -0
- package/dist/command-9zrHdbwT.js +7 -0
- package/dist/connectionEnded-DUgcobEd.js +9 -0
- package/dist/directTransport-DrexuPSm.js +5 -0
- package/dist/entry-SYsx9VNb.js +708 -0
- package/dist/host/auth/bearer.d.ts +5 -0
- package/dist/host/auth/bearer.d.ts.map +1 -0
- package/dist/host/auth/bearer.js +4 -0
- package/dist/host/auth/bearer.js.map +1 -0
- package/dist/host/auth/jwt.d.ts +5 -0
- package/dist/host/auth/jwt.d.ts.map +1 -0
- package/dist/host/auth/jwt.js +4 -0
- package/dist/host/auth/jwt.js.map +1 -0
- package/dist/host/auth/mtls.d.ts +8 -0
- package/dist/host/auth/mtls.d.ts.map +1 -0
- package/dist/host/auth/mtls.js +34 -0
- package/dist/host/auth/mtls.js.map +1 -0
- package/dist/host/auth/none.d.ts +3 -0
- package/dist/host/auth/none.d.ts.map +1 -0
- package/dist/host/auth/none.js +13 -0
- package/dist/host/auth/none.js.map +1 -0
- package/dist/host/command.d.ts +22 -0
- package/dist/host/command.d.ts.map +1 -0
- package/dist/host/command.js +61 -0
- package/dist/host/command.js.map +1 -0
- package/dist/host/local.d.ts +4 -0
- package/dist/host/local.d.ts.map +1 -0
- package/dist/host/local.js +52 -0
- package/dist/host/local.js.map +1 -0
- package/dist/host/unix.d.ts +11 -0
- package/dist/host/unix.d.ts.map +1 -0
- package/dist/host/unix.js +39 -0
- package/dist/host/unix.js.map +1 -0
- package/dist/host/websocket.d.ts +13 -0
- package/dist/host/websocket.d.ts.map +1 -0
- package/dist/host/websocket.js +36 -0
- package/dist/host/websocket.js.map +1 -0
- package/dist/host/worker.d.ts +4 -0
- package/dist/host/worker.d.ts.map +1 -0
- package/dist/host/worker.js +52 -0
- package/dist/host/worker.js.map +1 -0
- package/dist/importer-D7zSgqxq.js +347 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +86 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/Certificates.js +182 -0
- package/dist/internal/Certificates.js.map +1 -0
- package/dist/internal/FailureGroup.js +55 -0
- package/dist/internal/FailureGroup.js.map +1 -0
- package/dist/internal/Limits.d.ts +1 -0
- package/dist/internal/Limits.js +17 -0
- package/dist/internal/Limits.js.map +1 -0
- package/dist/internal/Peer.js +6 -0
- package/dist/internal/Peer.js.map +1 -0
- package/dist/internal/ServeLifecycle.js +26 -0
- package/dist/internal/ServeLifecycle.js.map +1 -0
- package/dist/internal/Supervisor.js +7 -0
- package/dist/internal/Supervisor.js.map +1 -0
- package/dist/internal/assembly.d.ts +5 -0
- package/dist/internal/assembly.js +138 -0
- package/dist/internal/assembly.js.map +1 -0
- package/dist/internal/auth/mtls.js +23 -0
- package/dist/internal/auth/mtls.js.map +1 -0
- package/dist/internal/auth/token.js +19 -0
- package/dist/internal/auth/token.js.map +1 -0
- package/dist/internal/auth/tokenCredentials.d.ts +8 -0
- package/dist/internal/auth/tokenCredentials.d.ts.map +1 -0
- package/dist/internal/auth/tokenCredentials.js +33 -0
- package/dist/internal/auth/tokenCredentials.js.map +1 -0
- package/dist/internal/child.js +125 -0
- package/dist/internal/child.js.map +1 -0
- package/dist/internal/childErrors.d.ts +33 -0
- package/dist/internal/childErrors.d.ts.map +1 -0
- package/dist/internal/childErrors.js +29 -0
- package/dist/internal/childErrors.js.map +1 -0
- package/dist/internal/childRuntime.js +59 -0
- package/dist/internal/childRuntime.js.map +1 -0
- package/dist/internal/cleanup.js +15 -0
- package/dist/internal/cleanup.js.map +1 -0
- package/dist/internal/config.js +6 -0
- package/dist/internal/config.js.map +1 -0
- package/dist/internal/control.js +42 -0
- package/dist/internal/control.js.map +1 -0
- package/dist/internal/credentials.d.ts +44 -0
- package/dist/internal/credentials.d.ts.map +1 -0
- package/dist/internal/credentials.js +54 -0
- package/dist/internal/credentials.js.map +1 -0
- package/dist/internal/directTransport.d.ts +3 -0
- package/dist/internal/directTransport.js +7 -0
- package/dist/internal/directTransport.js.map +1 -0
- package/dist/internal/entry.d.ts +9 -0
- package/dist/internal/entry.d.ts.map +1 -0
- package/dist/internal/entry.js +144 -0
- package/dist/internal/entry.js.map +1 -0
- package/dist/internal/entryConfig.d.ts +202 -0
- package/dist/internal/entryConfig.d.ts.map +1 -0
- package/dist/internal/entryConfig.js +176 -0
- package/dist/internal/entryConfig.js.map +1 -0
- package/dist/internal/execution.d.ts +5 -0
- package/dist/internal/execution.js +43 -0
- package/dist/internal/execution.js.map +1 -0
- package/dist/internal/follow.js +11 -0
- package/dist/internal/follow.js.map +1 -0
- package/dist/internal/forkTree.js +7 -0
- package/dist/internal/forkTree.js.map +1 -0
- package/dist/internal/host/local.js +28 -0
- package/dist/internal/host/local.js.map +1 -0
- package/dist/internal/host/runner.js +100 -0
- package/dist/internal/host/runner.js.map +1 -0
- package/dist/internal/host.js +71 -0
- package/dist/internal/host.js.map +1 -0
- package/dist/internal/importer.js +18 -0
- package/dist/internal/importer.js.map +1 -0
- package/dist/internal/listenerStartup.js +17 -0
- package/dist/internal/listenerStartup.js.map +1 -0
- package/dist/internal/loadModule.js +20 -0
- package/dist/internal/loadModule.js.map +1 -0
- package/dist/internal/platform.js +9 -0
- package/dist/internal/platform.js.map +1 -0
- package/dist/internal/port.d.ts +13 -0
- package/dist/internal/port.d.ts.map +1 -0
- package/dist/internal/port.js +275 -0
- package/dist/internal/port.js.map +1 -0
- package/dist/internal/release.js +25 -0
- package/dist/internal/release.js.map +1 -0
- package/dist/internal/runner/Bootstrap.js +11 -0
- package/dist/internal/runner/Bootstrap.js.map +1 -0
- package/dist/internal/runner/Connection.d.ts +11 -0
- package/dist/internal/runner/Connection.js +56 -0
- package/dist/internal/runner/Connection.js.map +1 -0
- package/dist/internal/runner/Execution.js +115 -0
- package/dist/internal/runner/Execution.js.map +1 -0
- package/dist/internal/runner/Framing.d.ts +11 -0
- package/dist/internal/runner/Framing.d.ts.map +1 -0
- package/dist/internal/runner/Framing.js +63 -0
- package/dist/internal/runner/Framing.js.map +1 -0
- package/dist/internal/runner/Local.js +46 -0
- package/dist/internal/runner/Local.js.map +1 -0
- package/dist/internal/runner/Lock.js +25 -0
- package/dist/internal/runner/Lock.js.map +1 -0
- package/dist/internal/runner/Multiplex.d.ts +5 -0
- package/dist/internal/runner/Multiplex.js +49 -0
- package/dist/internal/runner/Multiplex.js.map +1 -0
- package/dist/internal/runner/Packages.d.ts +3 -0
- package/dist/internal/runner/Packages.js +108 -0
- package/dist/internal/runner/Packages.js.map +1 -0
- package/dist/internal/runner/ProcessObservation.js +40 -0
- package/dist/internal/runner/ProcessObservation.js.map +1 -0
- package/dist/internal/runner/Processes.js +83 -0
- package/dist/internal/runner/Processes.js.map +1 -0
- package/dist/internal/runner/Retention.js +13 -0
- package/dist/internal/runner/Retention.js.map +1 -0
- package/dist/internal/runner/Store.js +78 -0
- package/dist/internal/runner/Store.js.map +1 -0
- package/dist/internal/runner/Supervisor.js +375 -0
- package/dist/internal/runner/Supervisor.js.map +1 -0
- package/dist/internal/runner/Watchdog.js +47 -0
- package/dist/internal/runner/Watchdog.js.map +1 -0
- package/dist/internal/runner/WatchdogProtocol.js +19 -0
- package/dist/internal/runner/WatchdogProtocol.js.map +1 -0
- package/dist/internal/runner/Wire.d.ts +5 -0
- package/dist/internal/runner/Wire.js +64 -0
- package/dist/internal/runner/Wire.js.map +1 -0
- package/dist/internal/runner/accept.js +57 -0
- package/dist/internal/runner/accept.js.map +1 -0
- package/dist/internal/runner/command.js +9 -0
- package/dist/internal/runner/command.js.map +1 -0
- package/dist/internal/runner/connectionEnded.js +11 -0
- package/dist/internal/runner/connectionEnded.js.map +1 -0
- package/dist/internal/runner/model.d.ts +17 -0
- package/dist/internal/runner/model.d.ts.map +1 -0
- package/dist/internal/runner/model.js +231 -0
- package/dist/internal/runner/model.js.map +1 -0
- package/dist/internal/serve.js +60 -0
- package/dist/internal/serve.js.map +1 -0
- package/dist/internal/services.d.ts +18 -0
- package/dist/internal/services.d.ts.map +1 -0
- package/dist/internal/services.js +22 -0
- package/dist/internal/services.js.map +1 -0
- package/dist/internal/socketChannel.js +24 -0
- package/dist/internal/socketChannel.js.map +1 -0
- package/dist/internal/stdioChannel.js +23 -0
- package/dist/internal/stdioChannel.js.map +1 -0
- package/dist/internal/supervisorPlugin.js +1 -0
- package/dist/internal/supervisorPlugin.js.map +1 -0
- package/dist/internal/transportOptions.d.ts +18 -0
- package/dist/internal/transportOptions.d.ts.map +1 -0
- package/dist/internal/watchdogMain.d.ts +1 -0
- package/dist/internal/watchdogMain.js +55 -0
- package/dist/internal/watchdogMain.js.map +1 -0
- package/dist/internal/websocket.js +46 -0
- package/dist/internal/websocket.js.map +1 -0
- package/dist/internal/websocketServer.js +61 -0
- package/dist/internal/websocketServer.js.map +1 -0
- package/dist/internal/workerBootstrap.js +9 -0
- package/dist/internal/workerBootstrap.js.map +1 -0
- package/dist/internal/workerChannel.js +24 -0
- package/dist/internal/workerChannel.js.map +1 -0
- package/dist/internal/workerMain.d.ts +1 -0
- package/dist/internal/workerMain.js +18 -0
- package/dist/internal/workerMain.js.map +1 -0
- package/dist/port-Blbh8tms.js +285 -0
- package/dist/release-BY4Mlluy.js +23 -0
- package/dist/rolldown-runtime-DYf_mQCB.js +34 -0
- package/dist/runner/auth/bearer.d.ts +7 -0
- package/dist/runner/auth/bearer.d.ts.map +1 -0
- package/dist/runner/auth/bearer.js +42 -0
- package/dist/runner/auth/bearer.js.map +1 -0
- package/dist/runner/auth/jwt.d.ts +19 -0
- package/dist/runner/auth/jwt.d.ts.map +1 -0
- package/dist/runner/auth/jwt.js +65 -0
- package/dist/runner/auth/jwt.js.map +1 -0
- package/dist/runner/auth/mtls.d.ts +11 -0
- package/dist/runner/auth/mtls.d.ts.map +1 -0
- package/dist/runner/auth/mtls.js +48 -0
- package/dist/runner/auth/mtls.js.map +1 -0
- package/dist/runner/auth/none.d.ts +3 -0
- package/dist/runner/auth/none.d.ts.map +1 -0
- package/dist/runner/auth/none.js +13 -0
- package/dist/runner/auth/none.js.map +1 -0
- package/dist/runner/index.d.ts +3 -0
- package/dist/runner/index.d.ts.map +1 -0
- package/dist/runner/index.js +71 -0
- package/dist/runner/index.js.map +1 -0
- package/dist/runner/stdio.d.ts +7 -0
- package/dist/runner/stdio.d.ts.map +1 -0
- package/dist/runner/stdio.js +39 -0
- package/dist/runner/stdio.js.map +1 -0
- package/dist/runner/unix.d.ts +7 -0
- package/dist/runner/unix.d.ts.map +1 -0
- package/dist/runner/unix.js +42 -0
- package/dist/runner/unix.js.map +1 -0
- package/dist/runner/websocket.d.ts +28 -0
- package/dist/runner/websocket.d.ts.map +1 -0
- package/dist/runner/websocket.js +77 -0
- package/dist/runner/websocket.js.map +1 -0
- package/dist/socketChannel-KmtzF-9G.js +3658 -0
- package/dist/supervisorPlugin-Inb1S5IV.js +737 -0
- package/dist/token-v5ne2I0-.js +17 -0
- package/dist/tokenCredentials-WUJm2AKm.js +31 -0
- package/dist/websocketServer-MXcCJx0v.js +58 -0
- package/dist/workerChannel-P82YdNjh.js +27 -0
- package/package.json +69 -3
- package/src/Auth.ts +62 -0
- package/src/Contract.ts +100 -0
- package/src/Errors.ts +82 -0
- package/src/Instance.ts +12 -0
- package/src/Testing.ts +135 -0
- package/src/Transport.ts +52 -0
- package/src/bin.ts +145 -0
- package/src/host/auth/bearer.ts +10 -0
- package/src/host/auth/jwt.ts +10 -0
- package/src/host/auth/mtls.ts +48 -0
- package/src/host/auth/none.ts +13 -0
- package/src/host/command.ts +131 -0
- package/src/host/local.ts +41 -0
- package/src/host/unix.ts +65 -0
- package/src/host/websocket.ts +66 -0
- package/src/host/worker.ts +90 -0
- package/src/index.ts +115 -0
- package/src/internal/Certificates.ts +330 -0
- package/src/internal/FailureGroup.ts +176 -0
- package/src/internal/Limits.ts +12 -0
- package/src/internal/Peer.ts +4 -0
- package/src/internal/ServeLifecycle.ts +53 -0
- package/src/internal/Supervisor.ts +14 -0
- package/src/internal/assembly.ts +337 -0
- package/src/internal/auth/mtls.ts +40 -0
- package/src/internal/auth/token.ts +26 -0
- package/src/internal/auth/tokenCredentials.ts +65 -0
- package/src/internal/child.ts +323 -0
- package/src/internal/childErrors.ts +44 -0
- package/src/internal/childRuntime.ts +111 -0
- package/src/internal/cleanup.ts +34 -0
- package/src/internal/config.ts +8 -0
- package/src/internal/control.ts +49 -0
- package/src/internal/credentials.ts +112 -0
- package/src/internal/directTransport.ts +38 -0
- package/src/internal/entry.ts +288 -0
- package/src/internal/entryConfig.ts +190 -0
- package/src/internal/execution.ts +121 -0
- package/src/internal/follow.ts +17 -0
- package/src/internal/forkTree.ts +13 -0
- package/src/internal/host/local.ts +53 -0
- package/src/internal/host/runner.ts +187 -0
- package/src/internal/host.ts +209 -0
- package/src/internal/importer.ts +27 -0
- package/src/internal/listenerStartup.ts +55 -0
- package/src/internal/loadModule.ts +23 -0
- package/src/internal/platform.ts +12 -0
- package/src/internal/port.ts +623 -0
- package/src/internal/release.ts +71 -0
- package/src/internal/runner/Bootstrap.ts +9 -0
- package/src/internal/runner/Connection.ts +184 -0
- package/src/internal/runner/Execution.ts +244 -0
- package/src/internal/runner/Framing.ts +122 -0
- package/src/internal/runner/Local.ts +73 -0
- package/src/internal/runner/Lock.ts +29 -0
- package/src/internal/runner/Multiplex.ts +120 -0
- package/src/internal/runner/Packages.ts +178 -0
- package/src/internal/runner/ProcessObservation.ts +63 -0
- package/src/internal/runner/Processes.ts +109 -0
- package/src/internal/runner/Retention.ts +49 -0
- package/src/internal/runner/Store.ts +160 -0
- package/src/internal/runner/Supervisor.ts +901 -0
- package/src/internal/runner/Watchdog.ts +83 -0
- package/src/internal/runner/WatchdogProtocol.ts +12 -0
- package/src/internal/runner/Wire.ts +99 -0
- package/src/internal/runner/accept.ts +103 -0
- package/src/internal/runner/command.ts +15 -0
- package/src/internal/runner/connectionEnded.ts +28 -0
- package/src/internal/runner/model.ts +364 -0
- package/src/internal/serve.ts +117 -0
- package/src/internal/services.ts +74 -0
- package/src/internal/socketChannel.ts +64 -0
- package/src/internal/stdioChannel.ts +25 -0
- package/src/internal/supervisorPlugin.ts +67 -0
- package/src/internal/transportOptions.ts +17 -0
- package/src/internal/watchdogMain.ts +72 -0
- package/src/internal/websocket.ts +81 -0
- package/src/internal/websocketServer.ts +152 -0
- package/src/internal/workerBootstrap.ts +6 -0
- package/src/internal/workerChannel.ts +54 -0
- package/src/internal/workerMain.ts +23 -0
- package/src/runner/auth/bearer.ts +66 -0
- package/src/runner/auth/jwt.ts +114 -0
- package/src/runner/auth/mtls.ts +37 -0
- package/src/runner/auth/none.ts +13 -0
- package/src/runner/index.ts +97 -0
- package/src/runner/stdio.ts +76 -0
- package/src/runner/unix.ts +53 -0
- package/src/runner/websocket.ts +143 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Cause,
|
|
3
|
+
Effect,
|
|
4
|
+
Fiber,
|
|
5
|
+
Option,
|
|
6
|
+
Queue,
|
|
7
|
+
Sink,
|
|
8
|
+
Stream,
|
|
9
|
+
TxQueue,
|
|
10
|
+
} from "effect"
|
|
11
|
+
import type { Socket } from "effect/socket"
|
|
12
|
+
import { TransportError } from "../../Errors"
|
|
13
|
+
import type { Channel as ByteChannel } from "../port"
|
|
14
|
+
import * as FailureGroup from "../FailureGroup"
|
|
15
|
+
import * as Framing from "./Framing"
|
|
16
|
+
|
|
17
|
+
export interface Peer {
|
|
18
|
+
readonly await: Effect.Effect<never, TransportError>
|
|
19
|
+
readonly fail: (error: TransportError) => Effect.Effect<void>
|
|
20
|
+
readonly end: Effect.Effect<void, TransportError>
|
|
21
|
+
readonly send: (
|
|
22
|
+
channel: Framing.Channel,
|
|
23
|
+
payload: Uint8Array,
|
|
24
|
+
) => Effect.Effect<void, TransportError | Framing.FrameError>
|
|
25
|
+
readonly take: (
|
|
26
|
+
channel: Framing.Channel,
|
|
27
|
+
) => Effect.Effect<Uint8Array, TransportError>
|
|
28
|
+
}
|
|
29
|
+
/** Ports own admission and ACK backpressure. This layer only frames and separates session/instance bytes. */
|
|
30
|
+
export const make = Effect.fnUntraced(function* (
|
|
31
|
+
channel: ByteChannel,
|
|
32
|
+
): Effect.fn.Return<Peer, never, import("effect").Scope.Scope> {
|
|
33
|
+
const group = yield* FailureGroup.make<TransportError>()
|
|
34
|
+
const incoming = {
|
|
35
|
+
0: yield* Queue.unbounded<Uint8Array>(),
|
|
36
|
+
1: yield* Queue.unbounded<Uint8Array>(),
|
|
37
|
+
}
|
|
38
|
+
const outgoing = {
|
|
39
|
+
0: yield* TxQueue.unbounded<Uint8Array, Cause.Done>(),
|
|
40
|
+
1: yield* TxQueue.unbounded<Uint8Array, Cause.Done>(),
|
|
41
|
+
}
|
|
42
|
+
const next = Effect.tx(
|
|
43
|
+
Effect.gen(function* () {
|
|
44
|
+
const session = yield* TxQueue.poll(outgoing[0])
|
|
45
|
+
if (Option.isSome(session)) return session.value
|
|
46
|
+
const instance = yield* TxQueue.poll(outgoing[1])
|
|
47
|
+
if (Option.isSome(instance)) return instance.value
|
|
48
|
+
if (
|
|
49
|
+
(yield* TxQueue.isDone(outgoing[0])) &&
|
|
50
|
+
(yield* TxQueue.isDone(outgoing[1]))
|
|
51
|
+
)
|
|
52
|
+
return yield* Cause.done()
|
|
53
|
+
return yield* Effect.txRetry
|
|
54
|
+
}),
|
|
55
|
+
)
|
|
56
|
+
const writing = yield* group.run(
|
|
57
|
+
Stream.fromEffectRepeat(next).pipe(
|
|
58
|
+
Stream.run(channel.output),
|
|
59
|
+
Effect.asVoid,
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
const decoder = Framing.decoder()
|
|
63
|
+
yield* group.run(
|
|
64
|
+
channel.input.pipe(
|
|
65
|
+
Stream.runForEach((bytes) =>
|
|
66
|
+
Effect.fromResult(decoder.decode(bytes)).pipe(
|
|
67
|
+
Effect.flatMap((frames) =>
|
|
68
|
+
Effect.forEach(
|
|
69
|
+
frames,
|
|
70
|
+
(frame) => Queue.offer(incoming[frame.channel], frame.payload),
|
|
71
|
+
{ discard: true },
|
|
72
|
+
),
|
|
73
|
+
),
|
|
74
|
+
),
|
|
75
|
+
),
|
|
76
|
+
Effect.andThen(decoder.finish),
|
|
77
|
+
Effect.andThen(Effect.fail(Framing.EndOfStream.make({}))),
|
|
78
|
+
Effect.mapError((error) =>
|
|
79
|
+
error._tag === "TransportError"
|
|
80
|
+
? error
|
|
81
|
+
: TransportError.make({ cause: Cause.fail(error) }),
|
|
82
|
+
),
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
return {
|
|
86
|
+
await: group.await,
|
|
87
|
+
fail: group.fail,
|
|
88
|
+
end: Effect.tx(
|
|
89
|
+
TxQueue.end(outgoing[0]).pipe(Effect.andThen(TxQueue.end(outgoing[1]))),
|
|
90
|
+
).pipe(Effect.andThen(Fiber.join(writing))),
|
|
91
|
+
send: (id, payload) =>
|
|
92
|
+
group.race(
|
|
93
|
+
Framing.encode({ channel: id, payload }).pipe(
|
|
94
|
+
Effect.flatMap((bytes) => TxQueue.offer(outgoing[id], bytes)),
|
|
95
|
+
Effect.asVoid,
|
|
96
|
+
),
|
|
97
|
+
),
|
|
98
|
+
take: (id) => group.race(Queue.take(incoming[id])),
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
export const fromSocket = Effect.fnUntraced(function* (socket: Socket.Socket) {
|
|
102
|
+
const reader = yield* socket.reader
|
|
103
|
+
const writer = yield* socket.writer
|
|
104
|
+
return yield* make({
|
|
105
|
+
input: Stream.fromEffectRepeat(reader.pull).pipe(
|
|
106
|
+
Stream.flattenIterable,
|
|
107
|
+
Stream.map((bytes) =>
|
|
108
|
+
typeof bytes === "string" ? new TextEncoder().encode(bytes) : bytes,
|
|
109
|
+
),
|
|
110
|
+
Stream.mapError((error) =>
|
|
111
|
+
TransportError.make({ cause: Cause.fail(error) }),
|
|
112
|
+
),
|
|
113
|
+
),
|
|
114
|
+
output: Sink.forEach((bytes: Uint8Array) => writer.write(bytes)).pipe(
|
|
115
|
+
Sink.mapError((error) =>
|
|
116
|
+
TransportError.make({ cause: Cause.fail(error) }),
|
|
117
|
+
),
|
|
118
|
+
),
|
|
119
|
+
})
|
|
120
|
+
})
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Array,
|
|
3
|
+
Cause,
|
|
4
|
+
Effect,
|
|
5
|
+
FileSystem,
|
|
6
|
+
Order,
|
|
7
|
+
Path,
|
|
8
|
+
Schema,
|
|
9
|
+
Stream,
|
|
10
|
+
String,
|
|
11
|
+
} from "effect"
|
|
12
|
+
import type { PlatformError } from "effect"
|
|
13
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/process"
|
|
14
|
+
// @effect-diagnostics-next-line nodeBuiltinImport:off -- WebCrypto has no incremental digest API; package files are hashed as streams without buffering the directory.
|
|
15
|
+
import { createHash } from "node:crypto"
|
|
16
|
+
import process from "node:process"
|
|
17
|
+
import { fileURLToPath } from "node:url"
|
|
18
|
+
import { PackageError, type CodeIdentity } from "./model"
|
|
19
|
+
|
|
20
|
+
const Manifest = Schema.Struct({ name: Schema.String, version: Schema.String })
|
|
21
|
+
export interface Expected {
|
|
22
|
+
readonly version?: string
|
|
23
|
+
readonly digest?: string
|
|
24
|
+
}
|
|
25
|
+
export const verify = (actual: CodeIdentity, expected: Expected = {}) =>
|
|
26
|
+
expected.version !== undefined && actual.version !== expected.version
|
|
27
|
+
? Effect.fail(
|
|
28
|
+
PackageError.make({
|
|
29
|
+
detail: `Package version mismatch: expected ${expected.version}, received ${actual.version}`,
|
|
30
|
+
}),
|
|
31
|
+
)
|
|
32
|
+
: expected.digest !== undefined && actual.digest !== expected.digest
|
|
33
|
+
? Effect.fail(
|
|
34
|
+
PackageError.make({
|
|
35
|
+
detail: `Package digest mismatch: expected ${expected.digest}, received ${actual.digest}`,
|
|
36
|
+
}),
|
|
37
|
+
)
|
|
38
|
+
: Effect.void
|
|
39
|
+
|
|
40
|
+
/** A path-sensitive SHA-256 of sorted entries and bytes, excluding every node_modules subtree. */
|
|
41
|
+
export const digest = Effect.fnUntraced(function* (directory: string) {
|
|
42
|
+
const fs = yield* FileSystem.FileSystem
|
|
43
|
+
const path = yield* Path.Path
|
|
44
|
+
const hash = yield* Effect.sync(() =>
|
|
45
|
+
createHash("sha256").update("graft-package-sha256-v1\0"),
|
|
46
|
+
)
|
|
47
|
+
const visit = (
|
|
48
|
+
relative: string,
|
|
49
|
+
): Effect.Effect<void, PackageError | PlatformError.PlatformError> =>
|
|
50
|
+
Effect.gen(function* () {
|
|
51
|
+
const filename = path.join(directory, relative)
|
|
52
|
+
const info = yield* fs.stat(filename)
|
|
53
|
+
if (info.type !== "File" && info.type !== "Directory")
|
|
54
|
+
return yield* PackageError.make({
|
|
55
|
+
detail: `Unsupported package entry: ${relative}`,
|
|
56
|
+
})
|
|
57
|
+
const name = new TextEncoder().encode(relative)
|
|
58
|
+
yield* Effect.sync(() => {
|
|
59
|
+
hash.update(
|
|
60
|
+
`${info.type}:${name.byteLength}:${info.type === "File" ? info.size : 0}:`,
|
|
61
|
+
)
|
|
62
|
+
hash.update(name)
|
|
63
|
+
})
|
|
64
|
+
if (info.type === "File") {
|
|
65
|
+
yield* fs.stream(filename).pipe(
|
|
66
|
+
Stream.runForEach((bytes) =>
|
|
67
|
+
Effect.sync(() => {
|
|
68
|
+
hash.update(bytes)
|
|
69
|
+
}),
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
} else {
|
|
73
|
+
const entries = yield* fs.readDirectory(filename)
|
|
74
|
+
for (const entry of Array.sort(entries, Order.String)) {
|
|
75
|
+
if (entry === "node_modules") continue
|
|
76
|
+
yield* visit(relative === "" ? entry : `${relative}/${entry}`)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
yield* visit("")
|
|
81
|
+
return yield* Effect.sync(() => hash.digest("hex"))
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Resolve using Node's import conditions without importing executable package code.
|
|
86
|
+
* The helper's cwd is the installation root and it inherits no NODE_OPTIONS/loaders.
|
|
87
|
+
* The installation directory and its package exports are trusted.
|
|
88
|
+
*/
|
|
89
|
+
export const resolve = Effect.fnUntraced(
|
|
90
|
+
function* (root: string, specifier: string, includeDigest: boolean = false) {
|
|
91
|
+
const segments = String.split(specifier, "/")
|
|
92
|
+
const scoped = String.startsWith("@")(specifier)
|
|
93
|
+
const name = Array.join(Array.take(segments, scoped ? 2 : 1), "/")
|
|
94
|
+
const fs = yield* FileSystem.FileSystem
|
|
95
|
+
const path = yield* Path.Path
|
|
96
|
+
const installation = yield* fs.realPath(root)
|
|
97
|
+
const directory = yield* fs.realPath(
|
|
98
|
+
path.join(installation, "node_modules", name),
|
|
99
|
+
)
|
|
100
|
+
const manifest = yield* fs
|
|
101
|
+
.readFileString(path.join(directory, "package.json"))
|
|
102
|
+
.pipe(
|
|
103
|
+
Effect.flatMap(Schema.decodeEffect(Schema.fromJsonString(Manifest))),
|
|
104
|
+
)
|
|
105
|
+
const resolved = yield* resolveEntry(installation, specifier)
|
|
106
|
+
const filename = yield* Effect.try({
|
|
107
|
+
try: () => fileURLToPath(resolved),
|
|
108
|
+
catch: (error) =>
|
|
109
|
+
PackageError.make({
|
|
110
|
+
detail: "The package export is not a file URL",
|
|
111
|
+
cause: Cause.die(error),
|
|
112
|
+
}),
|
|
113
|
+
})
|
|
114
|
+
const entry = yield* fs.realPath(filename)
|
|
115
|
+
const identity: CodeIdentity = {
|
|
116
|
+
...manifest,
|
|
117
|
+
...(includeDigest ? { digest: yield* digest(directory) } : {}),
|
|
118
|
+
}
|
|
119
|
+
return { directory, entry, identity }
|
|
120
|
+
},
|
|
121
|
+
Effect.catchTags({
|
|
122
|
+
PlatformError: (error) =>
|
|
123
|
+
Effect.fail(
|
|
124
|
+
PackageError.make({
|
|
125
|
+
detail: "Reading the installed package failed",
|
|
126
|
+
cause: Cause.fail(error),
|
|
127
|
+
}),
|
|
128
|
+
),
|
|
129
|
+
SchemaError: (error) =>
|
|
130
|
+
Effect.fail(
|
|
131
|
+
PackageError.make({
|
|
132
|
+
detail: "Reading the package manifest failed",
|
|
133
|
+
cause: Cause.fail(error),
|
|
134
|
+
}),
|
|
135
|
+
),
|
|
136
|
+
}),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
export const resolveEntry = (installation: string, specifier: string) =>
|
|
140
|
+
Effect.gen(function* () {
|
|
141
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
|
142
|
+
return yield* Effect.scoped(
|
|
143
|
+
Effect.gen(function* () {
|
|
144
|
+
const child = yield* spawner.spawn(
|
|
145
|
+
ChildProcess.make(
|
|
146
|
+
process.execPath,
|
|
147
|
+
[
|
|
148
|
+
"--input-type=module",
|
|
149
|
+
"--eval",
|
|
150
|
+
"process.stdout.write(import.meta.resolve(process.argv[1]))",
|
|
151
|
+
"--",
|
|
152
|
+
specifier,
|
|
153
|
+
],
|
|
154
|
+
{
|
|
155
|
+
cwd: installation,
|
|
156
|
+
env: {},
|
|
157
|
+
stdin: "ignore",
|
|
158
|
+
stdout: "pipe",
|
|
159
|
+
stderr: "pipe",
|
|
160
|
+
},
|
|
161
|
+
),
|
|
162
|
+
)
|
|
163
|
+
const [code, stdout, stderr] = yield* Effect.all(
|
|
164
|
+
[
|
|
165
|
+
child.exitCode,
|
|
166
|
+
child.stdout.pipe(Stream.decodeText(), Stream.mkString),
|
|
167
|
+
child.stderr.pipe(Stream.decodeText(), Stream.mkString),
|
|
168
|
+
],
|
|
169
|
+
{ concurrency: "unbounded" },
|
|
170
|
+
)
|
|
171
|
+
if (code !== 0)
|
|
172
|
+
return yield* PackageError.make({
|
|
173
|
+
detail: `Package resolution failed: ${stderr}`,
|
|
174
|
+
})
|
|
175
|
+
return stdout
|
|
176
|
+
}),
|
|
177
|
+
)
|
|
178
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Array,
|
|
3
|
+
Cause,
|
|
4
|
+
Effect,
|
|
5
|
+
Exit,
|
|
6
|
+
Fiber,
|
|
7
|
+
Ref,
|
|
8
|
+
Schedule,
|
|
9
|
+
Stream,
|
|
10
|
+
String,
|
|
11
|
+
} from "effect"
|
|
12
|
+
import type { PlatformError } from "effect"
|
|
13
|
+
import type { ChildProcessSpawner } from "effect/process"
|
|
14
|
+
import * as FailureGroup from "../FailureGroup"
|
|
15
|
+
|
|
16
|
+
/** Time allowed for a direct execution unit or launcher to exit after input EOF. */
|
|
17
|
+
export const stdinExitTimeout = "1 second"
|
|
18
|
+
|
|
19
|
+
/** Readers outlive the failure monitor so forced termination still captures its diagnostic tail. */
|
|
20
|
+
export const observe = Effect.fnUntraced(function* (
|
|
21
|
+
child: ChildProcessSpawner.ChildProcessHandle,
|
|
22
|
+
options: { readonly drainStdout?: boolean } = {},
|
|
23
|
+
) {
|
|
24
|
+
const failures = yield* FailureGroup.make<PlatformError.PlatformError>()
|
|
25
|
+
const tail = yield* Ref.make("")
|
|
26
|
+
// Command transports own stdout as a protocol stream.
|
|
27
|
+
const stdout = yield* (
|
|
28
|
+
options.drainStdout === false ? Effect.void : Stream.runDrain(child.stdout)
|
|
29
|
+
).pipe(Effect.forkScoped)
|
|
30
|
+
const stderr = yield* child.stderr.pipe(
|
|
31
|
+
Stream.decodeText(),
|
|
32
|
+
Stream.runForEach((text) =>
|
|
33
|
+
Ref.update(tail, (old) => String.takeRight(old + text, 8192)),
|
|
34
|
+
),
|
|
35
|
+
Effect.forkScoped,
|
|
36
|
+
)
|
|
37
|
+
yield* failures.run(Fiber.join(stdout))
|
|
38
|
+
yield* failures.run(Fiber.join(stderr))
|
|
39
|
+
const exited = yield* Effect.gen(function* () {
|
|
40
|
+
const result = yield* Effect.exit(child.exitCode)
|
|
41
|
+
// exitCode can fail before exit on an input error; isRunning follows Node's exit event.
|
|
42
|
+
yield* child.isRunning.pipe(
|
|
43
|
+
Effect.repeat({
|
|
44
|
+
schedule: Schedule.spaced("25 millis"),
|
|
45
|
+
while: (running) => running,
|
|
46
|
+
}),
|
|
47
|
+
)
|
|
48
|
+
const err = yield* Fiber.await(stderr)
|
|
49
|
+
const out = yield* Fiber.await(stdout)
|
|
50
|
+
return {
|
|
51
|
+
code: Exit.isSuccess(result) ? result.value : null,
|
|
52
|
+
signal: null,
|
|
53
|
+
stderr: yield* Ref.get(tail),
|
|
54
|
+
cause: Array.reduce(
|
|
55
|
+
[Exit.asVoid(result), err, out],
|
|
56
|
+
Cause.empty,
|
|
57
|
+
(cause: Cause.Cause<PlatformError.PlatformError>, exit) =>
|
|
58
|
+
Exit.isFailure(exit) ? Cause.combine(cause, exit.cause) : cause,
|
|
59
|
+
),
|
|
60
|
+
}
|
|
61
|
+
}).pipe(Effect.forkScoped)
|
|
62
|
+
return { exited: Fiber.join(exited), failure: failures.await }
|
|
63
|
+
})
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Cause, Effect, Option, Schedule, Stream, String } from "effect"
|
|
2
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/process"
|
|
3
|
+
import process from "node:process"
|
|
4
|
+
import {
|
|
5
|
+
ExecutionError,
|
|
6
|
+
TerminationUnconfirmed,
|
|
7
|
+
type ProcessIdentity,
|
|
8
|
+
type Termination,
|
|
9
|
+
} from "./model"
|
|
10
|
+
|
|
11
|
+
const inspect = (pid: number) =>
|
|
12
|
+
Effect.scoped(
|
|
13
|
+
Effect.gen(function* () {
|
|
14
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
|
15
|
+
const child = yield* spawner.spawn(
|
|
16
|
+
ChildProcess.make(
|
|
17
|
+
"ps",
|
|
18
|
+
["-p", `${pid}`, "-o", "lstart=", "-o", "stat="],
|
|
19
|
+
{
|
|
20
|
+
env: { ...process.env, LC_ALL: "C" },
|
|
21
|
+
stdin: "ignore",
|
|
22
|
+
},
|
|
23
|
+
),
|
|
24
|
+
)
|
|
25
|
+
const [code, output, stderr] = yield* Effect.all(
|
|
26
|
+
[
|
|
27
|
+
child.exitCode,
|
|
28
|
+
child.stdout.pipe(Stream.decodeText(), Stream.mkString),
|
|
29
|
+
child.stderr.pipe(Stream.decodeText(), Stream.mkString),
|
|
30
|
+
],
|
|
31
|
+
{ concurrency: "unbounded" },
|
|
32
|
+
)
|
|
33
|
+
if (code === 1 && String.trim(output) === "") return Option.none()
|
|
34
|
+
const [, born, status] =
|
|
35
|
+
/^(.{24})\s+(\S+)$/m.exec(String.trim(output)) ?? []
|
|
36
|
+
if (code !== 0 || born === undefined || status === undefined)
|
|
37
|
+
return yield* ExecutionError.make({
|
|
38
|
+
detail: "Cannot inspect process identity: " + stderr,
|
|
39
|
+
})
|
|
40
|
+
if (String.startsWith("Z")(status)) return Option.none()
|
|
41
|
+
return Option.some({ born })
|
|
42
|
+
}),
|
|
43
|
+
).pipe(
|
|
44
|
+
Effect.mapError((error) =>
|
|
45
|
+
error._tag === "ExecutionError"
|
|
46
|
+
? error
|
|
47
|
+
: ExecutionError.make({
|
|
48
|
+
detail: "Inspecting process identity failed",
|
|
49
|
+
cause: Cause.fail(error),
|
|
50
|
+
}),
|
|
51
|
+
),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
export const identity = (pid: number) =>
|
|
55
|
+
Effect.gen(function* () {
|
|
56
|
+
const current = yield* inspect(pid)
|
|
57
|
+
if (Option.isNone(current))
|
|
58
|
+
return yield* ExecutionError.make({
|
|
59
|
+
detail: "The child exited before its identity could be recorded",
|
|
60
|
+
})
|
|
61
|
+
return { pid, start: current.value }
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// The OS may discard argv before reporting a zombie; PID and start time remain the identity.
|
|
65
|
+
export const matches = (child: ProcessIdentity) =>
|
|
66
|
+
Effect.gen(function* () {
|
|
67
|
+
const current = yield* inspect(child.pid)
|
|
68
|
+
if (Option.isNone(current) || current.value.born !== child.start.born)
|
|
69
|
+
return false
|
|
70
|
+
return true
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
export const recover = (child: ProcessIdentity) =>
|
|
74
|
+
Effect.gen(function* () {
|
|
75
|
+
if (!(yield* matches(child)))
|
|
76
|
+
return {
|
|
77
|
+
kind: "Exited",
|
|
78
|
+
exit: { code: null, signal: null },
|
|
79
|
+
cause: Cause.empty,
|
|
80
|
+
} satisfies Termination
|
|
81
|
+
// The PID can still be reused between this final check and kill; an external supervisor covers descendants.
|
|
82
|
+
const sent = yield* Effect.exit(
|
|
83
|
+
Effect.try({
|
|
84
|
+
try: () => process.kill(child.pid, "SIGKILL"),
|
|
85
|
+
catch: (cause) =>
|
|
86
|
+
ExecutionError.make({
|
|
87
|
+
detail: "Signaling the orphan failed",
|
|
88
|
+
cause: Cause.die(cause),
|
|
89
|
+
}),
|
|
90
|
+
}),
|
|
91
|
+
)
|
|
92
|
+
const confirmed = yield* matches(child).pipe(
|
|
93
|
+
Effect.repeat({
|
|
94
|
+
schedule: Schedule.spaced("50 millis"),
|
|
95
|
+
while: (value) => value,
|
|
96
|
+
}),
|
|
97
|
+
Effect.timeoutOption("10 seconds"),
|
|
98
|
+
)
|
|
99
|
+
if (Option.isNone(confirmed))
|
|
100
|
+
return yield* TerminationUnconfirmed.make({
|
|
101
|
+
detail: "The orphan did not exit after recovery",
|
|
102
|
+
cause: sent._tag === "Failure" ? sent.cause : Cause.empty,
|
|
103
|
+
})
|
|
104
|
+
return {
|
|
105
|
+
kind: "Killed",
|
|
106
|
+
exit: { code: null, signal: "SIGKILL" },
|
|
107
|
+
cause: sent._tag === "Failure" ? sent.cause : Cause.empty,
|
|
108
|
+
} satisfies Termination
|
|
109
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Array, HashMap, Option } from "effect"
|
|
2
|
+
import { keyOf, type SessionKey, type SessionRecord } from "./model"
|
|
3
|
+
|
|
4
|
+
/** A terminal high-water record fences every older Open, even after its request id is forgotten. */
|
|
5
|
+
export const retain = (
|
|
6
|
+
records: ReadonlyArray<SessionRecord>,
|
|
7
|
+
): ReadonlyArray<SessionRecord> => {
|
|
8
|
+
const highWater = Array.reduce(
|
|
9
|
+
records,
|
|
10
|
+
HashMap.empty<SessionKey, number>(),
|
|
11
|
+
(water, record) =>
|
|
12
|
+
HashMap.set(
|
|
13
|
+
water,
|
|
14
|
+
keyOf(record.handle.key),
|
|
15
|
+
Math.max(
|
|
16
|
+
Option.getOrElse(
|
|
17
|
+
HashMap.get(water, keyOf(record.handle.key)),
|
|
18
|
+
() => 0,
|
|
19
|
+
),
|
|
20
|
+
record.handle.generation,
|
|
21
|
+
),
|
|
22
|
+
),
|
|
23
|
+
)
|
|
24
|
+
return Array.filter(
|
|
25
|
+
records,
|
|
26
|
+
(record) =>
|
|
27
|
+
record.phase !== "Stopped" ||
|
|
28
|
+
record.handle.generation ===
|
|
29
|
+
Option.getOrUndefined(HashMap.get(highWater, keyOf(record.handle.key))),
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
/** Apply the same retention rule to both in-memory stores. */
|
|
33
|
+
export const save = (
|
|
34
|
+
records: HashMap.HashMap<string, SessionRecord>,
|
|
35
|
+
record: SessionRecord,
|
|
36
|
+
) =>
|
|
37
|
+
HashMap.fromIterable(
|
|
38
|
+
Array.map(
|
|
39
|
+
retain(
|
|
40
|
+
Array.fromIterable(
|
|
41
|
+
HashMap.values(HashMap.set(records, record.handle.token, record)),
|
|
42
|
+
),
|
|
43
|
+
),
|
|
44
|
+
(record): readonly [string, SessionRecord] => [
|
|
45
|
+
record.handle.token,
|
|
46
|
+
record,
|
|
47
|
+
],
|
|
48
|
+
),
|
|
49
|
+
)
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Array,
|
|
3
|
+
Cause,
|
|
4
|
+
Layer,
|
|
5
|
+
Effect,
|
|
6
|
+
FileSystem,
|
|
7
|
+
HashMap,
|
|
8
|
+
HashSet,
|
|
9
|
+
Path,
|
|
10
|
+
Ref,
|
|
11
|
+
Schema,
|
|
12
|
+
Semaphore,
|
|
13
|
+
String,
|
|
14
|
+
type PlatformError,
|
|
15
|
+
} from "effect"
|
|
16
|
+
import { Digest, SecureRandom } from "effect/crypto"
|
|
17
|
+
import { Key, SessionRecord, Store, StateStorageError } from "./model"
|
|
18
|
+
import * as Retention from "./Retention"
|
|
19
|
+
|
|
20
|
+
/** Injectable store used by deterministic supervisor tests. */
|
|
21
|
+
export const memory = Effect.gen(function* () {
|
|
22
|
+
const records = yield* Ref.make(HashMap.empty<string, SessionRecord>())
|
|
23
|
+
return Store.of({
|
|
24
|
+
list: Ref.get(records).pipe(
|
|
25
|
+
Effect.map((records) => Array.fromIterable(HashMap.values(records))),
|
|
26
|
+
),
|
|
27
|
+
save: (record) => Ref.update(records, (all) => Retention.save(all, record)),
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Atomically replaced records retain unfinished sessions and each key's high-water mark.
|
|
33
|
+
* Older terminal request ids expire; Open fences their retries with expectedGeneration.
|
|
34
|
+
* Requires exclusive directory ownership; it is not a multi-writer database.
|
|
35
|
+
*/
|
|
36
|
+
export const directory = (root: string) =>
|
|
37
|
+
Effect.gen(function* () {
|
|
38
|
+
const fs = yield* FileSystem.FileSystem
|
|
39
|
+
const path = yield* Path.Path
|
|
40
|
+
const digest = yield* Digest.Digest
|
|
41
|
+
const json = Schema.fromJsonString(Schema.toCodecJson(SessionRecord))
|
|
42
|
+
const random = yield* SecureRandom.SecureRandom
|
|
43
|
+
const storage = <A, R>(
|
|
44
|
+
effect: Effect.Effect<
|
|
45
|
+
A,
|
|
46
|
+
PlatformError.PlatformError | Schema.SchemaError | StateStorageError,
|
|
47
|
+
R
|
|
48
|
+
>,
|
|
49
|
+
) =>
|
|
50
|
+
effect.pipe(
|
|
51
|
+
Effect.mapError((error) =>
|
|
52
|
+
error._tag === "StateStorageError"
|
|
53
|
+
? error
|
|
54
|
+
: StateStorageError.make({
|
|
55
|
+
detail: "Runner state storage failed",
|
|
56
|
+
cause: Cause.fail(error),
|
|
57
|
+
}),
|
|
58
|
+
),
|
|
59
|
+
)
|
|
60
|
+
yield* storage(
|
|
61
|
+
fs.makeDirectory(root, { recursive: true }).pipe(Effect.uninterruptible),
|
|
62
|
+
)
|
|
63
|
+
const mutex = yield* Semaphore.make(1)
|
|
64
|
+
const read = Effect.gen(function* () {
|
|
65
|
+
const files = yield* fs.readDirectory(root)
|
|
66
|
+
const rows = yield* Effect.forEach(
|
|
67
|
+
Array.filter(files, String.endsWith(".json")),
|
|
68
|
+
(file) =>
|
|
69
|
+
fs.readFileString(path.join(root, file)).pipe(
|
|
70
|
+
Effect.flatMap(Schema.decodeEffect(json)),
|
|
71
|
+
Effect.map((record) => ({ file, record })),
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
return rows
|
|
75
|
+
})
|
|
76
|
+
const compact = Effect.scoped(
|
|
77
|
+
Effect.gen(function* () {
|
|
78
|
+
const rows = yield* read
|
|
79
|
+
const retained = Retention.retain(Array.map(rows, (row) => row.record))
|
|
80
|
+
const tokens = HashSet.fromIterable(
|
|
81
|
+
Array.map(retained, (record) => record.handle.token),
|
|
82
|
+
)
|
|
83
|
+
const obsolete = Array.filter(
|
|
84
|
+
rows,
|
|
85
|
+
(row) => !HashSet.has(tokens, row.record.handle.token),
|
|
86
|
+
)
|
|
87
|
+
yield* Effect.forEach(
|
|
88
|
+
obsolete,
|
|
89
|
+
(row) =>
|
|
90
|
+
fs.remove(path.join(root, row.file)).pipe(Effect.uninterruptible),
|
|
91
|
+
{ discard: true },
|
|
92
|
+
)
|
|
93
|
+
if (obsolete.length > 0) {
|
|
94
|
+
const directory = yield* fs.open(root, { flag: "r" })
|
|
95
|
+
yield* directory.sync.pipe(Effect.uninterruptible)
|
|
96
|
+
}
|
|
97
|
+
}),
|
|
98
|
+
)
|
|
99
|
+
yield* storage(compact)
|
|
100
|
+
return Store.of({
|
|
101
|
+
list: storage(
|
|
102
|
+
mutex.withPermit(
|
|
103
|
+
read.pipe(Effect.map((rows) => Array.map(rows, (row) => row.record))),
|
|
104
|
+
),
|
|
105
|
+
),
|
|
106
|
+
save: (record) =>
|
|
107
|
+
storage(
|
|
108
|
+
mutex.withPermit(
|
|
109
|
+
Effect.scoped(
|
|
110
|
+
Effect.gen(function* () {
|
|
111
|
+
const encoded = yield* Schema.encodeEffect(json)(record)
|
|
112
|
+
const address = yield* Schema.encodeEffect(
|
|
113
|
+
Schema.fromJsonString(
|
|
114
|
+
Schema.Struct({ key: Key, generation: Schema.Int }),
|
|
115
|
+
),
|
|
116
|
+
)({
|
|
117
|
+
key: record.handle.key,
|
|
118
|
+
generation: record.handle.generation,
|
|
119
|
+
})
|
|
120
|
+
const name = yield* digest.digestHex(
|
|
121
|
+
Digest.sha256,
|
|
122
|
+
new TextEncoder().encode(address),
|
|
123
|
+
)
|
|
124
|
+
const nonce = yield* random.uuidV4()
|
|
125
|
+
const temporary = path.join(root, `.${name}.${nonce}.tmp`)
|
|
126
|
+
const destination = path.join(root, name + ".json")
|
|
127
|
+
yield* Effect.acquireRelease(
|
|
128
|
+
fs.open(temporary, { flag: "wx", mode: 0o600 }),
|
|
129
|
+
() =>
|
|
130
|
+
fs.remove(temporary, { force: true }).pipe(Effect.orDie),
|
|
131
|
+
).pipe(
|
|
132
|
+
Effect.flatMap((file) =>
|
|
133
|
+
file
|
|
134
|
+
.writeAll(new TextEncoder().encode(encoded))
|
|
135
|
+
.pipe(
|
|
136
|
+
Effect.uninterruptible,
|
|
137
|
+
Effect.andThen(file.sync.pipe(Effect.uninterruptible)),
|
|
138
|
+
),
|
|
139
|
+
),
|
|
140
|
+
)
|
|
141
|
+
// Keep rename and directory synchronization together if the caller is canceled.
|
|
142
|
+
yield* Effect.uninterruptible(
|
|
143
|
+
Effect.gen(function* () {
|
|
144
|
+
yield* fs.rename(temporary, destination)
|
|
145
|
+
const directory = yield* fs.open(root, { flag: "r" })
|
|
146
|
+
yield* directory.sync.pipe(Effect.uninterruptible)
|
|
147
|
+
}),
|
|
148
|
+
)
|
|
149
|
+
// The new high-water file is durable before any old record is removed.
|
|
150
|
+
yield* compact
|
|
151
|
+
}),
|
|
152
|
+
),
|
|
153
|
+
),
|
|
154
|
+
),
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
export const layerMemory = Layer.effect(Store, memory)
|
|
159
|
+
export const layerDirectory = (root: string) =>
|
|
160
|
+
Layer.effect(Store, directory(root))
|