@zachwill/pi-orchestrate 0.9.0 → 0.10.0

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.
@@ -0,0 +1,291 @@
1
+ import {
2
+ Context,
3
+ Deferred,
4
+ Effect,
5
+ Exit,
6
+ FiberSet,
7
+ Layer,
8
+ Schema,
9
+ SynchronizedRef,
10
+ } from "effect";
11
+ import {
12
+ createWorkerSession,
13
+ type ChildSessionOptions,
14
+ type WorkerSessionCreationError,
15
+ type WorkerSessionDependencies,
16
+ type WorkerSessionHandle,
17
+ } from "./session.js";
18
+
19
+ export class WorkerSessionAcquisitionClosedError extends Schema.TaggedError<WorkerSessionAcquisitionClosedError>()(
20
+ "WorkerSession.AcquisitionClosedError",
21
+ { message: Schema.String },
22
+ ) {}
23
+
24
+ export type WorkerSessionAcquisitionError =
25
+ | WorkerSessionCreationError
26
+ | WorkerSessionAcquisitionClosedError;
27
+
28
+ export interface ChildSessionsService {
29
+ /**
30
+ * Acquires a child session through a process-owned producer. The adopter must
31
+ * synchronously install ownership before returning a value. Returning undefined
32
+ * rejects adoption and leaves ChildSessions responsible for disposal.
33
+ */
34
+ readonly acquire: <Adopted>(
35
+ options: ChildSessionOptions,
36
+ adopt: (session: WorkerSessionHandle) => Adopted | undefined,
37
+ ) => Effect.Effect<Adopted | undefined, WorkerSessionAcquisitionError>;
38
+ /** Closes every handoff without waiting for uncancellable Pi calls. */
39
+ readonly shutdown: () => Effect.Effect<void>;
40
+ }
41
+
42
+ export class ChildSessions extends Context.Service<ChildSessions, ChildSessionsService>()(
43
+ "@zachwill/pi-orchestrate/ChildSessions",
44
+ ) {}
45
+
46
+ type AcquisitionHandoffState =
47
+ | { readonly _tag: "Pending" }
48
+ | { readonly _tag: "Offered"; readonly session: WorkerSessionHandle }
49
+ | { readonly _tag: "Adopting"; readonly session: WorkerSessionHandle }
50
+ | { readonly _tag: "Adopted" }
51
+ | { readonly _tag: "Abandoned"; readonly error?: WorkerSessionAcquisitionClosedError }
52
+ | { readonly _tag: "Failed" };
53
+
54
+ interface AcquisitionHandoff {
55
+ readonly state: SynchronizedRef.SynchronizedRef<AcquisitionHandoffState>;
56
+ readonly result: Deferred.Deferred<WorkerSessionHandle, WorkerSessionAcquisitionError>;
57
+ }
58
+
59
+ type ChildSessionsState =
60
+ | { readonly _tag: "Open"; readonly handoffs: ReadonlySet<AcquisitionHandoff> }
61
+ | { readonly _tag: "Closed"; readonly error: WorkerSessionAcquisitionClosedError };
62
+
63
+ type AdoptionReservation =
64
+ | { readonly _tag: "Reserved"; readonly session: WorkerSessionHandle }
65
+ | { readonly _tag: "Closed"; readonly error: WorkerSessionAcquisitionClosedError }
66
+ | { readonly _tag: "Abandoned" };
67
+
68
+ function disposeLateSession(session: WorkerSessionHandle): Effect.Effect<void> {
69
+ return session.dispose().pipe(Effect.catchCause(() => Effect.void));
70
+ }
71
+
72
+ function admitReclamationOrJoinAfterClosure(
73
+ fibers: FiberSet.FiberSet<void, never>,
74
+ reclamation: Effect.Effect<void>,
75
+ onOpenObserved: () => Effect.Effect<void>,
76
+ ): Effect.Effect<void> {
77
+ return Effect.suspend(() => {
78
+ if (fibers.state._tag === "Closed") return reclamation;
79
+ return Effect.gen(function* () {
80
+ yield* onOpenObserved().pipe(Effect.catchCause(() => Effect.void));
81
+ yield* FiberSet.run(fibers, reclamation, { startImmediately: true });
82
+ if (fibers.state._tag === "Closed") {
83
+ // FiberSet.run returns an interrupted sentinel when closure wins admission.
84
+ // If admission won, closure interrupts the admitted fiber instead. Real
85
+ // session disposal is cached and uninterruptible, so this fallback safely
86
+ // joins that same disposal in both cases rather than starting cleanup twice.
87
+ yield* reclamation;
88
+ }
89
+ });
90
+ });
91
+ }
92
+
93
+ export function createChildSessionsLayer(
94
+ overrides: Partial<WorkerSessionDependencies> = {},
95
+ ): Layer.Layer<ChildSessions> {
96
+ return Layer.effect(
97
+ ChildSessions,
98
+ Effect.gen(function* () {
99
+ const dependencies = {
100
+ beforeAdoptionReservation:
101
+ overrides.beforeAdoptionReservation ?? (() => Effect.void),
102
+ onReclamationOpenObserved:
103
+ overrides.onReclamationOpenObserved ?? (() => Effect.void),
104
+ };
105
+ const fibers = yield* FiberSet.make<void, never>();
106
+ const serviceState = yield* SynchronizedRef.make<ChildSessionsState>({
107
+ _tag: "Open",
108
+ handoffs: new Set(),
109
+ });
110
+
111
+ const withoutHandoff = (
112
+ state: ChildSessionsState,
113
+ handoff: AcquisitionHandoff,
114
+ ): ChildSessionsState => {
115
+ if (state._tag === "Closed" || !state.handoffs.has(handoff)) return state;
116
+ const handoffs = new Set(state.handoffs);
117
+ handoffs.delete(handoff);
118
+ return { _tag: "Open", handoffs };
119
+ };
120
+
121
+ const removeHandoff = (handoff: AcquisitionHandoff): Effect.Effect<void> =>
122
+ SynchronizedRef.update(serviceState, (state) => withoutHandoff(state, handoff));
123
+
124
+ const runReclamation = (session: WorkerSessionHandle): Effect.Effect<void> =>
125
+ admitReclamationOrJoinAfterClosure(
126
+ fibers,
127
+ disposeLateSession(session).pipe(Effect.uninterruptible),
128
+ dependencies.onReclamationOpenObserved,
129
+ );
130
+
131
+ const abandonHandoff = Effect.fn("ChildSessions.abandonHandoff")(function* (
132
+ handoff: AcquisitionHandoff,
133
+ ) {
134
+ const session = yield* SynchronizedRef.modifyEffect(serviceState, (service) => {
135
+ const nextService = withoutHandoff(service, handoff);
136
+ return SynchronizedRef.modify(handoff.state, (state): readonly [
137
+ { readonly session: WorkerSessionHandle | undefined },
138
+ AcquisitionHandoffState,
139
+ ] => {
140
+ if (state._tag === "Pending") {
141
+ return [{ session: undefined }, { _tag: "Abandoned" }];
142
+ }
143
+ if (state._tag === "Offered") {
144
+ return [{ session: state.session }, { _tag: "Abandoned" }];
145
+ }
146
+ return [{ session: undefined }, state];
147
+ }).pipe(Effect.map(({ session }) => [session, nextService] as const));
148
+ });
149
+ if (session) yield* runReclamation(session);
150
+ });
151
+
152
+ const shutdown = Effect.fn("ChildSessions.shutdown")(() =>
153
+ Effect.gen(function* () {
154
+ const closed = yield* SynchronizedRef.modifyEffect(serviceState, (state) => {
155
+ if (state._tag === "Closed") return Effect.succeed([undefined, state] as const);
156
+ const error = new WorkerSessionAcquisitionClosedError({
157
+ message: "Child sessions are shutting down",
158
+ });
159
+ return Effect.gen(function* () {
160
+ const sessions: WorkerSessionHandle[] = [];
161
+ for (const handoff of state.handoffs) {
162
+ const session = yield* SynchronizedRef.modifyEffect(handoff.state, (handoffState) => {
163
+ if (handoffState._tag === "Pending") {
164
+ return Deferred.fail(handoff.result, error).pipe(
165
+ Effect.as([undefined, { _tag: "Abandoned", error }] as const),
166
+ );
167
+ }
168
+ if (handoffState._tag === "Offered") {
169
+ return Effect.succeed([
170
+ handoffState.session,
171
+ { _tag: "Abandoned", error },
172
+ ] as const);
173
+ }
174
+ // Failed already settled its Deferred. Adopting is an ownership
175
+ // reservation whose synchronous winner must be allowed to commit.
176
+ return Effect.succeed([undefined, handoffState] as const);
177
+ });
178
+ if (session) sessions.push(session);
179
+ }
180
+ return [{ sessions }, { _tag: "Closed", error }] as const;
181
+ });
182
+ });
183
+ if (!closed) return;
184
+ for (const session of closed.sessions) yield* runReclamation(session);
185
+ }).pipe(Effect.uninterruptible)
186
+ );
187
+
188
+ yield* Effect.addFinalizer(() => shutdown());
189
+
190
+ const acquire = Effect.fn("ChildSessions.acquire")(function* <Adopted>(
191
+ options: ChildSessionOptions,
192
+ adopt: (session: WorkerSessionHandle) => Adopted | undefined,
193
+ ) {
194
+ const handoff: AcquisitionHandoff = {
195
+ state: yield* SynchronizedRef.make<AcquisitionHandoffState>({ _tag: "Pending" }),
196
+ result: yield* Deferred.make<WorkerSessionHandle, WorkerSessionAcquisitionError>(),
197
+ };
198
+ return yield* Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
199
+ const admissionError = yield* SynchronizedRef.modify(serviceState, (state) => {
200
+ if (state._tag === "Closed") return [state.error, state] as const;
201
+ return [undefined, {
202
+ _tag: "Open",
203
+ handoffs: new Set([...state.handoffs, handoff]),
204
+ }] as const;
205
+ });
206
+ if (admissionError) return yield* Effect.fail(admissionError);
207
+
208
+ const producer = createWorkerSession(options, overrides).pipe(
209
+ Effect.matchCauseEffect({
210
+ onFailure: (cause) => Effect.gen(function* () {
211
+ const failed = yield* SynchronizedRef.modifyEffect(handoff.state, (state) =>
212
+ state._tag === "Pending"
213
+ ? Deferred.failCause(handoff.result, cause).pipe(
214
+ Effect.as([true, { _tag: "Failed" }] as const),
215
+ )
216
+ : Effect.succeed([false, state] as const));
217
+ if (failed) yield* removeHandoff(handoff);
218
+ }),
219
+ onSuccess: (session) => Effect.gen(function* () {
220
+ const offered = yield* SynchronizedRef.modifyEffect(handoff.state, (state) =>
221
+ state._tag === "Pending"
222
+ ? Deferred.succeed(handoff.result, session).pipe(
223
+ Effect.as([true, { _tag: "Offered", session }] as const),
224
+ )
225
+ : Effect.succeed([false, state] as const));
226
+ if (!offered) {
227
+ yield* removeHandoff(handoff);
228
+ yield* runReclamation(session);
229
+ }
230
+ }),
231
+ }),
232
+ Effect.uninterruptible,
233
+ );
234
+ yield* FiberSet.run(fibers, producer, { startImmediately: true });
235
+
236
+ const session = yield* restore(
237
+ Deferred.await(handoff.result).pipe(
238
+ Effect.tap(() => dependencies.beforeAdoptionReservation()),
239
+ ),
240
+ ).pipe(Effect.onInterrupt(() => abandonHandoff(handoff)));
241
+ const reservation = yield* SynchronizedRef.modifyEffect(serviceState, (service) => {
242
+ if (service._tag === "Closed") {
243
+ return Effect.succeed([
244
+ { _tag: "Closed", error: service.error } satisfies AdoptionReservation,
245
+ service,
246
+ ] as const);
247
+ }
248
+ return SynchronizedRef.modify(
249
+ handoff.state,
250
+ (state): readonly [AdoptionReservation, AcquisitionHandoffState] => {
251
+ if (state._tag === "Abandoned") {
252
+ return state.error
253
+ ? [{ _tag: "Closed", error: state.error }, state]
254
+ : [{ _tag: "Abandoned" }, state];
255
+ }
256
+ if (state._tag !== "Offered" || state.session !== session) {
257
+ return [{ _tag: "Abandoned" }, state];
258
+ }
259
+ return [
260
+ { _tag: "Reserved", session },
261
+ { _tag: "Adopting", session },
262
+ ];
263
+ },
264
+ ).pipe(Effect.map((result) => [result, service] as const));
265
+ });
266
+ if (reservation._tag === "Closed") return yield* Effect.fail(reservation.error);
267
+ if (reservation._tag === "Abandoned") {
268
+ return yield* Effect.die(new Error("Child session acquisition handoff was abandoned"));
269
+ }
270
+
271
+ // The adopter is arbitrary synchronous runtime code. The reservation
272
+ // protects it from shutdown, but no SynchronizedRef semaphore is held.
273
+ const adopted = yield* Effect.exit(Effect.sync(() => adopt(reservation.session)));
274
+ const transferred = Exit.isSuccess(adopted) && adopted.value !== undefined;
275
+ yield* SynchronizedRef.update(handoff.state, (state): AcquisitionHandoffState => {
276
+ if (state._tag !== "Adopting" || state.session !== reservation.session) return state;
277
+ return transferred ? { _tag: "Adopted" } : { _tag: "Abandoned" };
278
+ });
279
+ yield* removeHandoff(handoff);
280
+ if (transferred) return adopted.value;
281
+
282
+ yield* runReclamation(reservation.session);
283
+ if (Exit.isFailure(adopted)) return yield* Effect.failCause(adopted.cause);
284
+ return undefined;
285
+ }));
286
+ });
287
+
288
+ return ChildSessions.of({ acquire, shutdown });
289
+ }),
290
+ );
291
+ }