@graftland/plugin-remote 0.3.1 → 0.3.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.
Files changed (147) hide show
  1. package/dist/Auth.d.ts +2 -2
  2. package/dist/Auth.d.ts.map +1 -1
  3. package/dist/Auth.js.map +1 -1
  4. package/dist/{Bootstrap-Dr6sX7el.js → Bootstrap-H9gDsgsC.js} +2 -2
  5. package/dist/{Contract-DL2mHvaT.js → Contract-DsJtisvL.js} +2 -2
  6. package/dist/Contract.js +1 -1
  7. package/dist/{Errors-BMKmu5Y7.js → Errors-DYhsGZcZ.js} +3 -1
  8. package/dist/Errors.d.ts +7 -0
  9. package/dist/Errors.d.ts.map +1 -1
  10. package/dist/Errors.js +3 -1
  11. package/dist/Errors.js.map +1 -1
  12. package/dist/{FailureGroup-BcH0XOBi.js → FailureGroup-BgT73AaR.js} +14 -4
  13. package/dist/{Framing-zaYdOJnT.js → Framing-G9eSaeLb.js} +1 -1
  14. package/dist/{Graft-CVyueNOf.js → Graft-B71CPA2Y.js} +20 -16
  15. package/dist/{Packages-JNL34JpS.js → Packages-AgoCGotL.js} +23 -26
  16. package/dist/{Peer-BT7eAGmh.js → Peer-DXeVrGBh.js} +5 -5
  17. package/dist/{ProcessObservation-DEGrEMfP.js → ProcessObservation-gLgGirYv.js} +1 -1
  18. package/dist/{ServeLifecycle-CJM0gG5B.js → ServeLifecycle-Bd1FPJ6e.js} +1 -1
  19. package/dist/Transport.d.ts +2 -2
  20. package/dist/Transport.d.ts.map +1 -1
  21. package/dist/Transport.js.map +1 -1
  22. package/dist/bin.js +48 -47
  23. package/dist/bin.js.map +1 -1
  24. package/dist/{childRuntime-An0HsIzl.js → childRuntime-Bm3iSg_N.js} +29 -26
  25. package/dist/{connectionEnded-DCpFCjMX.js → connectionEnded-BwcxQ7W8.js} +4 -4
  26. package/dist/{entry-B89sepMU.js → entry-psOpX3Bj.js} +72 -35
  27. package/dist/host/auth/bearer.js +1 -1
  28. package/dist/host/auth/jwt.js +1 -1
  29. package/dist/host/auth/mtls.js +2 -2
  30. package/dist/host/auth/none.js +1 -1
  31. package/dist/host/command.js +4 -4
  32. package/dist/host/local.js +3 -3
  33. package/dist/host/unix.js +3 -3
  34. package/dist/host/websocket.js +3 -3
  35. package/dist/host/worker.js +4 -5
  36. package/dist/{importer-Bk6U_sn4.js → importer-Cvf88eAY.js} +5 -8
  37. package/dist/index.js +9 -9
  38. package/dist/index.js.map +1 -1
  39. package/dist/internal/Certificates.js +4 -4
  40. package/dist/internal/Certificates.js.map +1 -1
  41. package/dist/internal/FailureGroup.js +3 -2
  42. package/dist/internal/FailureGroup.js.map +1 -1
  43. package/dist/internal/assembly.js +9 -2
  44. package/dist/internal/assembly.js.map +1 -1
  45. package/dist/internal/auth/mtls.js +1 -1
  46. package/dist/internal/auth/mtls.js.map +1 -1
  47. package/dist/internal/child.js +20 -17
  48. package/dist/internal/child.js.map +1 -1
  49. package/dist/internal/childRuntime.js +4 -4
  50. package/dist/internal/childRuntime.js.map +1 -1
  51. package/dist/internal/directTransport.d.ts +1 -1
  52. package/dist/internal/entry.d.ts.map +1 -1
  53. package/dist/internal/entry.js +1 -1
  54. package/dist/internal/entry.js.map +1 -1
  55. package/dist/internal/execution.js +40 -5
  56. package/dist/internal/execution.js.map +1 -1
  57. package/dist/internal/host/runner.js +7 -6
  58. package/dist/internal/host/runner.js.map +1 -1
  59. package/dist/internal/host.js +5 -5
  60. package/dist/internal/host.js.map +1 -1
  61. package/dist/internal/port.js +5 -5
  62. package/dist/internal/port.js.map +1 -1
  63. package/dist/internal/release.js +1 -1
  64. package/dist/internal/release.js.map +1 -1
  65. package/dist/internal/runner/Connection.d.ts +1 -1
  66. package/dist/internal/runner/Connection.js +4 -4
  67. package/dist/internal/runner/Connection.js.map +1 -1
  68. package/dist/internal/runner/Execution.js +3 -3
  69. package/dist/internal/runner/Execution.js.map +1 -1
  70. package/dist/internal/runner/Framing.js +1 -1
  71. package/dist/internal/runner/Framing.js.map +1 -1
  72. package/dist/internal/runner/Local.js +1 -1
  73. package/dist/internal/runner/Local.js.map +1 -1
  74. package/dist/internal/runner/Lock.js +1 -1
  75. package/dist/internal/runner/Lock.js.map +1 -1
  76. package/dist/internal/runner/Packages.d.ts +1 -1
  77. package/dist/internal/runner/Packages.js +22 -25
  78. package/dist/internal/runner/Packages.js.map +1 -1
  79. package/dist/internal/runner/Processes.js +7 -9
  80. package/dist/internal/runner/Processes.js.map +1 -1
  81. package/dist/internal/runner/Store.js +3 -3
  82. package/dist/internal/runner/Store.js.map +1 -1
  83. package/dist/internal/runner/Supervisor.js +46 -47
  84. package/dist/internal/runner/Supervisor.js.map +1 -1
  85. package/dist/internal/runner/accept.js +3 -3
  86. package/dist/internal/runner/accept.js.map +1 -1
  87. package/dist/internal/runner/model.js +1 -1
  88. package/dist/internal/runner/model.js.map +1 -1
  89. package/dist/internal/serve.js +1 -1
  90. package/dist/internal/serve.js.map +1 -1
  91. package/dist/internal/supervisorPlugin.js +1 -1
  92. package/dist/internal/watchdogMain.js +5 -5
  93. package/dist/internal/watchdogMain.js.map +1 -1
  94. package/dist/internal/websocketServer.js +3 -1
  95. package/dist/internal/websocketServer.js.map +1 -1
  96. package/dist/internal/workerMain.js +3 -3
  97. package/dist/{port-DSVUY8Bq.js → port--3x2zJ4D.js} +8 -8
  98. package/dist/{release-BY4Mlluy.js → release-DfZPuIcK.js} +1 -1
  99. package/dist/runner/auth/bearer.js +2 -2
  100. package/dist/runner/auth/jwt.js +4 -4
  101. package/dist/runner/auth/jwt.js.map +1 -1
  102. package/dist/runner/auth/mtls.js +3 -3
  103. package/dist/runner/auth/none.js +1 -1
  104. package/dist/runner/index.js +5 -5
  105. package/dist/runner/stdio.js +3 -3
  106. package/dist/runner/unix.js +3 -3
  107. package/dist/runner/websocket.js +4 -4
  108. package/dist/{socketChannel-C_ppdSzz.js → socketChannel-CJvL2DiB.js} +2 -2
  109. package/dist/{supervisorPlugin-CivhM7x5.js → supervisorPlugin-t6Z6PrhD.js} +72 -76
  110. package/dist/{token-Cc_1CTbE.js → token-CYBNNlhS.js} +1 -1
  111. package/dist/{tokenCredentials-CI6mMRRd.js → tokenCredentials-CbCuk-Rt.js} +3 -3
  112. package/dist/{websocketServer-D2Hk7wsA.js → websocketServer-NQhODlEt.js} +5 -3
  113. package/dist/{workerChannel-cmLc13vm.js → workerChannel-C8hhBJzA.js} +1 -1
  114. package/package.json +3 -3
  115. package/src/Auth.ts +9 -2
  116. package/src/Errors.ts +8 -0
  117. package/src/Transport.ts +6 -2
  118. package/src/bin.ts +28 -30
  119. package/src/index.ts +5 -3
  120. package/src/internal/Certificates.ts +10 -13
  121. package/src/internal/FailureGroup.ts +128 -121
  122. package/src/internal/assembly.ts +19 -10
  123. package/src/internal/auth/mtls.ts +19 -20
  124. package/src/internal/child.ts +248 -239
  125. package/src/internal/childRuntime.ts +81 -80
  126. package/src/internal/entry.ts +10 -11
  127. package/src/internal/execution.ts +69 -12
  128. package/src/internal/host/runner.ts +153 -153
  129. package/src/internal/host.ts +116 -120
  130. package/src/internal/port.ts +100 -103
  131. package/src/internal/release.ts +3 -2
  132. package/src/internal/runner/Connection.ts +52 -60
  133. package/src/internal/runner/Execution.ts +163 -164
  134. package/src/internal/runner/Framing.ts +9 -10
  135. package/src/internal/runner/Local.ts +26 -27
  136. package/src/internal/runner/Lock.ts +23 -25
  137. package/src/internal/runner/Packages.ts +67 -70
  138. package/src/internal/runner/Processes.ts +89 -92
  139. package/src/internal/runner/Store.ts +110 -114
  140. package/src/internal/runner/Supervisor.ts +392 -405
  141. package/src/internal/runner/accept.ts +76 -84
  142. package/src/internal/runner/model.ts +20 -21
  143. package/src/internal/serve.ts +15 -13
  144. package/src/internal/watchdogMain.ts +16 -11
  145. package/src/internal/websocketServer.ts +18 -3
  146. package/src/runner/auth/jwt.ts +10 -6
  147. package/dist/cleanup-CXLa1Wd9.js +0 -13
@@ -8,102 +8,99 @@ import {
8
8
  type Termination,
9
9
  } from "./model"
10
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))
11
+ const inspect = Effect.fnUntraced(
12
+ function* (pid: number) {
13
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
14
+ const child = yield* spawner.spawn(
15
+ ChildProcess.make(
16
+ "ps",
17
+ ["-p", `${pid}`, "-o", "lstart=", "-o", "stat="],
18
+ {
19
+ env: { LC_ALL: "C" },
20
+ extendEnv: true,
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)
58
37
  return yield* ExecutionError.make({
59
- detail: "The child exited before its identity could be recorded",
38
+ detail: "Cannot inspect process identity: " + stderr,
60
39
  })
61
- return { pid, start: current.value }
62
- })
40
+ if (String.startsWith("Z")(status)) return Option.none()
41
+ return Option.some({ born })
42
+ },
43
+ Effect.scoped,
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 = Effect.fnUntraced(function* (pid: number) {
55
+ const current = yield* inspect(pid)
56
+ if (Option.isNone(current))
57
+ return yield* ExecutionError.make({
58
+ detail: "The child exited before its identity could be recorded",
59
+ })
60
+ return { pid, start: current.value }
61
+ })
63
62
 
64
63
  // 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
- })
64
+ export const matches = Effect.fnUntraced(function* (child: ProcessIdentity) {
65
+ const current = yield* inspect(child.pid)
66
+ if (Option.isNone(current) || current.value.born !== child.start.born)
67
+ return false
68
+ return true
69
+ })
72
70
 
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
- })
71
+ export const recover = Effect.fnUntraced(function* (child: ProcessIdentity) {
72
+ if (!(yield* matches(child)))
104
73
  return {
105
- kind: "Killed",
106
- exit: { code: null, signal: "SIGKILL" },
107
- cause: sent._tag === "Failure" ? sent.cause : Cause.empty,
74
+ kind: "Exited",
75
+ exit: { code: null, signal: null },
76
+ cause: Cause.empty,
108
77
  } satisfies Termination
109
- })
78
+ // The PID can still be reused between this final check and kill; an external supervisor covers descendants.
79
+ const sent = yield* Effect.exit(
80
+ Effect.try({
81
+ try: () => process.kill(child.pid, "SIGKILL"),
82
+ catch: (cause) =>
83
+ ExecutionError.make({
84
+ detail: "Signaling the orphan failed",
85
+ cause: Cause.die(cause),
86
+ }),
87
+ }),
88
+ )
89
+ const confirmed = yield* matches(child).pipe(
90
+ Effect.repeat({
91
+ schedule: Schedule.spaced("50 millis"),
92
+ while: (value) => value,
93
+ }),
94
+ Effect.timeoutOption("10 seconds"),
95
+ )
96
+ if (Option.isNone(confirmed))
97
+ return yield* TerminationUnconfirmed.make({
98
+ detail: "The orphan did not exit after recovery",
99
+ cause: sent._tag === "Failure" ? sent.cause : Cause.empty,
100
+ })
101
+ return {
102
+ kind: "Killed",
103
+ exit: { code: null, signal: "SIGKILL" },
104
+ cause: sent._tag === "Failure" ? sent.cause : Cause.empty,
105
+ } satisfies Termination
106
+ })
@@ -33,127 +33,123 @@ export const memory = Effect.gen(function* () {
33
33
  * Older terminal request ids expire; Open fences their retries with expectedGeneration.
34
34
  * Requires exclusive directory ownership; it is not a multi-writer database.
35
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
- }),
36
+ export const directory = Effect.fnUntraced(function* (root: string) {
37
+ const fs = yield* FileSystem.FileSystem
38
+ const path = yield* Path.Path
39
+ const digest = yield* Digest.Digest
40
+ const json = Schema.fromJsonString(Schema.toCodecJson(SessionRecord))
41
+ const random = yield* SecureRandom.SecureRandom
42
+ const storage = <A, R>(
43
+ effect: Effect.Effect<
44
+ A,
45
+ PlatformError.PlatformError | Schema.SchemaError | StateStorageError,
46
+ R
47
+ >,
48
+ ) =>
49
+ effect.pipe(
50
+ Effect.mapError((error) =>
51
+ error._tag === "StateStorageError"
52
+ ? error
53
+ : StateStorageError.make({
54
+ detail: "Runner state storage failed",
55
+ cause: Cause.fail(error),
56
+ }),
57
+ ),
58
+ )
59
+ yield* storage(
60
+ fs.makeDirectory(root, { recursive: true }).pipe(Effect.uninterruptible),
61
+ )
62
+ const mutex = yield* Semaphore.make(1)
63
+ const read = Effect.gen(function* () {
64
+ const files = yield* fs.readDirectory(root)
65
+ const rows = yield* Effect.forEach(
66
+ Array.filter(files, String.endsWith(".json")),
67
+ (file) =>
68
+ fs.readFileString(path.join(root, file)).pipe(
69
+ Effect.flatMap(Schema.decodeEffect(json)),
70
+ Effect.map((record) => ({ file, record })),
58
71
  ),
59
- )
60
- yield* storage(
61
- fs.makeDirectory(root, { recursive: true }).pipe(Effect.uninterruptible),
62
72
  )
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
+ return rows
74
+ })
75
+ const compact = Effect.scoped(
76
+ Effect.gen(function* () {
77
+ const rows = yield* read
78
+ const retained = Retention.retain(Array.map(rows, (row) => row.record))
79
+ const tokens = HashSet.fromIterable(
80
+ Array.map(retained, (record) => record.handle.token),
73
81
  )
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
+ const obsolete = Array.filter(
83
+ rows,
84
+ (row) => !HashSet.has(tokens, row.record.handle.token),
85
+ )
86
+ yield* Effect.forEach(
87
+ obsolete,
88
+ (row) =>
89
+ fs.remove(path.join(root, row.file)).pipe(Effect.uninterruptible),
90
+ { discard: true },
91
+ )
92
+ if (obsolete.length > 0) {
93
+ const directory = yield* fs.open(root, { flag: "r" })
94
+ yield* directory.sync.pipe(Effect.uninterruptible)
95
+ }
96
+ }),
97
+ )
98
+ yield* storage(compact)
99
+ return Store.of({
100
+ list: storage(
101
+ mutex.withPermit(
102
+ read.pipe(Effect.map((rows) => Array.map(rows, (row) => row.record))),
103
+ ),
104
+ ),
105
+ save: Effect.fnUntraced(
106
+ function* (record: SessionRecord) {
107
+ const encoded = yield* Schema.encodeEffect(json)(record)
108
+ const address = yield* Schema.encodeEffect(
109
+ Schema.fromJsonString(
110
+ Schema.Struct({ key: Key, generation: Schema.Int }),
111
+ ),
112
+ )({
113
+ key: record.handle.key,
114
+ generation: record.handle.generation,
115
+ })
116
+ const name = yield* digest.digestHex(
117
+ Digest.sha256,
118
+ new TextEncoder().encode(address),
82
119
  )
83
- const obsolete = Array.filter(
84
- rows,
85
- (row) => !HashSet.has(tokens, row.record.handle.token),
120
+ const nonce = yield* random.uuidV4()
121
+ const temporary = path.join(root, `.${name}.${nonce}.tmp`)
122
+ const destination = path.join(root, name + ".json")
123
+ yield* Effect.acquireRelease(
124
+ fs.open(temporary, { flag: "wx", mode: 0o600 }),
125
+ () => fs.remove(temporary, { force: true }).pipe(Effect.orDie),
126
+ ).pipe(
127
+ Effect.flatMap((file) =>
128
+ file
129
+ .writeAll(new TextEncoder().encode(encoded))
130
+ .pipe(
131
+ Effect.uninterruptible,
132
+ Effect.andThen(file.sync.pipe(Effect.uninterruptible)),
133
+ ),
134
+ ),
86
135
  )
87
- yield* Effect.forEach(
88
- obsolete,
89
- (row) =>
90
- fs.remove(path.join(root, row.file)).pipe(Effect.uninterruptible),
91
- { discard: true },
136
+ // Keep rename and directory synchronization together if the caller is canceled.
137
+ yield* Effect.uninterruptible(
138
+ Effect.gen(function* () {
139
+ yield* fs.rename(temporary, destination)
140
+ const directory = yield* fs.open(root, { flag: "r" })
141
+ yield* directory.sync.pipe(Effect.uninterruptible)
142
+ }),
92
143
  )
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
- })
144
+ // The new high-water file is durable before any old record is removed.
145
+ yield* compact
146
+ },
147
+ Effect.scoped,
148
+ mutex.withPermit,
149
+ storage,
150
+ ),
156
151
  })
152
+ })
157
153
 
158
154
  export const layerMemory = Layer.effect(Store, memory)
159
155
  export const layerDirectory = (root: string) =>