@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
@@ -37,61 +37,62 @@ type LogEntry = Omit<typeof Log.Type, "cause"> & {
37
37
  }
38
38
 
39
39
  /** Queue data; serialization belongs to the RPC consumer, outside synchronous logger callbacks. */
40
- export const telemetry = (contract: RunnerContract) =>
41
- Effect.gen(function* () {
42
- const events = yield* Queue.bounded<ChildEvent>(2)
43
- const logs = yield* Queue.dropping<LogEntry, Cause.Done>(128)
44
- const dropped = MutableRef.make(0)
45
- const encode = (cause: Cause.Cause<unknown>) =>
46
- contract.encodeCause(cause).pipe(Effect.orDie)
47
- const logger = Logger.make((entry) => {
48
- // Logger callbacks are synchronous; a dropping offer and loss counter cannot suspend.
49
- const accepted = Queue.offerUnsafe(logs, {
50
- level: entry.logLevel,
51
- message: entry.message,
52
- cause: entry.cause,
53
- annotations: entry.fiber.getRef(References.CurrentLogAnnotations),
54
- })
55
- if (!accepted) MutableRef.increment(dropped)
40
+ export const telemetry = Effect.fnUntraced(function* (
41
+ contract: RunnerContract,
42
+ ) {
43
+ const events = yield* Queue.bounded<ChildEvent>(2)
44
+ const logs = yield* Queue.dropping<LogEntry, Cause.Done>(128)
45
+ const dropped = MutableRef.make(0)
46
+ const encode = (cause: Cause.Cause<unknown>) =>
47
+ contract.encodeCause(cause).pipe(Effect.orDie)
48
+ const logger = Logger.make((entry) => {
49
+ // Logger callbacks are synchronous; a dropping offer and loss counter cannot suspend.
50
+ const accepted = Queue.offerUnsafe(logs, {
51
+ level: entry.logLevel,
52
+ message: entry.message,
53
+ cause: entry.cause,
54
+ annotations: entry.fiber.getRef(References.CurrentLogAnnotations),
56
55
  })
57
- return {
58
- events,
59
- logs,
60
- logger,
61
- eventStream: Stream.fromQueue(events).pipe(
62
- Stream.mapEffect((event): Effect.Effect<typeof Event.Type> =>
63
- event._tag === "Failed"
64
- ? encode(event.cause).pipe(
65
- Effect.map(
66
- (cause) =>
67
- ({
68
- _tag: "Failed",
69
- cause,
70
- }) satisfies typeof Event.Type,
71
- ),
72
- )
73
- : Effect.succeed(event),
74
- ),
56
+ if (!accepted) MutableRef.increment(dropped)
57
+ })
58
+ return {
59
+ events,
60
+ logs,
61
+ logger,
62
+ eventStream: Stream.fromQueue(events).pipe(
63
+ Stream.mapEffect((event): Effect.Effect<typeof Event.Type> =>
64
+ event._tag === "Failed"
65
+ ? encode(event.cause).pipe(
66
+ Effect.map(
67
+ (cause) =>
68
+ ({
69
+ _tag: "Failed",
70
+ cause,
71
+ }) satisfies typeof Event.Type,
72
+ ),
73
+ )
74
+ : Effect.succeed(event),
75
75
  ),
76
- logStream: Stream.fromQueue(logs).pipe(
77
- Stream.mapEffect((entry) =>
78
- encode(entry.cause).pipe(
79
- Effect.map((cause): typeof LogEvent.Type => ({
80
- _tag: "Log",
81
- entry: { ...entry, cause },
82
- dropped: MutableRef.get(dropped),
83
- })),
84
- ),
85
- ),
86
- Stream.concat(
87
- Stream.sync((): typeof LogEvent.Type => ({
88
- _tag: "LogsEnded",
76
+ ),
77
+ logStream: Stream.fromQueue(logs).pipe(
78
+ Stream.mapEffect((entry) =>
79
+ encode(entry.cause).pipe(
80
+ Effect.map((cause): typeof LogEvent.Type => ({
81
+ _tag: "Log",
82
+ entry: { ...entry, cause },
89
83
  dropped: MutableRef.get(dropped),
90
84
  })),
91
85
  ),
92
86
  ),
93
- }
94
- })
87
+ Stream.concat(
88
+ Stream.sync((): typeof LogEvent.Type => ({
89
+ _tag: "LogsEnded",
90
+ dropped: MutableRef.get(dropped),
91
+ })),
92
+ ),
93
+ ),
94
+ }
95
+ })
95
96
  type Telemetry = Effect.Success<ReturnType<typeof telemetry>>
96
97
  interface Running {
97
98
  readonly fork: Pick<Fork.Fork<never, unknown>, "dispose" | "state">
@@ -114,210 +115,218 @@ const runningOf = (state: Lifecycle): Option.Option<Running> =>
114
115
  ? state.running
115
116
  : Option.none()
116
117
 
117
- const cleanup = (running: Running, retired: boolean) =>
118
- Effect.gen(function* () {
119
- const pluginExit = yield* Effect.exit(running.fork.dispose)
120
- const bridgeExit = yield* Effect.exit(running.bridge.dispose)
121
- yield* Fiber.join(running.observation)
122
- const failed = yield* Ref.get(running.observedFailure)
123
- return Array.reduce(
124
- [bridgeExit, pluginExit],
125
- retired ? failed : Cause.empty,
126
- (cause, exit) =>
127
- Exit.isFailure(exit)
128
- ? Cause.combine(
129
- cause,
130
- Cause.fromReasons(
131
- Array.flatMap(exit.cause.reasons, (reason) =>
132
- reason._tag === "Fail"
133
- ? reason.error.cause.reasons
134
- : [reason],
135
- ),
118
+ const cleanup = Effect.fnUntraced(function* (
119
+ running: Running,
120
+ retired: boolean,
121
+ ) {
122
+ const pluginExit = yield* Effect.exit(running.fork.dispose)
123
+ const bridgeExit = yield* Effect.exit(running.bridge.dispose)
124
+ yield* Fiber.join(running.observation)
125
+ const failed = yield* Ref.get(running.observedFailure)
126
+ return Array.reduce(
127
+ [bridgeExit, pluginExit],
128
+ retired ? failed : Cause.empty,
129
+ (cause, exit) =>
130
+ Exit.isFailure(exit)
131
+ ? Cause.combine(
132
+ cause,
133
+ Cause.fromReasons(
134
+ Array.flatMap(exit.cause.reasons, (reason) =>
135
+ reason._tag === "Fail" ? reason.error.cause.reasons : [reason],
136
136
  ),
137
- )
138
- : cause,
139
- )
140
- })
137
+ ),
138
+ )
139
+ : cause,
140
+ )
141
+ })
141
142
 
142
- export const lifecycle = (
143
+ export const lifecycle = Effect.fnUntraced(function* (
143
144
  contract: RunnerContract,
144
145
  peer: Peer,
145
146
  identity: typeof Identity.Type,
146
147
  telemetry: Telemetry,
147
- ) =>
148
- Effect.gen(function* () {
149
- const scope = yield* Effect.scope
150
- const instanceScope = yield* Scope.fork(scope)
151
- const releaseExports = yield* Deferred.make<void>()
152
- const state = yield* SynchronizedRef.make<Lifecycle>(Lifecycle.Idle())
153
- // This gate represents host consumers draining, not a second lifecycle state.
154
- const release = SynchronizedRef.update(state, (current) =>
155
- Lifecycle.Disposing({ running: runningOf(current) }),
156
- ).pipe(Effect.andThen(Deferred.succeed(releaseExports, undefined)))
148
+ ) {
149
+ const scope = yield* Effect.scope
150
+ const instanceScope = yield* Scope.fork(scope)
151
+ const releaseExports = yield* Deferred.make<void>()
152
+ const state = yield* SynchronizedRef.make<Lifecycle>(Lifecycle.Idle())
153
+ // This gate represents host consumers draining, not a second lifecycle state.
154
+ const disposing = (current: Lifecycle) =>
155
+ Lifecycle.Disposing({ running: runningOf(current) })
156
+ const release = SynchronizedRef.update(state, disposing).pipe(
157
+ Effect.andThen(Deferred.succeed(releaseExports, undefined)),
158
+ )
157
159
 
158
- const exportBridge = Graft.plugin(
159
- Plugin.make({
160
- name: `${contract.name}/exports`,
161
- inject: Array.map(contract.provides, (service) => service.key),
162
- apply: () =>
163
- Effect.gen(function* () {
164
- for (const service of contract.provides)
165
- yield* service.serve(peer).pipe(
166
- Effect.tapCause((cause) =>
167
- Cause.hasInterruptsOnly(cause)
168
- ? Effect.void
169
- : Queue.offer(
170
- telemetry.events,
171
- ChildEvent.Failed({ cause }),
172
- ),
173
- ),
174
- Effect.forkScoped,
175
- )
176
- yield* Effect.addFinalizer(() =>
177
- Effect.gen(function* () {
178
- const withdrawing = yield* SynchronizedRef.modify(
179
- state,
180
- (current) =>
181
- current._tag === "Disposing"
182
- ? [false, current]
183
- : [
184
- true,
185
- Lifecycle.Withdrawing({
186
- running: runningOf(current),
187
- }),
188
- ],
189
- )
190
- if (withdrawing)
191
- yield* Queue.offer(telemetry.events, ChildEvent.Withdrawing())
192
- yield* Deferred.await(releaseExports)
193
- }),
160
+ const exportBridge = Graft.plugin(
161
+ Plugin.make({
162
+ name: `${contract.name}/exports`,
163
+ inject: Array.map(contract.provides, (service) => service.key),
164
+ apply: Effect.fnUntraced(function* () {
165
+ for (const service of contract.provides)
166
+ yield* service.serve(peer).pipe(
167
+ Effect.tapCause((cause) =>
168
+ Cause.hasInterruptsOnly(cause)
169
+ ? Effect.void
170
+ : Queue.offer(telemetry.events, ChildEvent.Failed({ cause })),
171
+ ),
172
+ Effect.forkScoped,
173
+ )
174
+ yield* Effect.addFinalizer(
175
+ Effect.fnUntraced(function* () {
176
+ const withdrawing = yield* SynchronizedRef.modify(
177
+ state,
178
+ (current) =>
179
+ current._tag === "Disposing"
180
+ ? [false, current]
181
+ : [
182
+ true,
183
+ Lifecycle.Withdrawing({
184
+ running: runningOf(current),
185
+ }),
186
+ ],
194
187
  )
188
+ if (withdrawing)
189
+ yield* Queue.offer(telemetry.events, ChildEvent.Withdrawing())
190
+ yield* Deferred.await(releaseExports)
195
191
  }),
192
+ )
196
193
  }),
197
- ).pipe(Scope.provide(instanceScope))
194
+ }),
195
+ ).pipe(Scope.provide(instanceScope))
198
196
 
199
- const start = (input: Schema.Json) =>
200
- Effect.gen(function* () {
201
- // Imported clients outlive Load and remain available during plugin cleanup.
202
- for (const service of contract.inject)
203
- yield* service.publish(peer).pipe(
204
- Scope.provide(scope),
205
- Effect.mapError((error) =>
206
- ActivationFailed.make({
207
- detail: "Publishing imported services failed",
208
- cause: Cause.fail(error),
209
- }),
210
- ),
211
- )
212
- const module = yield* loadModule(contract.module)
213
- const plugin = Plugin.fromModule(module)
214
- if (Option.isNone(plugin))
215
- return yield* ActivationFailed.make({
216
- detail: "The implementation module exports no plugin",
217
- })
218
- const fork = yield* Graft.plugin(
219
- { ...plugin.value, retry: undefined },
220
- input,
221
- ).pipe(
222
- Effect.provideService(Instance, identity),
223
- Scope.provide(instanceScope),
224
- )
225
- const observedFailure = yield* Ref.make<Cause.Cause<unknown>>(
226
- Cause.empty,
227
- )
228
- const observation = yield* fork.state.pipe(
229
- Stream.runForEach((state) =>
230
- state._tag === "Failed"
231
- ? Ref.set(observedFailure, state.cause)
232
- : Effect.void,
233
- ),
234
- Effect.forkIn(instanceScope),
235
- )
236
- const bridge = yield* exportBridge
237
- return { fork, bridge, observedFailure, observation } satisfies Running
197
+ const start = Effect.fnUntraced(function* (input: Schema.Json) {
198
+ // Imported clients outlive Load and remain available during plugin cleanup.
199
+ for (const service of contract.inject)
200
+ yield* service.publish(peer).pipe(
201
+ Scope.provide(scope),
202
+ Effect.mapError((error) =>
203
+ ActivationFailed.make({
204
+ detail: "Publishing imported services failed",
205
+ cause: Cause.fail(error),
206
+ }),
207
+ ),
208
+ )
209
+ const module = yield* loadModule(contract.module)
210
+ const plugin = Plugin.fromModule(module)
211
+ if (Option.isNone(plugin))
212
+ return yield* ActivationFailed.make({
213
+ detail: "The implementation module exports no plugin",
238
214
  })
239
- const awaitActive = (running: Running) =>
240
- Effect.gen(function* () {
241
- const first = yield* running.fork.state.pipe(
242
- Stream.filter((state) => state._tag !== "Loading"),
243
- Stream.runHead,
244
- )
245
- if (Option.isNone(first))
246
- return yield* ActivationFailed.make({
247
- detail: "The remote fork disappeared during startup",
248
- })
249
- if (first.value._tag === "Failed")
250
- return yield* Effect.fail(contract.normalizeCause(first.value.cause))
251
- if (first.value._tag !== "Active")
252
- return yield* ActivationFailed.make({
253
- detail:
254
- "The remote plugin did not become active; check its declared dependencies",
255
- })
256
- yield* running.fork.state.pipe(
257
- Stream.runForEach((state) =>
258
- state._tag === "Failed"
259
- ? Queue.offer(
260
- telemetry.events,
261
- ChildEvent.Failed({ cause: state.cause }),
262
- )
263
- : Effect.void,
264
- ),
265
- Effect.forkIn(instanceScope),
266
- )
267
- const exported = yield* running.bridge.state.pipe(
268
- Stream.filter(
269
- (state) =>
270
- state._tag === "Active" ||
271
- state._tag === "Failed" ||
272
- state._tag === "Disposed",
273
- ),
274
- Stream.runHead,
275
- )
276
- if (Option.isSome(exported) && exported.value._tag === "Failed")
277
- return yield* ActivationFailed.make({
278
- detail: "The remote export bridge failed during startup",
279
- cause: exported.value.cause,
280
- })
281
- if (Option.isNone(exported) || exported.value._tag !== "Active")
282
- return yield* ActivationFailed.make({
283
- detail: "The remote export bridge stopped during startup",
284
- })
215
+ const fork = yield* Graft.plugin(
216
+ { ...plugin.value, retry: undefined },
217
+ input,
218
+ ).pipe(
219
+ Effect.provideService(Instance, identity),
220
+ Scope.provide(instanceScope),
221
+ )
222
+ const observedFailure = yield* Ref.make<Cause.Cause<unknown>>(Cause.empty)
223
+ const observation = yield* fork.state.pipe(
224
+ Stream.runForEach((state) =>
225
+ state._tag === "Failed"
226
+ ? Ref.set(observedFailure, state.cause)
227
+ : Effect.void,
228
+ ),
229
+ Effect.forkIn(instanceScope),
230
+ )
231
+ const bridge = yield* exportBridge
232
+ return { fork, bridge, observedFailure, observation } satisfies Running
233
+ })
234
+ const awaitActive = Effect.fnUntraced(function* (running: Running) {
235
+ const first = yield* running.fork.state.pipe(
236
+ Stream.filter((state) => state._tag !== "Loading"),
237
+ Stream.runHead,
238
+ )
239
+ if (Option.isNone(first))
240
+ return yield* ActivationFailed.make({
241
+ detail: "The remote fork disappeared during startup",
285
242
  })
286
- const load = (input: Schema.Json) =>
287
- Effect.gen(function* () {
288
- yield* SynchronizedRef.updateEffect(state, (current) =>
289
- current._tag === "Idle"
290
- ? Effect.succeed(Lifecycle.Loading())
291
- : Effect.fail(AlreadyLoaded.make({})),
292
- )
293
- const running = yield* start(input)
294
- yield* SynchronizedRef.update(state, (current) =>
243
+ if (first.value._tag === "Failed")
244
+ return yield* Effect.fail(contract.normalizeCause(first.value.cause))
245
+ if (first.value._tag !== "Active")
246
+ return yield* ActivationFailed.make({
247
+ detail:
248
+ "The remote plugin did not become active; check its declared dependencies",
249
+ })
250
+ yield* running.fork.state.pipe(
251
+ Stream.runForEach((state) =>
252
+ state._tag === "Failed"
253
+ ? Queue.offer(
254
+ telemetry.events,
255
+ ChildEvent.Failed({ cause: state.cause }),
256
+ )
257
+ : Effect.void,
258
+ ),
259
+ Effect.forkIn(instanceScope),
260
+ )
261
+ const exported = yield* running.bridge.state.pipe(
262
+ Stream.filter(
263
+ (state) =>
264
+ state._tag === "Active" ||
265
+ state._tag === "Failed" ||
266
+ state._tag === "Disposed",
267
+ ),
268
+ Stream.runHead,
269
+ )
270
+ if (Option.isSome(exported) && exported.value._tag === "Failed")
271
+ return yield* ActivationFailed.make({
272
+ detail: "The remote export bridge failed during startup",
273
+ cause: exported.value.cause,
274
+ })
275
+ if (Option.isNone(exported) || exported.value._tag !== "Active")
276
+ return yield* ActivationFailed.make({
277
+ detail: "The remote export bridge stopped during startup",
278
+ })
279
+ })
280
+ const load = Effect.fnUntraced(
281
+ function* (input: Schema.Json) {
282
+ yield* SynchronizedRef.updateEffect(state, (current) =>
283
+ current._tag === "Idle"
284
+ ? Effect.succeed(Lifecycle.Loading())
285
+ : Effect.fail(AlreadyLoaded.make({})),
286
+ )
287
+ const running = yield* start(input)
288
+ const disposed = yield* SynchronizedRef.modify(
289
+ state,
290
+ (current): readonly [boolean, Lifecycle] =>
295
291
  current._tag === "Withdrawing"
296
- ? Lifecycle.Withdrawing({ running: Option.some(running) })
292
+ ? [false, Lifecycle.Withdrawing({ running: Option.some(running) })]
297
293
  : current._tag === "Disposing"
298
- ? Lifecycle.Disposing({ running: Option.some(running) })
299
- : Lifecycle.Running({ running }),
300
- )
301
- yield* awaitActive(running)
302
- }).pipe(
303
- Effect.mapError((error) =>
304
- Cause.isCause(error)
305
- ? error
306
- : contract.normalizeCause(Cause.fail(error)),
307
- ),
294
+ ? [true, Lifecycle.Disposing({ running: Option.some(running) })]
295
+ : [false, Lifecycle.Running({ running })],
308
296
  )
309
- const dispose = Effect.gen(function* () {
310
- const previous = yield* SynchronizedRef.get(state)
311
- yield* release
312
- const current = runningOf(previous)
313
- const retired = previous._tag === "Withdrawing"
314
- const cause = Option.isSome(current)
315
- ? yield* cleanup(current.value, retired)
316
- : Cause.empty
317
- yield* Queue.offer(telemetry.events, ChildEvent.Stopped())
318
- yield* Queue.end(telemetry.logs)
319
- if (cause.reasons.length > 0)
320
- return yield* Effect.fail(contract.normalizeCause(cause))
321
- })
322
- return { load, dispose, release }
297
+ // Dispose saw Loading and found nothing to stop; this load owns the cleanup.
298
+ if (disposed) {
299
+ const cause = yield* cleanup(running, false)
300
+ return yield* Effect.fail(
301
+ Cause.combine(
302
+ Cause.fail(
303
+ ActivationFailed.make({
304
+ detail: "The execution was disposed during load",
305
+ }),
306
+ ),
307
+ cause,
308
+ ),
309
+ )
310
+ }
311
+ yield* awaitActive(running)
312
+ },
313
+ Effect.mapError((error) =>
314
+ contract.normalizeCause(Cause.isCause(error) ? error : Cause.fail(error)),
315
+ ),
316
+ )
317
+ const dispose = Effect.gen(function* () {
318
+ // One transition, so a concurrent Load or export withdrawal cannot slip between read and write.
319
+ const previous = yield* SynchronizedRef.getAndUpdate(state, disposing)
320
+ yield* Deferred.succeed(releaseExports, undefined)
321
+ const current = runningOf(previous)
322
+ const retired = previous._tag === "Withdrawing"
323
+ const cause = Option.isSome(current)
324
+ ? yield* cleanup(current.value, retired)
325
+ : Cause.empty
326
+ yield* Queue.offer(telemetry.events, ChildEvent.Stopped())
327
+ yield* Queue.end(telemetry.logs)
328
+ if (cause.reasons.length > 0)
329
+ return yield* Effect.fail(contract.normalizeCause(cause))
323
330
  })
331
+ return { load, dispose, release }
332
+ })