@typeonce/effect-machine 0.6.0 → 0.7.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.
Files changed (54) hide show
  1. package/README.md +4 -3
  2. package/dist/Machine.d.ts +24 -6
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js.map +1 -1
  5. package/dist/internal/machine/atom.d.ts +5 -0
  6. package/dist/internal/machine/atom.d.ts.map +1 -1
  7. package/dist/internal/machine/atom.js +6 -3
  8. package/dist/internal/machine/atom.js.map +1 -1
  9. package/dist/internal/machine/machine.d.ts.map +1 -1
  10. package/dist/internal/machine/machine.js +1 -1
  11. package/dist/internal/machine/machine.js.map +1 -1
  12. package/dist/unstable/reactivity/AtomMachine.d.ts +26 -0
  13. package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
  14. package/dist/unstable/reactivity/AtomMachine.js +23 -0
  15. package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
  16. package/docs/agent-guide.md +14 -0
  17. package/package.json +5 -5
  18. package/src/Machine.ts +6909 -0
  19. package/src/index.ts +1 -0
  20. package/src/internal/machine/activities.ts +108 -0
  21. package/src/internal/machine/atom.ts +684 -0
  22. package/src/internal/machine/cluster.ts +394 -0
  23. package/src/internal/machine/command.ts +58 -0
  24. package/src/internal/machine/commandRuntime.ts +43 -0
  25. package/src/internal/machine/configuration.ts +1331 -0
  26. package/src/internal/machine/errors.ts +87 -0
  27. package/src/internal/machine/executionPlan.ts +996 -0
  28. package/src/internal/machine/invocation.ts +119 -0
  29. package/src/internal/machine/machine.ts +1750 -0
  30. package/src/internal/machine/planner.ts +1933 -0
  31. package/src/internal/machine/process.ts +906 -0
  32. package/src/internal/machine/protocol.ts +322 -0
  33. package/src/internal/machine/readiness.ts +10 -0
  34. package/src/internal/machine/runtime.ts +2512 -0
  35. package/src/internal/machine/serialization.ts +498 -0
  36. package/src/internal/machine/stateDefinition.ts +270 -0
  37. package/src/internal/machine/symbols.ts +2 -0
  38. package/src/internal/machine/topology.ts +479 -0
  39. package/src/internal/testing/machine/arbitrary.ts +102 -0
  40. package/src/internal/testing/machine/exploration.ts +331 -0
  41. package/src/internal/testing/machine/finiteModel.ts +1498 -0
  42. package/src/internal/testing/machine/invariant.ts +372 -0
  43. package/src/internal/testing/machine/probe.ts +79 -0
  44. package/src/internal/testing/machine/referenceModel.ts +1505 -0
  45. package/src/internal/testing/machine/runtime.ts +1710 -0
  46. package/src/internal/testing/machine/runtimeInvariant.ts +486 -0
  47. package/src/internal/testing/machine/trace.ts +150 -0
  48. package/src/internal/testing/machine/verification.ts +1890 -0
  49. package/src/testing/MachineTest.ts +2067 -0
  50. package/src/testing/index.ts +7 -0
  51. package/src/unstable/cluster/ClusterMachine.ts +390 -0
  52. package/src/unstable/cluster/index.ts +1 -0
  53. package/src/unstable/reactivity/AtomMachine.ts +696 -0
  54. package/src/unstable/reactivity/index.ts +1 -0
@@ -0,0 +1,2512 @@
1
+ /**
2
+ * Internal machine process runtime helpers.
3
+ *
4
+ * @since 0.4.0
5
+ */
6
+
7
+ import * as Cause from "effect/Cause"
8
+ import * as Channel from "effect/Channel"
9
+ import * as Context from "effect/Context"
10
+ import * as Deferred from "effect/Deferred"
11
+ import * as Effect from "effect/Effect"
12
+ import * as Exit from "effect/Exit"
13
+ import * as Fiber from "effect/Fiber"
14
+ import * as Option from "effect/Option"
15
+ import * as PubSub from "effect/PubSub"
16
+ import * as Queue from "effect/Queue"
17
+ import * as Scope from "effect/Scope"
18
+ import * as Stream from "effect/Stream"
19
+ import * as SynchronizedRef from "effect/SynchronizedRef"
20
+ import type * as Take from "effect/Take"
21
+ import { ChildAlreadyExistsError, StoppedError } from "./errors.js"
22
+
23
+ type ChildDescriptor = {
24
+ readonly id: string
25
+ readonly machine: object
26
+ }
27
+
28
+ type ChildEntry =
29
+ | {
30
+ readonly _tag: "Starting"
31
+ readonly token: symbol
32
+ readonly ownerKey?: string
33
+ readonly ownerPath?: string
34
+ ownerActive?: boolean
35
+ }
36
+ | {
37
+ readonly _tag: "Started"
38
+ readonly token: symbol
39
+ readonly descriptor: ChildDescriptor | undefined
40
+ readonly ref: MachineRef<any, any, any, any>
41
+ readonly ownerKey?: string
42
+ readonly ownerPath?: string
43
+ ownerActive?: boolean
44
+ }
45
+
46
+ type ChildSelector = string | ChildDescriptor
47
+ type ChildKey = string | symbol
48
+
49
+ /** @internal */
50
+ export const activeSnapshotObserver: unique symbol = Symbol.for("effect/Machine/activeSnapshotObserver")
51
+
52
+ /** @internal */
53
+ export const sendParentOverride: unique symbol = Symbol.for("effect/Machine/sendParentOverride")
54
+
55
+ /** @internal */
56
+ export const acknowledgedSend: unique symbol = Symbol.for("effect/Machine/acknowledgedSend")
57
+
58
+ /** @internal */
59
+ export interface AcknowledgedDelivery<State> {
60
+ readonly before: State
61
+ readonly plan: unknown
62
+ readonly after: State
63
+ }
64
+
65
+ const AcknowledgedMessageTypeId: unique symbol = Symbol("effect/Machine/AcknowledgedMessage")
66
+
67
+ /** @internal */
68
+ export interface AcknowledgedMessage<Event> {
69
+ readonly [AcknowledgedMessageTypeId]: true
70
+ readonly event: Event
71
+ readonly deferred: Deferred.Deferred<AcknowledgedDelivery<unknown>, unknown>
72
+ }
73
+
74
+ /** @internal */
75
+ export type ProcessMessage<Event> = Event | AcknowledgedMessage<Event>
76
+
77
+ /** @internal */
78
+ export const isAcknowledgedMessage = <Event>(
79
+ message: ProcessMessage<Event>
80
+ ): message is AcknowledgedMessage<Event> =>
81
+ typeof message === "object" && message !== null && AcknowledgedMessageTypeId in message
82
+
83
+ /** @internal */
84
+ export const messageEvent = <Event>(message: ProcessMessage<Event>): Event =>
85
+ isAcknowledgedMessage(message) ? message.event : message
86
+
87
+ const succeedAcknowledgedMessage = <State>(
88
+ message: ProcessMessage<unknown> | undefined,
89
+ delivery: AcknowledgedDelivery<State>
90
+ ): void => {
91
+ if (message !== undefined && isAcknowledgedMessage(message)) {
92
+ Deferred.doneUnsafe(message.deferred, Effect.succeed(delivery as AcknowledgedDelivery<unknown>))
93
+ }
94
+ }
95
+
96
+ const failAcknowledgedMessage = (
97
+ message: ProcessMessage<unknown> | undefined,
98
+ cause: Cause.Cause<unknown>
99
+ ): void => {
100
+ if (message !== undefined && isAcknowledgedMessage(message)) {
101
+ Deferred.doneUnsafe(message.deferred, Effect.failCause(cause))
102
+ }
103
+ }
104
+
105
+ const stopAcknowledgedMessage = (message: ProcessMessage<unknown> | undefined): void => {
106
+ if (message !== undefined && isAcknowledgedMessage(message)) {
107
+ Deferred.doneUnsafe(message.deferred, Effect.fail(new StoppedError()))
108
+ }
109
+ }
110
+
111
+ type ChildObservation = Option.Option<MachineRef<any, any, any, any>>
112
+ type ChildObservationBatch = [ChildObservation, ...Array<ChildObservation>]
113
+
114
+ interface ChildObserver {
115
+ readonly child: ChildSelector
116
+ readonly id: string
117
+ values: ChildObservationBatch | undefined
118
+ waiter: Deferred.Deferred<void> | undefined
119
+ }
120
+
121
+ const offerChildObservation = (
122
+ observer: ChildObserver,
123
+ value: ChildObservation
124
+ ): void => {
125
+ if (observer.values === undefined) {
126
+ observer.values = [value]
127
+ } else {
128
+ observer.values.push(value)
129
+ }
130
+ if (observer.waiter !== undefined) {
131
+ const waiter = observer.waiter
132
+ observer.waiter = undefined
133
+ Deferred.doneUnsafe(waiter, Effect.void)
134
+ }
135
+ }
136
+
137
+ const takeChildObservations = (
138
+ observer: ChildObserver
139
+ ): Effect.Effect<ChildObservationBatch> =>
140
+ Effect.suspend(() => {
141
+ if (observer.values !== undefined) {
142
+ const values = observer.values
143
+ observer.values = undefined
144
+ return Effect.succeed(values)
145
+ }
146
+ const waiter = Deferred.makeUnsafe<void>()
147
+ observer.waiter = waiter
148
+ return Deferred.await(waiter).pipe(Effect.andThen(takeChildObservations(observer)))
149
+ })
150
+
151
+ interface ChildRegistry {
152
+ closed: boolean
153
+ readonly children: Map<ChildKey, ChildEntry>
154
+ observers: Set<ChildObserver> | undefined
155
+ scope: Scope.Closeable | undefined
156
+ }
157
+
158
+ const matchesChild = (
159
+ entry: ChildEntry,
160
+ child: ChildSelector
161
+ ): entry is Extract<ChildEntry, { readonly _tag: "Started" }> =>
162
+ entry._tag === "Started" && (typeof child === "string" || (
163
+ entry.descriptor !== undefined &&
164
+ entry.descriptor.id === child.id &&
165
+ entry.descriptor.machine === child.machine
166
+ ))
167
+
168
+ const selectRegistryChild = (
169
+ registry: ChildRegistry,
170
+ id: string,
171
+ child: ChildSelector
172
+ ): ChildObservation => {
173
+ if (registry.closed) return Option.none()
174
+ const entry = registry.children.get(id)
175
+ return entry !== undefined && matchesChild(entry, child) ? Option.some(entry.ref) : Option.none()
176
+ }
177
+
178
+ const publishRegistryChange = (registry: ChildRegistry): void => {
179
+ if (registry.observers === undefined) return
180
+ for (const observer of registry.observers) {
181
+ offerChildObservation(observer, selectRegistryChild(registry, observer.id, observer.child))
182
+ }
183
+ }
184
+
185
+ const unregisterChild = (registry: ChildRegistry, key: ChildKey, token: symbol): void => {
186
+ const entry = registry.children.get(key)
187
+ if (entry === undefined || entry.token !== token) return
188
+ registry.children.delete(key)
189
+ if (typeof key === "string") publishRegistryChange(registry)
190
+ }
191
+
192
+ const registerChild = (
193
+ registry: ChildRegistry,
194
+ key: ChildKey,
195
+ token: symbol,
196
+ ref: MachineRef<any, any, any, any>,
197
+ descriptor: ChildDescriptor | undefined
198
+ ): boolean => {
199
+ const entry = registry.children.get(key)
200
+ if (registry.closed || entry === undefined || entry._tag !== "Starting" || entry.token !== token) {
201
+ return false
202
+ }
203
+ registry.children.delete(key)
204
+ const started: ChildEntry = entry.ownerKey === undefined
205
+ ? { _tag: "Started", token, descriptor, ref }
206
+ : {
207
+ _tag: "Started",
208
+ token,
209
+ descriptor,
210
+ ref,
211
+ ownerKey: entry.ownerKey,
212
+ ownerPath: entry.ownerPath!,
213
+ ownerActive: entry.ownerActive === true
214
+ }
215
+ registry.children.set(key, started)
216
+ if (typeof key === "string") publishRegistryChange(registry)
217
+ return true
218
+ }
219
+
220
+ export type RuntimeSnapshot<State, Error = never, Output = never> =
221
+ | {
222
+ readonly status: "active"
223
+ readonly state: State
224
+ }
225
+ | {
226
+ readonly status: "done"
227
+ readonly state: State
228
+ readonly output: Output
229
+ }
230
+ | {
231
+ readonly status: "error"
232
+ readonly state: State
233
+ readonly cause: Cause.Cause<Error>
234
+ }
235
+ | {
236
+ readonly status: "stopped"
237
+ readonly state: State
238
+ }
239
+
240
+ interface VersionedSnapshot<State, Error, Output> {
241
+ readonly revision: number
242
+ readonly snapshot: RuntimeSnapshot<State, Error, Output>
243
+ readonly terminalizing: boolean
244
+ readonly changes: PubSub.PubSub<Take.Take<VersionedSnapshot<State, Error, Output>>> | undefined
245
+ /** Compiled drains retain one non-empty publication chunk until their next Effect boundary. */
246
+ pendingChanges?: VersionedSnapshotBatch<State, Error, Output> | undefined
247
+ }
248
+
249
+ type VersionedSnapshotBatch<State, Error, Output> = [
250
+ VersionedSnapshot<State, Error, Output>,
251
+ ...Array<VersionedSnapshot<State, Error, Output>>
252
+ ]
253
+
254
+ export type RuntimeOutcome<State, Error = never, Output = never> =
255
+ | {
256
+ readonly _tag: "Done"
257
+ readonly output: Output
258
+ readonly snapshot: Extract<RuntimeSnapshot<State, Error, Output>, { readonly status: "done" }>
259
+ }
260
+ | {
261
+ readonly _tag: "Failure"
262
+ readonly error: Error
263
+ readonly cause: Cause.Cause<Error>
264
+ readonly snapshot: Extract<RuntimeSnapshot<State, Error, Output>, { readonly status: "error" }>
265
+ }
266
+ | {
267
+ readonly _tag: "Defect"
268
+ readonly defect: unknown
269
+ readonly cause: Cause.Cause<Error>
270
+ readonly snapshot: Extract<RuntimeSnapshot<State, Error, Output>, { readonly status: "error" }>
271
+ }
272
+ | {
273
+ readonly _tag: "Interrupted"
274
+ readonly cause: Cause.Cause<Error>
275
+ readonly snapshot: Extract<RuntimeSnapshot<State, Error, Output>, { readonly status: "error" }>
276
+ }
277
+ | {
278
+ readonly _tag: "Cause"
279
+ readonly cause: Cause.Cause<Error>
280
+ readonly snapshot: Extract<RuntimeSnapshot<State, Error, Output>, { readonly status: "error" }>
281
+ }
282
+ | {
283
+ readonly _tag: "Stopped"
284
+ readonly snapshot: Extract<RuntimeSnapshot<State, Error, Output>, { readonly status: "stopped" }>
285
+ }
286
+
287
+ export interface MachineRef<out State, in Event, out Error = never, out Output = never> {
288
+ readonly id: string
289
+ readonly sessionId: string
290
+ readonly state: Effect.Effect<State>
291
+ readonly snapshot: Effect.Effect<RuntimeSnapshot<State, Error, Output>>
292
+ readonly changes: Stream.Stream<RuntimeSnapshot<State, Error, Output>>
293
+ readonly join: Effect.Effect<Output, Error | StoppedError>
294
+ readonly stop: Effect.Effect<void>
295
+ readonly send: (event: Event) => Effect.Effect<void, StoppedError>
296
+ readonly [acknowledgedSend]?: (
297
+ event: Event
298
+ ) => Effect.Effect<AcknowledgedDelivery<State>, Error | StoppedError>
299
+ readonly child: (child: any) => Effect.Effect<Option.Option<any>>
300
+ readonly childChanges: (child: any) => Stream.Stream<Option.Option<any>>
301
+ }
302
+
303
+ interface ProcessAddress<in Event> {
304
+ readonly id: string
305
+ readonly sessionId: string
306
+ readonly stop: Effect.Effect<void>
307
+ readonly send: (event: Event) => Effect.Effect<void, StoppedError>
308
+ }
309
+
310
+ export interface ProcessScope<Event> {
311
+ readonly self: ProcessAddress<Event>
312
+ readonly parent: ProcessAddress<unknown> | undefined
313
+ readonly spawn: ProcessSpawn
314
+ readonly sendParent: (event: unknown) => Effect.Effect<void, StoppedError>
315
+ readonly sendTo: (child: ChildSelector, event: unknown) => Effect.Effect<void, StoppedError>
316
+ readonly stopChild: (child: ChildSelector) => Effect.Effect<void>
317
+ /** @internal */
318
+ readonly failCause: (cause: Cause.Cause<unknown>) => Effect.Effect<void>
319
+ }
320
+
321
+ export interface ProcessContext<State, Event> extends ProcessScope<Event> {
322
+ readonly receive: Effect.Effect<Event>
323
+ /** @internal */
324
+ readonly poll?: Effect.Effect<Option.Option<Event>>
325
+ /** @internal */
326
+ readonly receiveMessage?: Effect.Effect<ProcessMessage<Event>>
327
+ /** @internal */
328
+ readonly pollMessage?: Effect.Effect<Option.Option<ProcessMessage<Event>>>
329
+ /** @internal */
330
+ readonly completeMessage?: (delivery: AcknowledgedDelivery<State>) => void
331
+ readonly state: Effect.Effect<State>
332
+ readonly setState: (state: State) => Effect.Effect<void>
333
+ readonly updateState: <E, R>(
334
+ f: (state: State) => Effect.Effect<State, E, R>
335
+ ) => Effect.Effect<void, E, R>
336
+ /** Present only when a compiled statechart is forced through the generic runtime. @internal */
337
+ readonly ownedChildren?: OwnedChildRuntime
338
+ }
339
+
340
+ /**
341
+ * Owner-local execution context for compiled statecharts.
342
+ *
343
+ * Unlike `ProcessContext`, synchronous mailbox and state operations do not
344
+ * introduce an Effect boundary. The compiled drain still returns an Effect so
345
+ * actor commands, invokes, observation callbacks, interruption, and the Effect
346
+ * scheduler remain explicit at their actual boundaries.
347
+ *
348
+ * @internal
349
+ */
350
+ export interface CompiledProcessContext<State, Event> {
351
+ readonly scope: ProcessScope<Event>
352
+ readonly ownedChildren: OwnedChildRuntime
353
+ readonly poll: () => Option.Option<Event>
354
+ readonly pollMessage: () => Option.Option<ProcessMessage<Event>>
355
+ readonly state: () => State
356
+ readonly completeMessage: (delivery: AcknowledgedDelivery<State>) => void
357
+ readonly commit: (state: State) => Effect.Effect<void> | undefined
358
+ /**
359
+ * Publishes the current synchronous segment before continuing with work that
360
+ * may suspend, run user effects, or make the committed state observable.
361
+ */
362
+ readonly runAfterChanges: <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>
363
+ executionState: unknown
364
+ }
365
+
366
+ type CompiledProcessInitial<State, Output> =
367
+ | { readonly state: State; readonly done: false; readonly output: undefined }
368
+ | { readonly state: State; readonly done: true; readonly output: Output }
369
+ | {
370
+ readonly state: State
371
+ readonly done: boolean
372
+ readonly output: Output | undefined
373
+ readonly executionState: unknown
374
+ }
375
+
376
+ export type CompiledProcessDrain<State, Event, Error, Requirements, Output> =
377
+ | {
378
+ readonly _tag: "Process"
379
+ readonly run: (
380
+ context: ProcessContext<State, Event>
381
+ ) => Effect.Effect<Option.Option<Output>, Error, Requirements>
382
+ }
383
+ | {
384
+ readonly _tag: "Owned"
385
+ readonly run: (
386
+ context: CompiledProcessContext<State, Event>
387
+ ) => Effect.Effect<Option.Option<Output>, Error, Requirements>
388
+ }
389
+
390
+ /**
391
+ * The complete capability descriptor consumed by the compact process runtime.
392
+ * Generic process logic omits this field entirely.
393
+ *
394
+ * @internal
395
+ */
396
+ export interface CompiledProcessExecution<
397
+ State,
398
+ Event,
399
+ Error,
400
+ Requirements,
401
+ Output,
402
+ InitialError
403
+ > {
404
+ readonly _tag: "Compiled"
405
+ readonly childless: boolean
406
+ readonly initial?: (
407
+ scope: ProcessScope<Event>
408
+ ) => Effect.Effect<CompiledProcessInitial<State, Output>, InitialError, Requirements>
409
+ readonly initialSync?: (
410
+ scope: ProcessScope<Event>
411
+ ) => CompiledProcessInitial<State, Output>
412
+ readonly drain: CompiledProcessDrain<State, Event, Error, Requirements, Output>
413
+ }
414
+
415
+ export type ProcessExecution<State, Event, Error, Requirements, Output, InitialError> =
416
+ | {
417
+ readonly _tag: "Childless"
418
+ }
419
+ | CompiledProcessExecution<State, Event, Error, Requirements, Output, InitialError>
420
+
421
+ const executionIsChildless = (
422
+ execution: ProcessExecution<any, any, any, any, any, any> | undefined
423
+ ): boolean => execution?._tag === "Childless" || execution?.childless === true
424
+
425
+ interface CompactProcessMailbox<Event> {
426
+ items: Array<ProcessMessage<Event>> | undefined
427
+ index: number
428
+ closed: boolean
429
+ }
430
+
431
+ const offerCompactMailbox = <Event>(mailbox: CompactProcessMailbox<Event>, event: ProcessMessage<Event>): void => {
432
+ const items = mailbox.items ?? []
433
+ mailbox.items = items
434
+ items.push(event)
435
+ }
436
+
437
+ const pollCompactMailbox = <Event>(mailbox: CompactProcessMailbox<Event>): Option.Option<ProcessMessage<Event>> => {
438
+ if (mailbox.items === undefined) {
439
+ return Option.none()
440
+ }
441
+ const event = mailbox.items[mailbox.index]!
442
+ mailbox.index += 1
443
+ if (mailbox.index === mailbox.items.length) {
444
+ mailbox.items = undefined
445
+ mailbox.index = 0
446
+ }
447
+ return Option.some(event)
448
+ }
449
+
450
+ const closeCompactMailbox = (mailbox: CompactProcessMailbox<unknown>): void => {
451
+ if (mailbox.items !== undefined) {
452
+ for (let index = mailbox.index; index < mailbox.items.length; index += 1) {
453
+ stopAcknowledgedMessage(mailbox.items[index])
454
+ }
455
+ }
456
+ mailbox.closed = true
457
+ mailbox.items = undefined
458
+ mailbox.index = 0
459
+ }
460
+
461
+ export interface ProcessLogic<
462
+ State,
463
+ Event,
464
+ out Error = never,
465
+ out Requirements = never,
466
+ out Output = never,
467
+ out InitialError = never
468
+ > {
469
+ /** @internal */
470
+ readonly execution?: ProcessExecution<State, Event, Error, Requirements, Output, InitialError>
471
+ initial(scope: ProcessScope<Event>): Effect.Effect<State, InitialError, Requirements>
472
+ run(context: ProcessContext<State, Event>): Effect.Effect<Output, Error, Requirements>
473
+ }
474
+
475
+ export interface ProcessSpawn {
476
+ <ChildState, ChildEvent, ChildError, ChildRequirements, ChildOutput, ChildInitialError = never>(
477
+ logic: ProcessLogic<ChildState, ChildEvent, ChildError, ChildRequirements, ChildOutput, ChildInitialError>
478
+ ): Effect.Effect<
479
+ MachineRef<ChildState, ChildEvent, ChildError, ChildOutput>,
480
+ ChildInitialError,
481
+ Exclude<ChildRequirements, Scope.Scope>
482
+ >
483
+ <ChildState, ChildEvent, ChildError, ChildRequirements, ChildOutput, ChildInitialError = never>(
484
+ logic: ProcessLogic<ChildState, ChildEvent, ChildError, ChildRequirements, ChildOutput, ChildInitialError>,
485
+ options: {
486
+ readonly id: string
487
+ readonly descriptor?: ChildDescriptor
488
+ readonly onOutcome?: (
489
+ outcome: RuntimeOutcome<ChildState, ChildError, ChildOutput>
490
+ ) => Effect.Effect<void>
491
+ readonly [activeSnapshotObserver]?: (
492
+ snapshot: Extract<RuntimeSnapshot<ChildState, ChildError, ChildOutput>, { readonly status: "active" }>
493
+ ) => Effect.Effect<void>
494
+ readonly [sendParentOverride]?: (event: unknown) => Effect.Effect<void, StoppedError>
495
+ }
496
+ ): Effect.Effect<
497
+ MachineRef<ChildState, ChildEvent, ChildError, ChildOutput>,
498
+ ChildAlreadyExistsError | ChildInitialError,
499
+ Exclude<ChildRequirements, Scope.Scope>
500
+ >
501
+ }
502
+
503
+ export class MachineRuntime extends Context.Service<MachineRuntime, ProcessScope<any>>()(
504
+ "effect/Machine/MachineRuntime"
505
+ ) {}
506
+
507
+ export const provideMachineRuntime = <A, E, R, Event>(
508
+ effect: Effect.Effect<A, E, R>,
509
+ scope: ProcessScope<Event>
510
+ ): Effect.Effect<A, E, Exclude<R, MachineRuntime>> =>
511
+ Effect.provideService(effect, MachineRuntime, scope as ProcessScope<any>)
512
+
513
+ const classifyOutcome = <State, Error, Output>(
514
+ snapshot: RuntimeSnapshot<State, Error, Output>
515
+ ): RuntimeOutcome<State, Error, Output> | undefined => {
516
+ switch (snapshot.status) {
517
+ case "active": {
518
+ return undefined
519
+ }
520
+ case "done": {
521
+ return {
522
+ _tag: "Done",
523
+ output: snapshot.output,
524
+ snapshot
525
+ }
526
+ }
527
+ case "error": {
528
+ const failure = snapshot.cause.reasons.find(Cause.isFailReason)
529
+ if (failure !== undefined) {
530
+ return {
531
+ _tag: "Failure",
532
+ error: failure.error,
533
+ cause: snapshot.cause,
534
+ snapshot
535
+ }
536
+ }
537
+ const defect = snapshot.cause.reasons.find(Cause.isDieReason)
538
+ if (defect !== undefined) {
539
+ return {
540
+ _tag: "Defect",
541
+ defect: defect.defect,
542
+ cause: snapshot.cause,
543
+ snapshot
544
+ }
545
+ }
546
+ const interrupted = snapshot.cause.reasons.find(Cause.isInterruptReason)
547
+ if (interrupted !== undefined) {
548
+ return {
549
+ _tag: "Interrupted",
550
+ cause: snapshot.cause,
551
+ snapshot
552
+ }
553
+ }
554
+ return {
555
+ _tag: "Cause",
556
+ cause: snapshot.cause,
557
+ snapshot
558
+ }
559
+ }
560
+ case "stopped": {
561
+ return {
562
+ _tag: "Stopped",
563
+ snapshot
564
+ }
565
+ }
566
+ }
567
+ }
568
+
569
+ const notifyActiveSnapshot = <State, Error, Output>(
570
+ onSnapshot: (
571
+ snapshot: Extract<RuntimeSnapshot<State, Error, Output>, { readonly status: "active" }>
572
+ ) => Effect.Effect<void>,
573
+ snapshot: Extract<RuntimeSnapshot<State, Error, Output>, { readonly status: "active" }>
574
+ ): Effect.Effect<void> =>
575
+ Effect.suspend(() => onSnapshot(snapshot)).pipe(
576
+ Effect.exit,
577
+ Effect.asVoid
578
+ )
579
+
580
+ export const watch = <State, Event, Error = never, Output = never>(
581
+ ref: MachineRef<State, Event, Error, Output>
582
+ ): Stream.Stream<RuntimeOutcome<State, Error, Output>> =>
583
+ ref.changes.pipe(
584
+ Stream.filter((snapshot) => snapshot.status !== "active"),
585
+ Stream.map((snapshot) => classifyOutcome(snapshot)!),
586
+ Stream.take(1)
587
+ )
588
+
589
+ interface ProcessRuntime {
590
+ readonly nextSessionId: Effect.Effect<string>
591
+ }
592
+
593
+ const makeProcessRuntime: Effect.Effect<ProcessRuntime> = Effect.sync(() => {
594
+ let sessionIdCounter = 0
595
+ return {
596
+ nextSessionId: Effect.sync(() => `machine:${sessionIdCounter++}`)
597
+ }
598
+ })
599
+
600
+ interface StartInternalOptions {
601
+ readonly detached?: boolean
602
+ readonly id?: string
603
+ readonly onOutcome?: (outcome: RuntimeOutcome<any, any, any>) => Effect.Effect<void>
604
+ readonly onSnapshot?: (
605
+ snapshot: Extract<RuntimeSnapshot<any, any, any>, { readonly status: "active" }>
606
+ ) => Effect.Effect<void>
607
+ readonly onReady?: (
608
+ ref: MachineRef<any, any, any, any>,
609
+ requestStop: Effect.Effect<void>
610
+ ) => Effect.Effect<void>
611
+ readonly onReadySync?: (ref: MachineRef<any, any, any, any>) => boolean
612
+ readonly onStop?: Effect.Effect<void>
613
+ readonly onStopSync?: () => void
614
+ readonly skipStoppedOutcome?: boolean
615
+ readonly parent?: ProcessAddress<unknown>
616
+ readonly runtime: ProcessRuntime
617
+ readonly sendParent?: (event: unknown) => Effect.Effect<void, StoppedError>
618
+ }
619
+
620
+ /** @internal */
621
+ export interface OwnedChildSpawnOptions {
622
+ readonly key: string
623
+ readonly path: string
624
+ readonly id: string
625
+ readonly duplicateId: string
626
+ readonly descriptor?: ChildDescriptor
627
+ readonly onOutcome: (
628
+ isCurrent: () => boolean,
629
+ outcome: RuntimeOutcome<any, any, any>
630
+ ) => Effect.Effect<void>
631
+ readonly onSnapshot?: (
632
+ isCurrent: () => boolean,
633
+ snapshot: Extract<RuntimeSnapshot<any, any, any>, { readonly status: "active" }>
634
+ ) => Effect.Effect<void>
635
+ readonly sendParent: (
636
+ isCurrent: () => boolean,
637
+ event: unknown
638
+ ) => Effect.Effect<void, StoppedError>
639
+ }
640
+
641
+ /** @internal */
642
+ export interface OwnedChildRuntime {
643
+ readonly spawn: (
644
+ makeLogic: () => ProcessLogic<any, any, any, any, any, any>,
645
+ options: OwnedChildSpawnOptions
646
+ ) => Effect.Effect<void, any, any>
647
+ readonly stopAll: () => Effect.Effect<void>
648
+ readonly stopPaths: (paths: ReadonlyArray<string>) => Effect.Effect<void> | undefined
649
+ }
650
+
651
+ interface ChildRuntime {
652
+ readonly close: <A, E>(exit: Exit.Exit<A, E>) => Effect.Effect<void>
653
+ readonly spawn: ProcessSpawn
654
+ readonly get: <ChildState, ChildEvent, ChildError, ChildOutput>(
655
+ child: ChildSelector
656
+ ) => Effect.Effect<Option.Option<MachineRef<ChildState, ChildEvent, ChildError, ChildOutput>>>
657
+ readonly changes: <ChildState, ChildEvent, ChildError, ChildOutput>(
658
+ child: ChildSelector
659
+ ) => Stream.Stream<Option.Option<MachineRef<ChildState, ChildEvent, ChildError, ChildOutput>>>
660
+ readonly sendTo: (child: ChildSelector, event: unknown) => Effect.Effect<void, StoppedError>
661
+ readonly stop: (child: ChildSelector) => Effect.Effect<void>
662
+ readonly owned: OwnedChildRuntime
663
+ }
664
+
665
+ class OwnedChildRuntimeImpl implements OwnedChildRuntime {
666
+ private scopedServices: Context.Context<any> | undefined
667
+
668
+ constructor(
669
+ private readonly registry: ChildRegistry,
670
+ private readonly self: ProcessAddress<any>,
671
+ private readonly runtime: ProcessRuntime,
672
+ private readonly services?: Context.Context<any>
673
+ ) {}
674
+
675
+ private has(key: string): boolean {
676
+ for (const entry of this.registry.children.values()) {
677
+ if (entry.ownerActive && entry.ownerKey === key) return true
678
+ }
679
+ return false
680
+ }
681
+
682
+ private stopEntry(entry: ChildEntry): Effect.Effect<void> {
683
+ entry.ownerActive = false
684
+ return entry._tag === "Started" ? entry.ref.stop : Effect.void
685
+ }
686
+
687
+ spawn(
688
+ makeLogic: () => ProcessLogic<any, any, any, any, any, any>,
689
+ options: OwnedChildSpawnOptions
690
+ ): Effect.Effect<void, any, any> {
691
+ const token = Symbol()
692
+ let startedChild: MachineRef<any, any, any, any> | undefined
693
+ const isCurrent = (): boolean => {
694
+ const entry = this.registry.children.get(options.id)
695
+ return entry?.token === token && entry.ownerKey === options.key && entry.ownerActive === true
696
+ }
697
+ return Effect.suspend(() => {
698
+ if (this.registry.closed) return Effect.interrupt
699
+ if (this.has(options.key) || this.registry.children.has(options.id)) {
700
+ return Effect.fail(new ChildAlreadyExistsError({ id: options.duplicateId }))
701
+ }
702
+ const logic = makeLogic()
703
+ const scope = this.registry.scope ??= Scope.makeUnsafe("parallel")
704
+ this.registry.children.set(options.id, {
705
+ _tag: "Starting",
706
+ token,
707
+ ownerKey: options.key,
708
+ ownerPath: options.path,
709
+ ownerActive: true
710
+ })
711
+ const startOptions: StartInternalOptions = {
712
+ detached: true,
713
+ id: options.id,
714
+ sendParent: (event) => options.sendParent(isCurrent, event),
715
+ onOutcome: (outcome) => options.onOutcome(isCurrent, outcome),
716
+ ...(options.onSnapshot === undefined
717
+ ? undefined
718
+ : { onSnapshot: (snapshot) => options.onSnapshot!(isCurrent, snapshot) }),
719
+ onReadySync: (child) => {
720
+ startedChild = child
721
+ return registerChild(this.registry, options.id, token, child, options.descriptor)
722
+ },
723
+ onStopSync: () => unregisterChild(this.registry, options.id, token),
724
+ skipStoppedOutcome: true,
725
+ parent: this.self,
726
+ runtime: this.runtime
727
+ }
728
+ const execution = logic.execution
729
+ const synchronous = this.services !== undefined && options.onSnapshot === undefined &&
730
+ execution?._tag === "Compiled" && execution.childless && execution.drain._tag === "Owned" &&
731
+ execution.initialSync !== undefined
732
+ const start = synchronous
733
+ ? Effect.flatMap(
734
+ this.runtime.nextSessionId,
735
+ (sessionId) =>
736
+ new CompiledProcess(
737
+ logic,
738
+ startOptions,
739
+ this.scopedServices ??= Context.add(this.services!, Scope.Scope, scope),
740
+ sessionId
741
+ ).initializeCompiledSync()
742
+ )
743
+ : startLogicInternal(logic, startOptions)
744
+ const guarded = start.pipe(
745
+ Effect.onExit((exit) => {
746
+ if (Exit.isSuccess(exit)) return Effect.void
747
+ unregisterChild(this.registry, options.id, token)
748
+ return startedChild === undefined ? Effect.void : startedChild.stop
749
+ })
750
+ )
751
+ return (synchronous ? guarded : Scope.provide(guarded, scope)).pipe(Effect.asVoid)
752
+ })
753
+ }
754
+
755
+ stopAll(): Effect.Effect<void> {
756
+ return Effect.suspend(() => {
757
+ const effects: Array<Effect.Effect<void>> = []
758
+ for (const entry of this.registry.children.values()) {
759
+ if (entry.ownerActive) effects.push(this.stopEntry(entry))
760
+ }
761
+ return effects.length === 0
762
+ ? Effect.void
763
+ : effects.length === 1
764
+ ? effects[0]!
765
+ : Effect.all(effects, { concurrency: "unbounded", discard: true })
766
+ })
767
+ }
768
+
769
+ stopPaths(paths: ReadonlyArray<string>): Effect.Effect<void> | undefined {
770
+ if (paths.length === 0) return undefined
771
+ const pathSet = new Set(paths)
772
+ const effects: Array<Effect.Effect<void>> = []
773
+ for (const entry of this.registry.children.values()) {
774
+ if (entry.ownerActive && entry.ownerPath !== undefined && pathSet.has(entry.ownerPath)) {
775
+ effects.push(this.stopEntry(entry))
776
+ }
777
+ }
778
+ return effects.length === 0
779
+ ? undefined
780
+ : effects.length === 1
781
+ ? effects[0]!
782
+ : Effect.all(effects, { concurrency: "unbounded", discard: true })
783
+ }
784
+ }
785
+
786
+ const noChildChanges = Stream.succeed(Option.none()).pipe(Stream.concat(Stream.never))
787
+ const noParentSend = (_event: unknown): Effect.Effect<void, StoppedError> => Effect.void
788
+
789
+ const childlessRuntime: ChildRuntime = {
790
+ close: () => Effect.void,
791
+ spawn: (() => Effect.die(new Error("Childless machine logic cannot spawn a process"))) as ProcessSpawn,
792
+ get: () => Effect.succeed(Option.none()),
793
+ changes: () => noChildChanges,
794
+ sendTo: () => Effect.void,
795
+ stop: () => Effect.void,
796
+ owned: {
797
+ spawn: () => Effect.die(new Error("Childless machine logic cannot spawn an owned process")),
798
+ stopAll: () => Effect.void,
799
+ stopPaths: () => undefined
800
+ }
801
+ }
802
+
803
+ const makeChildRuntimeSync = (
804
+ self: ProcessAddress<any>,
805
+ runtime: ProcessRuntime,
806
+ services?: Context.Context<any>
807
+ ): ChildRuntime => {
808
+ // Child-registry decisions are synchronous and every access below runs in
809
+ // one Effect.sync / Effect.suspend step. Keep the unobserved representation
810
+ // compact; selector-specific handoffs are installed only while
811
+ // childChanges streams are running.
812
+ const registry: ChildRegistry = {
813
+ closed: false,
814
+ children: new Map(),
815
+ observers: undefined,
816
+ scope: undefined
817
+ }
818
+
819
+ const close = <A, E>(exit: Exit.Exit<A, E>): Effect.Effect<void> =>
820
+ Effect.suspend(() => {
821
+ if (registry.closed) {
822
+ return Effect.void
823
+ }
824
+ registry.closed = true
825
+ if (registry.scope === undefined) {
826
+ return Effect.void
827
+ }
828
+ const finalizers = Scope.closeUnsafe(registry.scope, exit)
829
+ let first: Effect.Effect<void> | undefined
830
+ let rest: Array<Effect.Effect<void>> | undefined
831
+ for (const entry of registry.children.values()) {
832
+ if (entry._tag !== "Started") {
833
+ continue
834
+ }
835
+ if (first === undefined) {
836
+ first = entry.ref.stop
837
+ } else {
838
+ rest ??= [first]
839
+ rest.push(entry.ref.stop)
840
+ }
841
+ }
842
+ if (finalizers !== undefined) {
843
+ if (first === undefined) {
844
+ first = finalizers
845
+ } else {
846
+ rest ??= [first]
847
+ rest.push(finalizers)
848
+ }
849
+ }
850
+ const cleanup = rest ?? first
851
+ return cleanup === undefined
852
+ ? Effect.void
853
+ : Array.isArray(cleanup)
854
+ ? Effect.all(cleanup, { concurrency: "unbounded", discard: true })
855
+ : cleanup
856
+ })
857
+
858
+ const unregister = (
859
+ key: ChildKey,
860
+ token: symbol
861
+ ): Effect.Effect<void> => Effect.sync(() => unregisterChild(registry, key, token))
862
+
863
+ const register = (
864
+ key: ChildKey,
865
+ token: symbol,
866
+ ref: MachineRef<any, any, any, any>,
867
+ descriptor: ChildDescriptor | undefined
868
+ ): Effect.Effect<boolean> => Effect.sync(() => registerChild(registry, key, token, ref, descriptor))
869
+
870
+ const get: ChildRuntime["get"] = (child) => {
871
+ const id = typeof child === "string" ? child : child.id
872
+ return Effect.sync(() => {
873
+ if (registry.closed) {
874
+ return Option.none()
875
+ }
876
+ const entry = registry.children.get(id)
877
+ return entry !== undefined && matchesChild(entry, child)
878
+ ? Option.some(entry.ref)
879
+ : Option.none()
880
+ })
881
+ }
882
+
883
+ const changes: ChildRuntime["changes"] = (child) => {
884
+ const id = typeof child === "string" ? child : child.id
885
+ return Stream.fromChannel(
886
+ Channel.fromTransform((_, streamScope) =>
887
+ Effect.sync((): ChildObserver => ({ child, id, values: undefined, waiter: undefined })).pipe(
888
+ Effect.flatMap((observer) => {
889
+ const removeObserver = Effect.sync(() => {
890
+ if (registry.observers !== undefined) {
891
+ registry.observers.delete(observer)
892
+ if (registry.observers.size === 0) {
893
+ registry.observers = undefined
894
+ }
895
+ }
896
+ observer.values = undefined
897
+ observer.waiter = undefined
898
+ })
899
+ return Scope.addFinalizer(streamScope, removeObserver).pipe(
900
+ Effect.andThen(
901
+ Effect.sync(() => {
902
+ if (!registry.closed && streamScope.state._tag !== "Closed") {
903
+ registry.observers ??= new Set()
904
+ registry.observers.add(observer)
905
+ }
906
+ offerChildObservation(observer, selectRegistryChild(registry, id, child))
907
+ })
908
+ ),
909
+ Effect.as(takeChildObservations(observer))
910
+ )
911
+ })
912
+ )
913
+ )
914
+ )
915
+ }
916
+
917
+ const sendTo = (child: ChildSelector, event: unknown): Effect.Effect<void, StoppedError> => {
918
+ const id = typeof child === "string" ? child : child.id
919
+ return Effect.suspend(() => {
920
+ if (registry.closed) {
921
+ return Effect.void
922
+ }
923
+ const entry = registry.children.get(id)
924
+ return entry !== undefined && matchesChild(entry, child)
925
+ ? entry.ref.send(event)
926
+ : Effect.void
927
+ })
928
+ }
929
+
930
+ const stop = (child: ChildSelector): Effect.Effect<void> => {
931
+ const id = typeof child === "string" ? child : child.id
932
+ return Effect.suspend(() => {
933
+ if (registry.closed) {
934
+ return Effect.void
935
+ }
936
+ const entry = registry.children.get(id)
937
+ return entry !== undefined && matchesChild(entry, child)
938
+ ? entry.ref.stop
939
+ : Effect.void
940
+ })
941
+ }
942
+
943
+ function spawn<ChildState, ChildEvent, ChildError, ChildRequirements, ChildOutput, ChildInitialError = never>(
944
+ logic: ProcessLogic<ChildState, ChildEvent, ChildError, ChildRequirements, ChildOutput, ChildInitialError>
945
+ ): Effect.Effect<
946
+ MachineRef<ChildState, ChildEvent, ChildError, ChildOutput>,
947
+ ChildInitialError,
948
+ Exclude<ChildRequirements, Scope.Scope>
949
+ >
950
+ function spawn<ChildState, ChildEvent, ChildError, ChildRequirements, ChildOutput, ChildInitialError = never>(
951
+ logic: ProcessLogic<ChildState, ChildEvent, ChildError, ChildRequirements, ChildOutput, ChildInitialError>,
952
+ spawnOptions: {
953
+ readonly id: string
954
+ readonly descriptor?: ChildDescriptor
955
+ readonly onOutcome?: (
956
+ outcome: RuntimeOutcome<ChildState, ChildError, ChildOutput>
957
+ ) => Effect.Effect<void>
958
+ readonly [activeSnapshotObserver]?: (
959
+ snapshot: Extract<RuntimeSnapshot<ChildState, ChildError, ChildOutput>, { readonly status: "active" }>
960
+ ) => Effect.Effect<void>
961
+ readonly [sendParentOverride]?: (event: unknown) => Effect.Effect<void, StoppedError>
962
+ }
963
+ ): Effect.Effect<
964
+ MachineRef<ChildState, ChildEvent, ChildError, ChildOutput>,
965
+ ChildAlreadyExistsError | ChildInitialError,
966
+ Exclude<ChildRequirements, Scope.Scope>
967
+ >
968
+ function spawn<ChildState, ChildEvent, ChildError, ChildRequirements, ChildOutput, ChildInitialError = never>(
969
+ logic: ProcessLogic<ChildState, ChildEvent, ChildError, ChildRequirements, ChildOutput, ChildInitialError>,
970
+ spawnOptions?: {
971
+ readonly id: string
972
+ readonly descriptor?: ChildDescriptor
973
+ readonly onOutcome?: (
974
+ outcome: RuntimeOutcome<ChildState, ChildError, ChildOutput>
975
+ ) => Effect.Effect<void>
976
+ readonly [activeSnapshotObserver]?: (
977
+ snapshot: Extract<RuntimeSnapshot<ChildState, ChildError, ChildOutput>, { readonly status: "active" }>
978
+ ) => Effect.Effect<void>
979
+ readonly [sendParentOverride]?: (event: unknown) => Effect.Effect<void, StoppedError>
980
+ }
981
+ ): Effect.Effect<
982
+ MachineRef<ChildState, ChildEvent, ChildError, ChildOutput>,
983
+ ChildAlreadyExistsError | ChildInitialError,
984
+ Exclude<ChildRequirements, Scope.Scope>
985
+ > {
986
+ const token = Symbol()
987
+ const key = spawnOptions?.id ?? token
988
+ let startedChild: MachineRef<any, any, any, any> | undefined
989
+ return Effect.suspend((): Effect.Effect<
990
+ MachineRef<ChildState, ChildEvent, ChildError, ChildOutput>,
991
+ ChildAlreadyExistsError | ChildInitialError,
992
+ Exclude<ChildRequirements, Scope.Scope>
993
+ > => {
994
+ if (registry.closed) {
995
+ return Effect.interrupt
996
+ }
997
+ if (typeof key === "string" && registry.children.has(key)) {
998
+ return Effect.fail(new ChildAlreadyExistsError({ id: key }))
999
+ }
1000
+ registry.scope ??= Scope.makeUnsafe("parallel")
1001
+ registry.children.set(key, { _tag: "Starting", token })
1002
+ return startLogicInternal(logic, {
1003
+ detached: true,
1004
+ ...(spawnOptions?.id === undefined ? undefined : { id: spawnOptions.id }),
1005
+ ...(spawnOptions?.onOutcome === undefined ? undefined : { onOutcome: spawnOptions.onOutcome }),
1006
+ ...(spawnOptions?.[activeSnapshotObserver] === undefined
1007
+ ? undefined
1008
+ : { onSnapshot: spawnOptions[activeSnapshotObserver] }),
1009
+ ...(spawnOptions?.[sendParentOverride] === undefined
1010
+ ? undefined
1011
+ : { sendParent: spawnOptions[sendParentOverride] }),
1012
+ onReady: (child, requestChildStop) =>
1013
+ Effect.sync(() => {
1014
+ startedChild = child
1015
+ }).pipe(
1016
+ Effect.andThen(register(key, token, child, spawnOptions?.descriptor)),
1017
+ Effect.flatMap((registered) => registered ? Effect.void : requestChildStop)
1018
+ ),
1019
+ onStop: unregister(key, token),
1020
+ parent: self,
1021
+ runtime
1022
+ }).pipe(
1023
+ Effect.onExit((exit) =>
1024
+ Exit.isFailure(exit)
1025
+ ? unregister(key, token).pipe(
1026
+ Effect.andThen(startedChild === undefined ? Effect.void : startedChild.stop)
1027
+ )
1028
+ : Effect.void
1029
+ ),
1030
+ Scope.provide(registry.scope)
1031
+ )
1032
+ })
1033
+ }
1034
+
1035
+ return {
1036
+ close,
1037
+ spawn,
1038
+ get,
1039
+ changes,
1040
+ sendTo,
1041
+ stop,
1042
+ owned: new OwnedChildRuntimeImpl(registry, self, runtime, services)
1043
+ }
1044
+ }
1045
+
1046
+ const makeChildRuntime = (
1047
+ self: ProcessAddress<any>,
1048
+ runtime: ProcessRuntime,
1049
+ services?: Context.Context<any>
1050
+ ): Effect.Effect<ChildRuntime> => Effect.sync(() => makeChildRuntimeSync(self, runtime, services))
1051
+
1052
+ // `Machine.logic` permits an arbitrary Effect program, including programs that
1053
+ // suspend or supervise their own fibers. Keep its two-fiber worker/supervisor
1054
+ // protocol as the general contract rather than weakening it for statecharts.
1055
+ const startGenericInternal: <
1056
+ State,
1057
+ Event,
1058
+ Error = never,
1059
+ Requirements = never,
1060
+ Output = never,
1061
+ InitialError = never
1062
+ >(
1063
+ logic: ProcessLogic<State, Event, Error, Requirements, Output, InitialError>,
1064
+ options: StartInternalOptions
1065
+ ) => Effect.Effect<
1066
+ MachineRef<State, Event, Error, Output>,
1067
+ InitialError,
1068
+ Requirements
1069
+ > = Effect.fnUntraced(function*<State, Event, Error, Requirements, Output, InitialError>(
1070
+ logic: ProcessLogic<State, Event, Error, Requirements, Output, InitialError>,
1071
+ options: StartInternalOptions
1072
+ ) {
1073
+ const {
1074
+ detached,
1075
+ id: requestedId,
1076
+ onOutcome,
1077
+ onReady,
1078
+ onReadySync,
1079
+ onSnapshot,
1080
+ onStop,
1081
+ onStopSync,
1082
+ parent,
1083
+ runtime,
1084
+ sendParent: overrideSendParent
1085
+ } = options
1086
+ type ProcessTermination =
1087
+ | { readonly _tag: "Stopped" }
1088
+ | { readonly _tag: "Done"; readonly output: Output }
1089
+ | { readonly _tag: "Failure"; readonly cause: Cause.Cause<Error> }
1090
+
1091
+ const sessionId = yield* runtime.nextSessionId
1092
+ const id = requestedId ?? sessionId
1093
+ const queue = yield* Queue.unbounded<ProcessMessage<Event>>()
1094
+ const termination = yield* Deferred.make<ProcessTermination>()
1095
+ const done = yield* Deferred.make<Output, Error | StoppedError>()
1096
+ const awaitCompletion = Deferred.await(done).pipe(Effect.exit, Effect.asVoid)
1097
+ let initializing = true
1098
+ let inFlightMessage: ProcessMessage<Event> | undefined
1099
+ const requestStop = Deferred.succeed(termination, { _tag: "Stopped" }).pipe(Effect.asVoid)
1100
+ const self: ProcessAddress<Event> = {
1101
+ id,
1102
+ sessionId,
1103
+ // Initialization must finish constructing a state before a stopped
1104
+ // snapshot can be published. A stop requested there is therefore recorded
1105
+ // and returns so initialization can finish. Once running, the requesting
1106
+ // process waits forever and is interrupted by the supervisor after the
1107
+ // stop request wins, so execution never continues after `self.stop`.
1108
+ stop: Effect.suspend(() =>
1109
+ initializing
1110
+ ? requestStop
1111
+ : requestStop.pipe(Effect.andThen(Effect.never))
1112
+ ),
1113
+ send: (event: Event) =>
1114
+ Queue.offer(queue, event).pipe(
1115
+ Effect.flatMap((accepted) => accepted ? Effect.void : Effect.fail(new StoppedError()))
1116
+ )
1117
+ }
1118
+ const sendAcknowledged:
1119
+ | ((event: Event) => Effect.Effect<AcknowledgedDelivery<State>, Error | StoppedError>)
1120
+ | undefined = logic.execution?._tag !== "Compiled"
1121
+ ? undefined
1122
+ : (event) =>
1123
+ Effect.uninterruptibleMask((restore) =>
1124
+ Deferred.make<AcknowledgedDelivery<unknown>, unknown>().pipe(
1125
+ Effect.flatMap((deferred) =>
1126
+ Queue.offer(queue, {
1127
+ [AcknowledgedMessageTypeId]: true as const,
1128
+ event,
1129
+ deferred
1130
+ }).pipe(
1131
+ Effect.flatMap((accepted) =>
1132
+ accepted
1133
+ ? restore(Deferred.await(deferred))
1134
+ : Effect.fail(new StoppedError())
1135
+ )
1136
+ )
1137
+ ),
1138
+ Effect.map((delivery) => delivery as AcknowledgedDelivery<State>)
1139
+ )
1140
+ ) as Effect.Effect<AcknowledgedDelivery<State>, Error | StoppedError>
1141
+
1142
+ let {
1143
+ changes: childChanges,
1144
+ close: closeChildren,
1145
+ get: getChild,
1146
+ owned: ownedChildren,
1147
+ sendTo,
1148
+ spawn,
1149
+ stop: stopChild
1150
+ } = childlessRuntime
1151
+ if (!executionIsChildless(logic.execution)) {
1152
+ ;({
1153
+ changes: childChanges,
1154
+ close: closeChildren,
1155
+ get: getChild,
1156
+ owned: ownedChildren,
1157
+ sendTo,
1158
+ spawn,
1159
+ stop: stopChild
1160
+ } = yield* makeChildRuntime(self, runtime))
1161
+ }
1162
+ const cleanupStartupFailure = <A, E>(exit: Exit.Exit<A, E>): Effect.Effect<void> =>
1163
+ Exit.isFailure(exit)
1164
+ ? closeChildren(exit)
1165
+ : Effect.void
1166
+ const cleanup = onStopSync === undefined ? onStop ?? Effect.void : Effect.sync(onStopSync)
1167
+ const sendParent = overrideSendParent ?? (parent === undefined ? noParentSend : parent.send)
1168
+
1169
+ const scope: ProcessScope<Event> = {
1170
+ self,
1171
+ parent,
1172
+ spawn,
1173
+ sendParent,
1174
+ sendTo,
1175
+ stopChild,
1176
+ failCause: (cause) =>
1177
+ Deferred.succeed(termination, {
1178
+ _tag: "Failure",
1179
+ cause: cause as Cause.Cause<Error>
1180
+ }).pipe(Effect.asVoid)
1181
+ }
1182
+
1183
+ const initial = yield* logic.initial(scope).pipe(
1184
+ Effect.onExit(cleanupStartupFailure),
1185
+ Effect.ensuring(Effect.sync(() => {
1186
+ initializing = false
1187
+ }))
1188
+ )
1189
+ const current = yield* SynchronizedRef.make<VersionedSnapshot<State, Error, Output>>({
1190
+ revision: 0,
1191
+ terminalizing: false,
1192
+ changes: undefined,
1193
+ snapshot: {
1194
+ status: "active",
1195
+ state: initial
1196
+ }
1197
+ })
1198
+ const publishSnapshot: (
1199
+ snapshot: VersionedSnapshot<State, Error, Output>
1200
+ ) => Effect.Effect<VersionedSnapshot<State, Error, Output>> = onSnapshot === undefined
1201
+ ? (snapshot) =>
1202
+ snapshot.changes === undefined
1203
+ ? Effect.succeed(snapshot)
1204
+ : PubSub.publish(snapshot.changes, [snapshot] as const).pipe(Effect.as(snapshot))
1205
+ : (snapshot) => {
1206
+ const publish = snapshot.changes === undefined
1207
+ ? Effect.succeed(snapshot)
1208
+ : PubSub.publish(snapshot.changes, [snapshot] as const).pipe(Effect.as(snapshot))
1209
+ const runtimeSnapshot = snapshot.snapshot
1210
+ return runtimeSnapshot.status !== "active"
1211
+ ? publish
1212
+ : publish.pipe(Effect.tap(() => notifyActiveSnapshot(onSnapshot, runtimeSnapshot)))
1213
+ }
1214
+
1215
+ const completeChanges = (
1216
+ snapshot: VersionedSnapshot<State, Error, Output>
1217
+ ): Effect.Effect<void> =>
1218
+ snapshot.changes === undefined
1219
+ ? Effect.void
1220
+ : PubSub.publish(snapshot.changes, Exit.succeed<void>(undefined)).pipe(Effect.asVoid)
1221
+
1222
+ const completeIfTerminal = (
1223
+ snapshot: VersionedSnapshot<State, Error, Output>
1224
+ ): Effect.Effect<VersionedSnapshot<State, Error, Output>> => {
1225
+ if (snapshot.snapshot.status === "active") {
1226
+ return Effect.succeed(snapshot)
1227
+ }
1228
+ return completeChanges(snapshot).pipe(Effect.as(snapshot))
1229
+ }
1230
+
1231
+ const publishIfCurrent = (
1232
+ snapshot: VersionedSnapshot<State, Error, Output>
1233
+ ): Effect.Effect<VersionedSnapshot<State, Error, Output> | undefined> =>
1234
+ SynchronizedRef.get(current).pipe(
1235
+ Effect.flatMap((
1236
+ currentSnapshot
1237
+ ): Effect.Effect<VersionedSnapshot<State, Error, Output> | undefined> =>
1238
+ currentSnapshot.revision === snapshot.revision
1239
+ ? publishSnapshot(snapshot).pipe(Effect.flatMap(completeIfTerminal))
1240
+ : Effect.succeed(undefined)
1241
+ )
1242
+ )
1243
+
1244
+ type SnapshotModification = readonly [
1245
+ VersionedSnapshot<State, Error, Output> | undefined,
1246
+ VersionedSnapshot<State, Error, Output>
1247
+ ]
1248
+
1249
+ const updateSnapshot = <E2, R2>(
1250
+ f: (
1251
+ snapshot: RuntimeSnapshot<State, Error, Output>
1252
+ ) => Effect.Effect<RuntimeSnapshot<State, Error, Output> | undefined, E2, R2>
1253
+ ): Effect.Effect<RuntimeSnapshot<State, Error, Output> | undefined, E2, R2> =>
1254
+ SynchronizedRef.modifyEffect(
1255
+ current,
1256
+ (current) =>
1257
+ current.terminalizing
1258
+ ? Effect.succeed([undefined, current] as const)
1259
+ : Effect.map(
1260
+ f(current.snapshot),
1261
+ (next) => {
1262
+ if (next === undefined) {
1263
+ return [undefined, current] as const
1264
+ }
1265
+ const versioned = {
1266
+ revision: current.revision + 1,
1267
+ snapshot: next,
1268
+ terminalizing: false,
1269
+ changes: current.changes
1270
+ }
1271
+ return [versioned, versioned] as const
1272
+ }
1273
+ )
1274
+ ).pipe(
1275
+ Effect.flatMap((versioned) => versioned === undefined ? Effect.succeed(undefined) : publishIfCurrent(versioned)),
1276
+ Effect.map((published) => published?.snapshot)
1277
+ )
1278
+
1279
+ const reserveTerminalSnapshot = (
1280
+ f: (
1281
+ snapshot: Extract<RuntimeSnapshot<State, Error, Output>, { readonly status: "active" }>
1282
+ ) => RuntimeSnapshot<State, Error, Output>
1283
+ ): Effect.Effect<RuntimeSnapshot<State, Error, Output> | undefined> =>
1284
+ SynchronizedRef.modify(
1285
+ current,
1286
+ (current): SnapshotModification => {
1287
+ if (current.terminalizing || current.snapshot.status !== "active") {
1288
+ return [undefined, current]
1289
+ }
1290
+ return [
1291
+ {
1292
+ revision: current.revision + 1,
1293
+ snapshot: f(current.snapshot),
1294
+ terminalizing: true,
1295
+ changes: current.changes
1296
+ },
1297
+ { ...current, terminalizing: true }
1298
+ ]
1299
+ }
1300
+ ).pipe(Effect.map((versioned) => versioned?.snapshot))
1301
+
1302
+ const setAndPublishSnapshot = (
1303
+ snapshot: RuntimeSnapshot<State, Error, Output>
1304
+ ): Effect.Effect<void> =>
1305
+ SynchronizedRef.updateAndGet(current, (current) => ({
1306
+ revision: current.revision + 1,
1307
+ snapshot,
1308
+ terminalizing: true,
1309
+ changes: current.changes
1310
+ })).pipe(
1311
+ Effect.flatMap(publishSnapshot),
1312
+ Effect.flatMap(completeIfTerminal),
1313
+ Effect.asVoid
1314
+ )
1315
+
1316
+ const setActiveState = (state: State) =>
1317
+ updateSnapshot((snapshot) =>
1318
+ Effect.succeed(
1319
+ snapshot.status === "active"
1320
+ ? {
1321
+ status: "active",
1322
+ state
1323
+ }
1324
+ : undefined
1325
+ )
1326
+ ).pipe(Effect.asVoid)
1327
+
1328
+ const terminalizeWith = (
1329
+ snapshot: RuntimeSnapshot<State, Error, Output>,
1330
+ exit: Exit.Exit<unknown, unknown>,
1331
+ completeDone: Effect.Effect<void>
1332
+ ): Effect.Effect<void> => {
1333
+ const notifyOutcome =
1334
+ onOutcome === undefined || (snapshot.status === "stopped" && options.skipStoppedOutcome === true)
1335
+ ? Effect.void
1336
+ : Effect.suspend(() => onOutcome(classifyOutcome(snapshot)!)).pipe(
1337
+ Effect.exit,
1338
+ Effect.asVoid
1339
+ )
1340
+ return Effect.uninterruptible(
1341
+ Effect.sync(() => {
1342
+ while (true) {
1343
+ const pending = Queue.takeUnsafe(queue)
1344
+ if (pending === undefined || Exit.isFailure(pending)) break
1345
+ stopAcknowledgedMessage(pending.value)
1346
+ }
1347
+ }).pipe(
1348
+ Effect.andThen(Queue.shutdown(queue)),
1349
+ Effect.andThen(closeChildren(exit)),
1350
+ Effect.andThen(setAndPublishSnapshot(snapshot)),
1351
+ Effect.andThen(Effect.sync(() => {
1352
+ if (Exit.isFailure(exit)) {
1353
+ failAcknowledgedMessage(inFlightMessage, exit.cause)
1354
+ } else {
1355
+ stopAcknowledgedMessage(inFlightMessage)
1356
+ }
1357
+ inFlightMessage = undefined
1358
+ })),
1359
+ Effect.andThen(notifyOutcome),
1360
+ Effect.andThen(cleanup),
1361
+ Effect.andThen(completeDone)
1362
+ )
1363
+ )
1364
+ }
1365
+
1366
+ const reserveStoppedSnapshot = reserveTerminalSnapshot((snapshot) => ({
1367
+ status: "stopped",
1368
+ state: snapshot.state
1369
+ }))
1370
+
1371
+ const reserveFailureSnapshot = (cause: Cause.Cause<Error>) =>
1372
+ reserveTerminalSnapshot((snapshot) => ({
1373
+ status: "error",
1374
+ state: snapshot.state,
1375
+ cause
1376
+ }))
1377
+
1378
+ const reserveSuccessSnapshot = (output: Output) =>
1379
+ reserveTerminalSnapshot((snapshot) => ({
1380
+ status: "done",
1381
+ state: snapshot.state,
1382
+ output
1383
+ }))
1384
+
1385
+ const terminalizeReservedStop = (
1386
+ snapshot: RuntimeSnapshot<State, Error, Output>
1387
+ ): Effect.Effect<void> => {
1388
+ const exit = Exit.void
1389
+ return terminalizeWith(
1390
+ snapshot,
1391
+ exit,
1392
+ Deferred.fail(done, new StoppedError())
1393
+ )
1394
+ }
1395
+
1396
+ const terminalizeReservedFailure = (
1397
+ snapshot: RuntimeSnapshot<State, Error, Output>,
1398
+ cause: Cause.Cause<Error>
1399
+ ): Effect.Effect<void> => {
1400
+ const exit = Exit.failCause(cause)
1401
+ return terminalizeWith(snapshot, exit, Deferred.failCause(done, cause))
1402
+ }
1403
+
1404
+ const terminalizeReservedSuccess = (
1405
+ snapshot: RuntimeSnapshot<State, Error, Output>,
1406
+ output: Output
1407
+ ): Effect.Effect<void> => {
1408
+ const exit = Exit.succeed(output)
1409
+ return terminalizeWith(snapshot, exit, Deferred.succeed(done, output))
1410
+ }
1411
+
1412
+ const stop: Effect.Effect<void> = Effect.uninterruptible(
1413
+ requestStop.pipe(Effect.andThen(awaitCompletion))
1414
+ )
1415
+
1416
+ const acknowledgedContext:
1417
+ | Pick<
1418
+ ProcessContext<State, Event>,
1419
+ "receiveMessage" | "pollMessage" | "completeMessage"
1420
+ >
1421
+ | undefined = logic.execution?._tag !== "Compiled" ? undefined : {
1422
+ receiveMessage: Queue.take(queue).pipe(
1423
+ Effect.tap((message) =>
1424
+ Effect.sync(() => {
1425
+ inFlightMessage = isAcknowledgedMessage(message) ? message : undefined
1426
+ })
1427
+ )
1428
+ ),
1429
+ pollMessage: Queue.poll(queue).pipe(
1430
+ Effect.tap((message) =>
1431
+ Effect.sync(() => {
1432
+ if (Option.isSome(message)) {
1433
+ inFlightMessage = isAcknowledgedMessage(message.value) ? message.value : undefined
1434
+ }
1435
+ })
1436
+ )
1437
+ ),
1438
+ completeMessage: (delivery) => {
1439
+ succeedAcknowledgedMessage(inFlightMessage, delivery)
1440
+ inFlightMessage = undefined
1441
+ }
1442
+ }
1443
+ const context: ProcessContext<State, Event> = {
1444
+ ...scope,
1445
+ ...(logic.execution?._tag === "Compiled" && !logic.execution.childless ? { ownedChildren } : undefined),
1446
+ ...acknowledgedContext,
1447
+ receive: Queue.take(queue).pipe(Effect.map(messageEvent)),
1448
+ poll: Queue.poll(queue).pipe(Effect.map(Option.map(messageEvent))),
1449
+ state: SynchronizedRef.get(current).pipe(Effect.map((current) => current.snapshot.state)),
1450
+ setState: setActiveState,
1451
+ updateState: (f) =>
1452
+ updateSnapshot((snapshot) =>
1453
+ snapshot.status === "active"
1454
+ ? f(snapshot.state).pipe(
1455
+ Effect.map((state) => ({
1456
+ status: "active" as const,
1457
+ state
1458
+ }))
1459
+ )
1460
+ : Effect.succeed(undefined)
1461
+ ).pipe(Effect.asVoid)
1462
+ }
1463
+
1464
+ const getOrCreateChanges = SynchronizedRef.modifyEffect(
1465
+ current,
1466
+ (current) => {
1467
+ if (current.snapshot.status !== "active") {
1468
+ return Effect.succeed([undefined, current] as const)
1469
+ }
1470
+ if (current.changes !== undefined) {
1471
+ return Effect.succeed([current.changes, current] as const)
1472
+ }
1473
+ return PubSub.unbounded<Take.Take<VersionedSnapshot<State, Error, Output>>>({ replay: 1 }).pipe(
1474
+ Effect.map((changes) => [changes, { ...current, changes }] as const)
1475
+ )
1476
+ }
1477
+ )
1478
+
1479
+ const changesStream: Stream.Stream<RuntimeSnapshot<State, Error, Output>> = Stream.unwrap(
1480
+ Effect.gen(function*() {
1481
+ const changes = yield* getOrCreateChanges
1482
+ if (changes === undefined) {
1483
+ return Stream.succeed((yield* SynchronizedRef.get(current)).snapshot)
1484
+ }
1485
+ const subscription = yield* PubSub.subscribe(changes)
1486
+ const captured = yield* SynchronizedRef.get(current)
1487
+ if (captured.snapshot.status !== "active") {
1488
+ return Stream.succeed(captured.snapshot)
1489
+ }
1490
+ return Stream.succeed(captured.snapshot).pipe(
1491
+ Stream.concat(
1492
+ Stream.fromChannel(Channel.fromEffectTake(PubSub.take(subscription))).pipe(
1493
+ Stream.filter((next) => next.revision > captured.revision),
1494
+ Stream.map((next) => next.snapshot)
1495
+ )
1496
+ )
1497
+ )
1498
+ })
1499
+ )
1500
+
1501
+ const ref: MachineRef<State, Event, Error, Output> = {
1502
+ id,
1503
+ sessionId,
1504
+ state: SynchronizedRef.get(current).pipe(Effect.map((current) => current.snapshot.state)),
1505
+ snapshot: SynchronizedRef.get(current).pipe(Effect.map((current) => current.snapshot)),
1506
+ changes: changesStream,
1507
+ join: Deferred.await(done),
1508
+ stop,
1509
+ send: self.send,
1510
+ ...(sendAcknowledged === undefined ? undefined : { [acknowledgedSend]: sendAcknowledged }),
1511
+ child: getChild,
1512
+ childChanges
1513
+ }
1514
+
1515
+ if (onReadySync !== undefined && !onReadySync(ref)) {
1516
+ yield* requestStop
1517
+ } else if (onReady !== undefined) {
1518
+ yield* onReady(ref, requestStop)
1519
+ }
1520
+ if (onSnapshot !== undefined) {
1521
+ yield* notifyActiveSnapshot(onSnapshot, { status: "active", state: initial })
1522
+ }
1523
+
1524
+ const reserveTermination = (termination: ProcessTermination) => {
1525
+ switch (termination._tag) {
1526
+ case "Stopped":
1527
+ return reserveStoppedSnapshot
1528
+ case "Done":
1529
+ return reserveSuccessSnapshot(termination.output)
1530
+ case "Failure":
1531
+ return reserveFailureSnapshot(termination.cause)
1532
+ }
1533
+ }
1534
+
1535
+ const completeTermination = (
1536
+ termination: ProcessTermination,
1537
+ snapshot: RuntimeSnapshot<State, Error, Output>
1538
+ ) => {
1539
+ switch (termination._tag) {
1540
+ case "Stopped":
1541
+ return terminalizeReservedStop(snapshot)
1542
+ case "Done":
1543
+ return terminalizeReservedSuccess(snapshot, termination.output)
1544
+ case "Failure":
1545
+ return terminalizeReservedFailure(snapshot, termination.cause)
1546
+ }
1547
+ }
1548
+
1549
+ const forkRuntime = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
1550
+ detached === true
1551
+ ? Effect.forkDetach(effect)
1552
+ : Effect.forkChild(effect)
1553
+
1554
+ const pendingTermination = yield* Deferred.poll(termination)
1555
+ const worker = Option.isNone(pendingTermination)
1556
+ ? yield* Effect.uninterruptibleMask((restore) =>
1557
+ restore(Effect.suspend(() => logic.run(context))).pipe(
1558
+ Effect.exit,
1559
+ Effect.flatMap((exit) =>
1560
+ Deferred.succeed(
1561
+ termination,
1562
+ Exit.isFailure(exit)
1563
+ ? { _tag: "Failure", cause: exit.cause }
1564
+ : { _tag: "Done", output: exit.value }
1565
+ )
1566
+ )
1567
+ )
1568
+ ).pipe(forkRuntime)
1569
+ : undefined
1570
+
1571
+ // One Deferred arbitrates all terminal causes. The supervisor reserves the
1572
+ // terminal snapshot before interrupting the worker, so worker finalizers
1573
+ // cannot mutate the frozen state. It then waits for those finalizers before
1574
+ // publishing and completing `join` / `stop`.
1575
+ const runFiber: Effect.Effect<void, never, Requirements> = Effect.uninterruptibleMask((restore) =>
1576
+ Effect.gen(function*() {
1577
+ const requested = Option.isSome(pendingTermination)
1578
+ ? yield* pendingTermination.value
1579
+ : yield* restore(Deferred.await(termination))
1580
+
1581
+ const snapshot = yield* reserveTermination(requested)
1582
+ if (worker !== undefined) {
1583
+ yield* Fiber.interrupt(worker)
1584
+ }
1585
+ if (snapshot === undefined) {
1586
+ return yield* awaitCompletion
1587
+ }
1588
+ return yield* completeTermination(requested, snapshot)
1589
+ })
1590
+ )
1591
+
1592
+ yield* forkRuntime(runFiber)
1593
+ yield* Effect.yieldNow
1594
+
1595
+ return ref
1596
+ })
1597
+
1598
+ type CompiledTermination =
1599
+ | { readonly _tag: "Stopped" }
1600
+ | { readonly _tag: "Done"; readonly output: unknown }
1601
+ | { readonly _tag: "Failure"; readonly cause: Cause.Cause<unknown> }
1602
+
1603
+ type CompiledInitialized = CompiledProcessInitial<unknown, unknown>
1604
+
1605
+ // Stopping is commonly used only for resource cleanup. Keep that path free of
1606
+ // Error stack capture and materialize the typed join failure only if observed.
1607
+ const CompiledStoppedCompletion: unique symbol = Symbol("effect/Machine/CompiledStoppedCompletion")
1608
+
1609
+ type CompiledCompletion =
1610
+ | Effect.Effect<unknown, unknown>
1611
+ | typeof CompiledStoppedCompletion
1612
+
1613
+ type CompiledLifecycle = "Active" | "TerminationRequested" | "Completed"
1614
+ type CompiledRunState = "Initializing" | "Idle" | "Draining"
1615
+
1616
+ /**
1617
+ * Compact runtime for compiled statecharts.
1618
+ *
1619
+ * All long-lived state is stored directly on this object. Operations are
1620
+ * implemented by shared prototype methods, and public Effect / Stream values
1621
+ * are materialized only when accessed. Arbitrary process logic continues to
1622
+ * use the general runtime above.
1623
+ */
1624
+ class CompiledProcess implements MachineRef<any, any, any, any> {
1625
+ readonly id: string
1626
+ readonly sessionId: string
1627
+ readonly send: (event: unknown) => Effect.Effect<void, StoppedError>;
1628
+ [acknowledgedSend](
1629
+ event: unknown
1630
+ ): Effect.Effect<AcknowledgedDelivery<unknown>, unknown | StoppedError> {
1631
+ return this.sendAcknowledgedEffect(event)
1632
+ }
1633
+
1634
+ private readonly mailbox: CompactProcessMailbox<unknown> = {
1635
+ items: undefined,
1636
+ index: 0,
1637
+ closed: false
1638
+ }
1639
+ private readonly address: ProcessAddress<unknown>
1640
+ private childRuntime: ChildRuntime = childlessRuntime
1641
+ private processScope!: ProcessScope<unknown>
1642
+ private processContext: ProcessContext<unknown, unknown> | undefined
1643
+ private compiledContext: CompiledProcessContext<unknown, unknown> | undefined
1644
+ private current!: VersionedSnapshot<unknown, unknown, unknown>
1645
+ /**
1646
+ * `Active` has no terminal payload, `TerminationRequested` owns
1647
+ * `termination` and its optional reserved snapshot, and `Completed` owns
1648
+ * `completion`. `runState` independently tracks worker activity.
1649
+ */
1650
+ private lifecycle: CompiledLifecycle = "Active"
1651
+ private runState: CompiledRunState = "Initializing"
1652
+ private completion: CompiledCompletion | undefined
1653
+ private waiter: Deferred.Deferred<unknown, unknown> | undefined
1654
+ private termination: CompiledTermination | undefined
1655
+ private terminationSnapshot: RuntimeSnapshot<unknown, unknown, unknown> | undefined
1656
+ private worker: Fiber.Fiber<any, never> | undefined
1657
+ private interruptRequested = false
1658
+ private offerRevision = 0
1659
+ private inFlightMessage: ProcessMessage<unknown> | undefined
1660
+
1661
+ constructor(
1662
+ private readonly logic: ProcessLogic<any, any, any, any, any, any>,
1663
+ private readonly options: StartInternalOptions,
1664
+ private readonly services: Context.Context<any>,
1665
+ sessionId: string
1666
+ ) {
1667
+ this.sessionId = sessionId
1668
+ this.id = options.id ?? sessionId
1669
+ this.send = (event) => this.offerMessage(event)
1670
+ this.address = {
1671
+ id: this.id,
1672
+ sessionId,
1673
+ stop: Effect.suspend(() => this.stopFromProcess()),
1674
+ send: this.send
1675
+ }
1676
+ }
1677
+
1678
+ private get execution(): CompiledProcessExecution<any, any, any, any, any, any> {
1679
+ return this.logic.execution as CompiledProcessExecution<any, any, any, any, any, any>
1680
+ }
1681
+
1682
+ initializeCompiledSync(): Effect.Effect<MachineRef<any, any, any, any>, unknown> {
1683
+ if (!this.execution.childless) {
1684
+ this.childRuntime = makeChildRuntimeSync(this.address, this.options.runtime, this.services)
1685
+ }
1686
+ const parent = this.options.parent
1687
+ const sendParent = this.options.sendParent ?? (parent === undefined ? noParentSend : parent.send)
1688
+ this.processScope = {
1689
+ self: this.address,
1690
+ parent,
1691
+ spawn: this.childRuntime.spawn,
1692
+ sendParent,
1693
+ sendTo: this.childRuntime.sendTo,
1694
+ stopChild: this.childRuntime.stop,
1695
+ failCause: (cause: Cause.Cause<unknown>) => this.failCause(cause)
1696
+ }
1697
+ const compiledInitial = this.execution.initialSync!
1698
+ let initialized: CompiledInitialized
1699
+ try {
1700
+ initialized = compiledInitial(this.processScope)
1701
+ } catch (error) {
1702
+ this.runState = "Idle"
1703
+ return Effect.fail(error)
1704
+ }
1705
+ this.runState = "Idle"
1706
+ this.current = {
1707
+ revision: 0,
1708
+ terminalizing: false,
1709
+ changes: undefined,
1710
+ snapshot: { status: "active", state: initialized.state }
1711
+ }
1712
+ this.compiledContext = new CompiledProcessContextImpl(this.processScope, this.childRuntime.owned, this)
1713
+ if ("executionState" in initialized) {
1714
+ this.compiledContext.executionState = initialized.executionState
1715
+ }
1716
+ if (this.options.onReadySync !== undefined && !this.options.onReadySync(this)) {
1717
+ this.requestTerminationSync({ _tag: "Stopped" })
1718
+ }
1719
+ if (initialized.done === true && this.lifecycle === "Active") {
1720
+ this.requestTerminationSync({ _tag: "Done", output: initialized.output })
1721
+ }
1722
+ if (
1723
+ initialized.done === false && this.execution.childless &&
1724
+ this.lifecycle === "Active" &&
1725
+ this.mailbox.items === undefined
1726
+ ) {
1727
+ return Effect.succeed(this)
1728
+ }
1729
+ this.runState = "Draining"
1730
+ return Effect.provideContext(this.drainRuntime(), this.services).pipe(Effect.as(this))
1731
+ }
1732
+
1733
+ initialize(): Effect.Effect<MachineRef<any, any, any, any>, unknown, any> {
1734
+ const self = this
1735
+ return Effect.gen(function*() {
1736
+ if (!self.execution.childless) {
1737
+ self.childRuntime = yield* makeChildRuntime(self.address, self.options.runtime, self.services)
1738
+ }
1739
+ const parent = self.options.parent
1740
+ const sendParent = self.options.sendParent ?? (parent === undefined ? noParentSend : parent.send)
1741
+ self.processScope = {
1742
+ self: self.address,
1743
+ parent,
1744
+ spawn: self.childRuntime.spawn,
1745
+ sendParent,
1746
+ sendTo: self.childRuntime.sendTo,
1747
+ stopChild: self.childRuntime.stop,
1748
+ failCause: (cause: Cause.Cause<unknown>) => self.failCause(cause)
1749
+ }
1750
+
1751
+ const cleanupStartupFailure = <A, E>(exit: Exit.Exit<A, E>): Effect.Effect<void> =>
1752
+ Exit.isFailure(exit) ? self.childRuntime.close(exit) : Effect.void
1753
+ const compiledInitial = self.execution.initial
1754
+ const initializeEffect: Effect.Effect<
1755
+ {
1756
+ readonly state: unknown
1757
+ readonly done: boolean | undefined
1758
+ readonly output: unknown
1759
+ },
1760
+ unknown,
1761
+ any
1762
+ > = compiledInitial === undefined
1763
+ ? self.logic.initial(self.processScope).pipe(
1764
+ Effect.map((state) => ({ state, done: undefined, output: undefined } as const))
1765
+ )
1766
+ : compiledInitial(self.processScope)
1767
+ const initialized = yield* initializeEffect.pipe(
1768
+ Effect.onExit(cleanupStartupFailure),
1769
+ Effect.ensuring(Effect.sync(() => {
1770
+ self.runState = "Idle"
1771
+ }))
1772
+ )
1773
+ const initial = initialized.state
1774
+ self.current = {
1775
+ revision: 0,
1776
+ terminalizing: false,
1777
+ changes: undefined,
1778
+ snapshot: { status: "active", state: initial }
1779
+ }
1780
+ if (self.execution.drain._tag === "Process") {
1781
+ self.processContext = {
1782
+ ...self.processScope,
1783
+ receive: Effect.never,
1784
+ poll: Effect.sync(() => Option.map(pollCompactMailbox(self.mailbox), messageEvent)),
1785
+ receiveMessage: Effect.never,
1786
+ pollMessage: Effect.sync(() => self.pollCompiledMessage()),
1787
+ completeMessage: (delivery) => self.completeCompiledMessage(delivery),
1788
+ state: Effect.sync(() => self.current.snapshot.state),
1789
+ setState: (state: unknown) => self.setActiveState(state),
1790
+ updateState: (f) => self.updateState(f)
1791
+ }
1792
+ } else {
1793
+ self.compiledContext = new CompiledProcessContextImpl(self.processScope, self.childRuntime.owned, self)
1794
+ if ("executionState" in initialized) {
1795
+ self.compiledContext.executionState = initialized.executionState
1796
+ }
1797
+ }
1798
+
1799
+ if (self.options.onReadySync !== undefined && !self.options.onReadySync(self)) {
1800
+ yield* self.requestTermination({ _tag: "Stopped" })
1801
+ } else if (self.options.onReady !== undefined) {
1802
+ yield* self.options.onReady(self, self.requestTermination({ _tag: "Stopped" }).pipe(Effect.asVoid))
1803
+ }
1804
+ if (self.options.onSnapshot !== undefined) {
1805
+ yield* notifyActiveSnapshot(self.options.onSnapshot, { status: "active", state: initial })
1806
+ }
1807
+ if (initialized.done === true && self.lifecycle === "Active") {
1808
+ yield* self.requestTermination({ _tag: "Done", output: initialized.output })
1809
+ }
1810
+
1811
+ // A compiled machine startup plan has already settled entry actions,
1812
+ // raised events, and eventless transitions. If it is known active and
1813
+ // neither startup hooks nor emitted work queued an event, there is no
1814
+ // first drain to perform. Future sends observe an idle run state and
1815
+ // schedule the ordinary compiled worker.
1816
+ if (
1817
+ initialized.done === false && self.execution.childless &&
1818
+ self.lifecycle === "Active" && self.mailbox.items === undefined
1819
+ ) {
1820
+ return self
1821
+ }
1822
+
1823
+ self.runState = "Draining"
1824
+ yield* self.drainRuntime()
1825
+ return self
1826
+ })
1827
+ }
1828
+
1829
+ get state(): Effect.Effect<unknown> {
1830
+ return Effect.sync(() => this.current.snapshot.state)
1831
+ }
1832
+
1833
+ get snapshot(): Effect.Effect<RuntimeSnapshot<unknown, unknown, unknown>> {
1834
+ return Effect.sync(() => this.current.snapshot)
1835
+ }
1836
+
1837
+ get changes(): Stream.Stream<RuntimeSnapshot<unknown, unknown, unknown>> {
1838
+ return this.changesStream()
1839
+ }
1840
+
1841
+ get join(): Effect.Effect<unknown, unknown> {
1842
+ return Effect.suspend(() => {
1843
+ if (this.lifecycle === "Completed") {
1844
+ return this.resolveCompletion(this.completion!)
1845
+ }
1846
+ this.waiter ??= Deferred.makeUnsafe<unknown, unknown>()
1847
+ return Deferred.await(this.waiter)
1848
+ })
1849
+ }
1850
+
1851
+ get stop(): Effect.Effect<void> {
1852
+ return Effect.uninterruptible(this.stopEffect())
1853
+ }
1854
+
1855
+ child(child: ChildSelector): Effect.Effect<Option.Option<any>> {
1856
+ return this.childRuntime.get(child)
1857
+ }
1858
+
1859
+ childChanges(child: ChildSelector): Stream.Stream<Option.Option<any>> {
1860
+ return this.childRuntime.changes(child)
1861
+ }
1862
+
1863
+ private requestTermination(requested: CompiledTermination): Effect.Effect<boolean> {
1864
+ return Effect.sync(() => this.requestTerminationSync(requested))
1865
+ }
1866
+
1867
+ private hasTerminationRequest(): boolean {
1868
+ return this.lifecycle === "TerminationRequested"
1869
+ }
1870
+
1871
+ private requestTerminationSync(requested: CompiledTermination): boolean {
1872
+ if (this.lifecycle !== "Active") {
1873
+ return false
1874
+ }
1875
+ this.lifecycle = "TerminationRequested"
1876
+ this.termination = requested
1877
+ this.terminationSnapshot = this.reserveTermination(requested)
1878
+ return true
1879
+ }
1880
+
1881
+ private reserveTermination(
1882
+ requested: CompiledTermination
1883
+ ): RuntimeSnapshot<unknown, unknown, unknown> | undefined {
1884
+ const latest = this.current
1885
+ if (latest === undefined || latest.terminalizing || latest.snapshot.status !== "active") {
1886
+ return undefined
1887
+ }
1888
+ const snapshot: RuntimeSnapshot<unknown, unknown, unknown> = requested._tag === "Stopped"
1889
+ ? { status: "stopped", state: latest.snapshot.state }
1890
+ : requested._tag === "Done"
1891
+ ? { status: "done", state: latest.snapshot.state, output: requested.output }
1892
+ : { status: "error", state: latest.snapshot.state, cause: requested.cause }
1893
+ this.current = { ...latest, terminalizing: true }
1894
+ return snapshot
1895
+ }
1896
+
1897
+ private stopFromProcess(): Effect.Effect<void> {
1898
+ const request = this.requestTermination({ _tag: "Stopped" }).pipe(Effect.asVoid)
1899
+ return this.runState === "Initializing" ? request : request.pipe(Effect.andThen(Effect.interrupt))
1900
+ }
1901
+
1902
+ private failCause(cause: Cause.Cause<unknown>): Effect.Effect<void> {
1903
+ const requested = { _tag: "Failure", cause } as const
1904
+ return this.requestTermination(requested).pipe(
1905
+ Effect.flatMap((accepted) =>
1906
+ accepted
1907
+ ? Effect.forkDetach(this.settleRequestedTermination()).pipe(Effect.asVoid)
1908
+ : Effect.void
1909
+ )
1910
+ )
1911
+ }
1912
+
1913
+ private sendAcknowledgedEffect(
1914
+ event: unknown
1915
+ ): Effect.Effect<AcknowledgedDelivery<unknown>, unknown | StoppedError> {
1916
+ return Effect.uninterruptibleMask((restore) =>
1917
+ Deferred.make<AcknowledgedDelivery<unknown>, unknown>().pipe(
1918
+ Effect.flatMap((deferred) =>
1919
+ this.offerMessage({
1920
+ [AcknowledgedMessageTypeId]: true as const,
1921
+ event,
1922
+ deferred
1923
+ }).pipe(
1924
+ Effect.andThen(restore(Deferred.await(deferred)))
1925
+ )
1926
+ )
1927
+ )
1928
+ )
1929
+ }
1930
+
1931
+ private offerMessage(message: ProcessMessage<unknown>): Effect.Effect<void, StoppedError> {
1932
+ return Effect.uninterruptible(
1933
+ Effect.suspend(() => {
1934
+ if (this.mailbox.closed || this.lifecycle !== "Active") {
1935
+ return Effect.fail(new StoppedError())
1936
+ }
1937
+ offerCompactMailbox(this.mailbox, message)
1938
+ this.offerRevision += 1
1939
+ if (this.runState === "Draining") {
1940
+ return Effect.void
1941
+ }
1942
+ this.runState = "Draining"
1943
+ const scheduled = Effect.yieldNow.pipe(
1944
+ Effect.andThen(Effect.provideContext(this.drainRuntime(), this.services))
1945
+ )
1946
+ const fork = this.options.detached === true
1947
+ ? Effect.forkDetach(scheduled, { startImmediately: true })
1948
+ : Effect.forkChild(scheduled, { startImmediately: true })
1949
+ return fork.pipe(
1950
+ Effect.flatMap((fiber) =>
1951
+ Effect.sync(() => {
1952
+ this.worker = fiber
1953
+ if (!this.interruptRequested) {
1954
+ return false
1955
+ }
1956
+ this.interruptRequested = false
1957
+ return true
1958
+ }).pipe(
1959
+ Effect.flatMap((interrupt) => interrupt ? this.interruptAndFinish(fiber) : Effect.void)
1960
+ )
1961
+ ),
1962
+ Effect.asVoid
1963
+ )
1964
+ })
1965
+ )
1966
+ }
1967
+
1968
+ private stopEffect(): Effect.Effect<void> {
1969
+ return Effect.suspend(() => {
1970
+ if (this.lifecycle === "Completed") {
1971
+ return Effect.void
1972
+ }
1973
+ if (this.finishIdleChildlessStop()) {
1974
+ return Effect.void
1975
+ }
1976
+ const requested = { _tag: "Stopped" } as const
1977
+ return this.requestTermination(requested).pipe(
1978
+ Effect.flatMap((accepted) =>
1979
+ accepted
1980
+ ? this.settleRequestedTermination()
1981
+ : this.awaitCompletion()
1982
+ )
1983
+ )
1984
+ })
1985
+ }
1986
+
1987
+ private finishIdleChildlessStop(): boolean {
1988
+ if (
1989
+ !this.execution.childless || this.runState !== "Idle" || this.worker !== undefined ||
1990
+ this.lifecycle !== "Active" ||
1991
+ (this.options.onOutcome !== undefined && this.options.skipStoppedOutcome !== true) ||
1992
+ this.options.onStop !== undefined ||
1993
+ this.current.changes !== undefined ||
1994
+ this.current.terminalizing || this.current.snapshot.status !== "active"
1995
+ ) {
1996
+ return false
1997
+ }
1998
+ const snapshot = { status: "stopped" as const, state: this.current.snapshot.state }
1999
+ this.lifecycle = "TerminationRequested"
2000
+ this.termination = { _tag: "Stopped" }
2001
+ this.terminationSnapshot = snapshot
2002
+ closeCompactMailbox(this.mailbox)
2003
+ this.current = {
2004
+ revision: this.current.revision + 1,
2005
+ terminalizing: true,
2006
+ changes: undefined,
2007
+ snapshot
2008
+ }
2009
+ stopAcknowledgedMessage(this.inFlightMessage)
2010
+ this.inFlightMessage = undefined
2011
+ this.interruptRequested = false
2012
+ if (this.compiledContext !== undefined) {
2013
+ this.compiledContext.executionState = undefined
2014
+ }
2015
+ this.options.onStopSync?.()
2016
+ this.completion = CompiledStoppedCompletion
2017
+ this.lifecycle = "Completed"
2018
+ if (this.waiter !== undefined) {
2019
+ Deferred.doneUnsafe(this.waiter, this.resolveCompletion(CompiledStoppedCompletion))
2020
+ this.waiter = undefined
2021
+ }
2022
+ return true
2023
+ }
2024
+
2025
+ private settleRequestedTermination(): Effect.Effect<void> {
2026
+ return Effect.suspend(() => {
2027
+ if (this.runState !== "Draining") {
2028
+ return this.finishRequestedTermination()
2029
+ }
2030
+ if (this.worker === undefined) {
2031
+ this.interruptRequested = true
2032
+ return this.awaitCompletion()
2033
+ }
2034
+ return this.interruptAndFinish(this.worker).pipe(
2035
+ Effect.andThen(this.awaitCompletion())
2036
+ )
2037
+ })
2038
+ }
2039
+
2040
+ private interruptAndFinish(worker: Fiber.Fiber<any, never>): Effect.Effect<void> {
2041
+ return Fiber.interrupt(worker).pipe(
2042
+ Effect.andThen(
2043
+ Effect.suspend(() =>
2044
+ this.lifecycle !== "Completed"
2045
+ ? this.finishRequestedTermination()
2046
+ : Effect.void
2047
+ )
2048
+ )
2049
+ )
2050
+ }
2051
+
2052
+ private awaitCompletion(): Effect.Effect<void> {
2053
+ return this.lifecycle !== "Completed"
2054
+ ? this.join.pipe(Effect.exit, Effect.asVoid)
2055
+ : Effect.void
2056
+ }
2057
+
2058
+ private resolveCompletion(completion: CompiledCompletion): Effect.Effect<unknown, unknown> {
2059
+ if (completion !== CompiledStoppedCompletion) {
2060
+ return completion
2061
+ }
2062
+ const stopped = Effect.fail(new StoppedError())
2063
+ this.completion = stopped
2064
+ return stopped
2065
+ }
2066
+
2067
+ private drainRuntime(): Effect.Effect<void, never, any> {
2068
+ const self = this
2069
+ return Effect.uninterruptibleMask((restore) =>
2070
+ Effect.gen(function*() {
2071
+ let observedRevision = self.offerRevision
2072
+ while (true) {
2073
+ if (self.hasTerminationRequest()) {
2074
+ return yield* self.finishRequestedTermination()
2075
+ }
2076
+
2077
+ const exit = yield* restore(
2078
+ Effect.suspend(() => {
2079
+ const drain = self.execution.drain
2080
+ return drain._tag === "Process"
2081
+ ? drain.run(self.processContext!)
2082
+ : drain.run(self.compiledContext!)
2083
+ })
2084
+ ).pipe(Effect.exit)
2085
+ self.flushPendingChanges()
2086
+ if (Exit.isFailure(exit)) {
2087
+ if (self.lifecycle === "Active") {
2088
+ yield* self.requestTermination({ _tag: "Failure", cause: exit.cause })
2089
+ }
2090
+ return yield* self.finishRequestedTermination()
2091
+ }
2092
+ if (Option.isSome(exit.value)) {
2093
+ yield* self.requestTermination({ _tag: "Done", output: exit.value.value })
2094
+ return yield* self.finishRequestedTermination()
2095
+ }
2096
+ if (self.hasTerminationRequest()) {
2097
+ return yield* self.finishRequestedTermination()
2098
+ }
2099
+
2100
+ if (self.offerRevision !== observedRevision) {
2101
+ observedRevision = self.offerRevision
2102
+ continue
2103
+ }
2104
+ self.runState = "Idle"
2105
+ self.worker = undefined
2106
+ return
2107
+ }
2108
+ })
2109
+ )
2110
+ }
2111
+
2112
+ private finishRequestedTermination(): Effect.Effect<void> {
2113
+ return Effect.suspend(() => {
2114
+ if (this.lifecycle !== "TerminationRequested") {
2115
+ return Effect.void
2116
+ }
2117
+ const requested = this.termination
2118
+ if (requested === undefined) {
2119
+ return Effect.void
2120
+ }
2121
+ const snapshot = this.terminationSnapshot ?? this.reserveTermination(requested)
2122
+ if (snapshot === undefined) {
2123
+ return this.awaitCompletion()
2124
+ }
2125
+ const exit = requested._tag === "Stopped"
2126
+ ? Exit.void
2127
+ : requested._tag === "Done"
2128
+ ? Exit.succeed(requested.output)
2129
+ : Exit.failCause(requested.cause)
2130
+ const completion: CompiledCompletion = requested._tag === "Stopped"
2131
+ ? CompiledStoppedCompletion
2132
+ : requested._tag === "Done"
2133
+ ? Effect.succeed(requested.output)
2134
+ : Effect.failCause(requested.cause)
2135
+ const notifyOutcome = this.options.onOutcome === undefined ||
2136
+ (requested._tag === "Stopped" && this.options.skipStoppedOutcome === true)
2137
+ ? Effect.void
2138
+ : Effect.suspend(() => this.options.onOutcome!(classifyOutcome(snapshot)!)).pipe(
2139
+ Effect.exit,
2140
+ Effect.asVoid
2141
+ )
2142
+ return Effect.uninterruptible(
2143
+ Effect.sync(() => {
2144
+ closeCompactMailbox(this.mailbox)
2145
+ }).pipe(
2146
+ Effect.andThen(this.childRuntime.close(exit)),
2147
+ Effect.andThen(this.setAndPublishSnapshot(snapshot)),
2148
+ Effect.andThen(Effect.sync(() => {
2149
+ if (requested._tag === "Failure") {
2150
+ failAcknowledgedMessage(this.inFlightMessage, requested.cause)
2151
+ } else {
2152
+ stopAcknowledgedMessage(this.inFlightMessage)
2153
+ }
2154
+ this.inFlightMessage = undefined
2155
+ })),
2156
+ Effect.andThen(notifyOutcome),
2157
+ Effect.andThen(this.options.onStop ?? Effect.void),
2158
+ Effect.andThen(Effect.sync(() => {
2159
+ this.options.onStopSync?.()
2160
+ this.runState = "Idle"
2161
+ this.worker = undefined
2162
+ this.interruptRequested = false
2163
+ if (this.compiledContext !== undefined) {
2164
+ this.compiledContext.executionState = undefined
2165
+ }
2166
+ this.completion = completion
2167
+ this.lifecycle = "Completed"
2168
+ if (this.waiter !== undefined) {
2169
+ Deferred.doneUnsafe(this.waiter, this.resolveCompletion(completion))
2170
+ this.waiter = undefined
2171
+ }
2172
+ }))
2173
+ )
2174
+ )
2175
+ })
2176
+ }
2177
+
2178
+ private publishSnapshot(
2179
+ snapshot: VersionedSnapshot<unknown, unknown, unknown>
2180
+ ): Effect.Effect<VersionedSnapshot<unknown, unknown, unknown>> {
2181
+ const publish = snapshot.changes === undefined
2182
+ ? Effect.succeed(snapshot)
2183
+ : PubSub.publish(snapshot.changes, [snapshot] as const).pipe(Effect.as(snapshot))
2184
+ const current = snapshot.snapshot
2185
+ return this.options.onSnapshot === undefined || current.status !== "active"
2186
+ ? publish
2187
+ : publish.pipe(Effect.tap(() => notifyActiveSnapshot(this.options.onSnapshot!, current)))
2188
+ }
2189
+
2190
+ private completeChanges(snapshot: VersionedSnapshot<unknown, unknown, unknown>): Effect.Effect<void> {
2191
+ return snapshot.changes === undefined
2192
+ ? Effect.void
2193
+ : PubSub.publish(snapshot.changes, Exit.succeed<void>(undefined)).pipe(Effect.asVoid)
2194
+ }
2195
+
2196
+ private setAndPublishSnapshot(snapshot: RuntimeSnapshot<unknown, unknown, unknown>): Effect.Effect<void> {
2197
+ return Effect.suspend(() => {
2198
+ this.flushPendingChanges()
2199
+ const versioned = {
2200
+ revision: this.current.revision + 1,
2201
+ snapshot,
2202
+ terminalizing: true,
2203
+ changes: this.current.changes
2204
+ }
2205
+ this.current = versioned
2206
+ return this.publishSnapshot(versioned).pipe(
2207
+ Effect.flatMap((published) => this.completeChanges(published)),
2208
+ Effect.asVoid
2209
+ )
2210
+ })
2211
+ }
2212
+
2213
+ private setActiveState(state: unknown): Effect.Effect<void> {
2214
+ return Effect.suspend(() => this.commitActiveState(state) ?? Effect.void)
2215
+ }
2216
+
2217
+ pollCompiledMessage(): Option.Option<ProcessMessage<unknown>> {
2218
+ const message = pollCompactMailbox(this.mailbox)
2219
+ if (Option.isSome(message)) {
2220
+ this.inFlightMessage = isAcknowledgedMessage(message.value) ? message.value : undefined
2221
+ }
2222
+ return message
2223
+ }
2224
+
2225
+ completeCompiledMessage(delivery: AcknowledgedDelivery<unknown>): void {
2226
+ succeedAcknowledgedMessage(this.inFlightMessage, delivery)
2227
+ this.inFlightMessage = undefined
2228
+ }
2229
+
2230
+ compiledState(): unknown {
2231
+ return this.current.snapshot.state
2232
+ }
2233
+
2234
+ commitCompiledState(state: unknown): Effect.Effect<void> | undefined {
2235
+ return this.commitActiveState(state, true)
2236
+ }
2237
+
2238
+ private commitActiveState(state: unknown, batchChanges = false): Effect.Effect<void> | undefined {
2239
+ const latest = this.current
2240
+ if (latest.terminalizing || latest.snapshot.status !== "active") {
2241
+ return undefined
2242
+ }
2243
+ const pendingChanges = latest.pendingChanges
2244
+ if (pendingChanges !== undefined) {
2245
+ latest.pendingChanges = undefined
2246
+ }
2247
+ const activeSnapshot = { status: "active" as const, state }
2248
+ const versioned = {
2249
+ revision: latest.revision + 1,
2250
+ snapshot: activeSnapshot,
2251
+ terminalizing: false,
2252
+ changes: latest.changes
2253
+ } as VersionedSnapshot<unknown, unknown, unknown>
2254
+ this.current = versioned
2255
+ if (versioned.changes !== undefined) {
2256
+ if (pendingChanges === undefined) {
2257
+ if (batchChanges) {
2258
+ versioned.pendingChanges = [versioned]
2259
+ } else {
2260
+ PubSub.publishUnsafe(versioned.changes, [versioned] as const)
2261
+ }
2262
+ } else {
2263
+ pendingChanges.push(versioned)
2264
+ if (batchChanges) {
2265
+ versioned.pendingChanges = pendingChanges
2266
+ } else {
2267
+ PubSub.publishUnsafe(versioned.changes, pendingChanges)
2268
+ }
2269
+ }
2270
+ }
2271
+ return this.options.onSnapshot === undefined
2272
+ ? undefined
2273
+ : notifyActiveSnapshot(this.options.onSnapshot, activeSnapshot)
2274
+ }
2275
+
2276
+ flushPendingChanges(): void {
2277
+ const current = this.current
2278
+ const pendingChanges = current?.pendingChanges
2279
+ if (pendingChanges === undefined || current.changes === undefined) {
2280
+ return
2281
+ }
2282
+ current.pendingChanges = undefined
2283
+ PubSub.publishUnsafe(current.changes, pendingChanges)
2284
+ }
2285
+
2286
+ private updateState<E, R>(
2287
+ f: (state: unknown) => Effect.Effect<unknown, E, R>
2288
+ ): Effect.Effect<void, E, R> {
2289
+ return Effect.suspend(() => {
2290
+ const observed = this.current
2291
+ if (observed.terminalizing || observed.snapshot.status !== "active") {
2292
+ return Effect.void
2293
+ }
2294
+ return f(observed.snapshot.state).pipe(
2295
+ Effect.flatMap((state) => {
2296
+ const latest = this.current
2297
+ return latest.terminalizing || latest.revision !== observed.revision
2298
+ ? Effect.void
2299
+ : this.setActiveState(state)
2300
+ })
2301
+ )
2302
+ })
2303
+ }
2304
+
2305
+ private getOrCreateChanges(): Effect.Effect<
2306
+ PubSub.PubSub<Take.Take<VersionedSnapshot<unknown, unknown, unknown>>> | undefined
2307
+ > {
2308
+ return Effect.suspend(() => {
2309
+ const observed = this.current
2310
+ if (observed.snapshot.status !== "active") {
2311
+ return Effect.succeed(undefined)
2312
+ }
2313
+ if (observed.changes !== undefined) {
2314
+ return Effect.succeed(observed.changes)
2315
+ }
2316
+ return PubSub.unbounded<Take.Take<VersionedSnapshot<unknown, unknown, unknown>>>({ replay: 1 }).pipe(
2317
+ Effect.flatMap((candidate) =>
2318
+ Effect.sync(() => {
2319
+ const latest = this.current
2320
+ if (latest.snapshot.status !== "active") {
2321
+ return [undefined, true] as const
2322
+ }
2323
+ if (latest.changes !== undefined) {
2324
+ return [latest.changes, true] as const
2325
+ }
2326
+ this.current = { ...latest, changes: candidate }
2327
+ return [candidate, false] as const
2328
+ }).pipe(
2329
+ Effect.flatMap(([changes, discard]) =>
2330
+ discard ? PubSub.shutdown(candidate).pipe(Effect.as(changes)) : Effect.succeed(changes)
2331
+ )
2332
+ )
2333
+ )
2334
+ )
2335
+ })
2336
+ }
2337
+
2338
+ private changesStream(): Stream.Stream<RuntimeSnapshot<unknown, unknown, unknown>> {
2339
+ const self = this
2340
+ return Stream.unwrap(
2341
+ Effect.gen(function*() {
2342
+ const changes = yield* self.getOrCreateChanges()
2343
+ if (changes === undefined) {
2344
+ return Stream.succeed(self.current.snapshot)
2345
+ }
2346
+ const subscription = yield* PubSub.subscribe(changes)
2347
+ const captured = self.current
2348
+ if (captured.snapshot.status !== "active") {
2349
+ return Stream.succeed(captured.snapshot)
2350
+ }
2351
+ return Stream.succeed(captured.snapshot).pipe(
2352
+ Stream.concat(
2353
+ Stream.fromChannel(Channel.fromEffectTake(PubSub.take(subscription))).pipe(
2354
+ Stream.filter((next) => next.revision > captured.revision),
2355
+ Stream.map((next) => next.snapshot)
2356
+ )
2357
+ )
2358
+ )
2359
+ })
2360
+ )
2361
+ }
2362
+ }
2363
+
2364
+ class CompiledProcessContextImpl implements CompiledProcessContext<unknown, unknown> {
2365
+ executionState: unknown
2366
+
2367
+ constructor(
2368
+ readonly scope: ProcessScope<unknown>,
2369
+ readonly ownedChildren: OwnedChildRuntime,
2370
+ private readonly process: CompiledProcess
2371
+ ) {}
2372
+
2373
+ poll(): Option.Option<unknown> {
2374
+ return Option.map(this.process.pollCompiledMessage(), messageEvent)
2375
+ }
2376
+
2377
+ pollMessage(): Option.Option<ProcessMessage<unknown>> {
2378
+ return this.process.pollCompiledMessage()
2379
+ }
2380
+
2381
+ state(): unknown {
2382
+ return this.process.compiledState()
2383
+ }
2384
+
2385
+ completeMessage(delivery: AcknowledgedDelivery<unknown>): void {
2386
+ this.process.completeCompiledMessage(delivery)
2387
+ }
2388
+
2389
+ commit(state: unknown): Effect.Effect<void> | undefined {
2390
+ return this.process.commitCompiledState(state)
2391
+ }
2392
+
2393
+ runAfterChanges<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> {
2394
+ this.process.flushPendingChanges()
2395
+ return effect
2396
+ }
2397
+ }
2398
+
2399
+ const startCompactCompiledInternal: typeof startGenericInternal = Effect.fnUntraced(function*(
2400
+ logic: ProcessLogic<any, any, any, any, any, any>,
2401
+ options: StartInternalOptions
2402
+ ) {
2403
+ const sessionId = yield* options.runtime.nextSessionId
2404
+ const services = yield* Effect.context<any>()
2405
+ const execution = logic.execution as CompiledProcessExecution<any, any, any, any, any, any>
2406
+ const process = new CompiledProcess(logic, options, services, sessionId)
2407
+ // A compiled initializer is synchronous by construction. Only startup
2408
+ // callbacks that themselves return Effects need the generic initialization
2409
+ // program; the compiled drain is still provided the complete service context.
2410
+ return yield* execution.initialSync !== undefined &&
2411
+ options.onReady === undefined && options.onSnapshot === undefined
2412
+ ? process.initializeCompiledSync()
2413
+ : process.initialize()
2414
+ }) as typeof startGenericInternal
2415
+
2416
+ const startLogicInternal: typeof startGenericInternal = ((
2417
+ logic: ProcessLogic<any, any, any, any, any, any>,
2418
+ options: StartInternalOptions
2419
+ ) =>
2420
+ logic.execution?._tag === "Compiled"
2421
+ ? startCompactCompiledInternal(logic, options)
2422
+ : startGenericInternal(logic, options)) as typeof startGenericInternal
2423
+
2424
+ export type ProcessRuntimeStrategy = "generic" | "compiled" | "auto"
2425
+
2426
+ const startProcessWithStrategy = Effect.fnUntraced(function*(
2427
+ logic: ProcessLogic<any, any, any, any, any, any>,
2428
+ strategy: ProcessRuntimeStrategy,
2429
+ options?: {
2430
+ readonly id?: string
2431
+ }
2432
+ ) {
2433
+ const runtime = yield* makeProcessRuntime
2434
+ const internalOptions: StartInternalOptions = options === undefined
2435
+ ? {
2436
+ detached: true,
2437
+ runtime
2438
+ }
2439
+ : {
2440
+ ...options,
2441
+ detached: true,
2442
+ runtime
2443
+ }
2444
+ if (strategy === "generic") {
2445
+ return yield* startGenericInternal(logic, internalOptions)
2446
+ }
2447
+ if (strategy === "compiled") {
2448
+ if (logic.execution?._tag !== "Compiled") {
2449
+ return yield* Effect.die(new Error("Machine cannot force the compiled runtime for generic process logic"))
2450
+ }
2451
+ return yield* startCompactCompiledInternal(logic, internalOptions)
2452
+ }
2453
+ return yield* startLogicInternal(logic, internalOptions)
2454
+ })
2455
+
2456
+ /** @internal Test-only startup strategy selection. */
2457
+ export const startProcessWithStrategyForTesting = <
2458
+ State,
2459
+ Event,
2460
+ Error = never,
2461
+ Requirements = never,
2462
+ Output = never,
2463
+ InitialError = never
2464
+ >(
2465
+ logic: ProcessLogic<State, Event, Error, Requirements, Output, InitialError>,
2466
+ strategy: ProcessRuntimeStrategy,
2467
+ options?: {
2468
+ readonly id?: string
2469
+ }
2470
+ ): Effect.Effect<
2471
+ MachineRef<State, Event, Error, Output>,
2472
+ InitialError,
2473
+ Requirements
2474
+ > => startProcessWithStrategy(logic, strategy, options) as any
2475
+
2476
+ export const startProcess: <
2477
+ State,
2478
+ Event,
2479
+ Error = never,
2480
+ Requirements = never,
2481
+ Output = never,
2482
+ InitialError = never
2483
+ >(
2484
+ logic: ProcessLogic<State, Event, Error, Requirements, Output, InitialError>,
2485
+ options?: {
2486
+ readonly id?: string
2487
+ }
2488
+ ) => Effect.Effect<
2489
+ MachineRef<State, Event, Error, Output>,
2490
+ InitialError,
2491
+ Requirements
2492
+ > = Effect.fnUntraced(function*<State, Event, Error, Requirements, Output, InitialError>(
2493
+ logic: ProcessLogic<State, Event, Error, Requirements, Output, InitialError>,
2494
+ options?: {
2495
+ readonly id?: string
2496
+ }
2497
+ ) {
2498
+ const runtime = yield* makeProcessRuntime
2499
+ return yield* startLogicInternal(
2500
+ logic,
2501
+ options === undefined
2502
+ ? {
2503
+ detached: true,
2504
+ runtime
2505
+ }
2506
+ : {
2507
+ ...options,
2508
+ detached: true,
2509
+ runtime
2510
+ }
2511
+ )
2512
+ })