@typeonce/effect-machine 0.9.0 → 0.11.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 (93) hide show
  1. package/README.md +167 -28
  2. package/dist/Machine.d.ts +491 -283
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +74 -76
  5. package/dist/Machine.js.map +1 -1
  6. package/dist/internal/machine/atom.d.ts +7 -3
  7. package/dist/internal/machine/atom.d.ts.map +1 -1
  8. package/dist/internal/machine/atom.js +42 -6
  9. package/dist/internal/machine/atom.js.map +1 -1
  10. package/dist/internal/machine/cluster.d.ts.map +1 -1
  11. package/dist/internal/machine/cluster.js +2 -1
  12. package/dist/internal/machine/cluster.js.map +1 -1
  13. package/dist/internal/machine/command.js +2 -2
  14. package/dist/internal/machine/command.js.map +1 -1
  15. package/dist/internal/machine/commandRuntime.d.ts.map +1 -1
  16. package/dist/internal/machine/commandRuntime.js +6 -3
  17. package/dist/internal/machine/commandRuntime.js.map +1 -1
  18. package/dist/internal/machine/configuration.d.ts +8 -1
  19. package/dist/internal/machine/configuration.d.ts.map +1 -1
  20. package/dist/internal/machine/configuration.js +9 -4
  21. package/dist/internal/machine/configuration.js.map +1 -1
  22. package/dist/internal/machine/errors.d.ts +1 -1
  23. package/dist/internal/machine/errors.d.ts.map +1 -1
  24. package/dist/internal/machine/executionPlan.d.ts +3 -3
  25. package/dist/internal/machine/executionPlan.d.ts.map +1 -1
  26. package/dist/internal/machine/executionPlan.js +50 -25
  27. package/dist/internal/machine/executionPlan.js.map +1 -1
  28. package/dist/internal/machine/invocation.d.ts.map +1 -1
  29. package/dist/internal/machine/invocation.js +3 -2
  30. package/dist/internal/machine/invocation.js.map +1 -1
  31. package/dist/internal/machine/machine.d.ts +15 -19
  32. package/dist/internal/machine/machine.d.ts.map +1 -1
  33. package/dist/internal/machine/machine.js +16 -10
  34. package/dist/internal/machine/machine.js.map +1 -1
  35. package/dist/internal/machine/planner.d.ts +2 -1
  36. package/dist/internal/machine/planner.d.ts.map +1 -1
  37. package/dist/internal/machine/planner.js +61 -24
  38. package/dist/internal/machine/planner.js.map +1 -1
  39. package/dist/internal/machine/process.d.ts +3 -0
  40. package/dist/internal/machine/process.d.ts.map +1 -1
  41. package/dist/internal/machine/process.js +11 -8
  42. package/dist/internal/machine/process.js.map +1 -1
  43. package/dist/internal/machine/protocol.d.ts +7 -13
  44. package/dist/internal/machine/protocol.d.ts.map +1 -1
  45. package/dist/internal/machine/protocol.js +128 -50
  46. package/dist/internal/machine/protocol.js.map +1 -1
  47. package/dist/internal/machine/runtime.d.ts +22 -2
  48. package/dist/internal/machine/runtime.d.ts.map +1 -1
  49. package/dist/internal/machine/runtime.js +138 -10
  50. package/dist/internal/machine/runtime.js.map +1 -1
  51. package/dist/internal/machine/topology.d.ts +7 -0
  52. package/dist/internal/machine/topology.d.ts.map +1 -1
  53. package/dist/internal/machine/topology.js +6 -0
  54. package/dist/internal/machine/topology.js.map +1 -1
  55. package/dist/internal/testing/machine/finiteModel.js +1 -1
  56. package/dist/internal/testing/machine/finiteModel.js.map +1 -1
  57. package/dist/internal/testing/machine/runtime.d.ts.map +1 -1
  58. package/dist/internal/testing/machine/runtime.js +2 -1
  59. package/dist/internal/testing/machine/runtime.js.map +1 -1
  60. package/dist/internal/testing/machine/verification.d.ts.map +1 -1
  61. package/dist/internal/testing/machine/verification.js +5 -3
  62. package/dist/internal/testing/machine/verification.js.map +1 -1
  63. package/dist/testing/MachineTest.d.ts +7 -7
  64. package/dist/testing/MachineTest.js +7 -7
  65. package/dist/unstable/cluster/ClusterMachine.d.ts +1 -1
  66. package/dist/unstable/cluster/ClusterMachine.js +1 -1
  67. package/dist/unstable/reactivity/AtomMachine.d.ts +27 -8
  68. package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
  69. package/dist/unstable/reactivity/AtomMachine.js +20 -3
  70. package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
  71. package/docs/agent-guide.md +191 -54
  72. package/package.json +1 -1
  73. package/src/Machine.ts +1220 -360
  74. package/src/internal/machine/atom.ts +76 -53
  75. package/src/internal/machine/cluster.ts +2 -1
  76. package/src/internal/machine/command.ts +2 -2
  77. package/src/internal/machine/commandRuntime.ts +9 -4
  78. package/src/internal/machine/configuration.ts +23 -5
  79. package/src/internal/machine/errors.ts +1 -1
  80. package/src/internal/machine/executionPlan.ts +74 -29
  81. package/src/internal/machine/invocation.ts +3 -2
  82. package/src/internal/machine/machine.ts +74 -49
  83. package/src/internal/machine/planner.ts +68 -24
  84. package/src/internal/machine/process.ts +75 -8
  85. package/src/internal/machine/protocol.ts +207 -68
  86. package/src/internal/machine/runtime.ts +266 -11
  87. package/src/internal/machine/topology.ts +15 -0
  88. package/src/internal/testing/machine/finiteModel.ts +1 -1
  89. package/src/internal/testing/machine/runtime.ts +2 -1
  90. package/src/internal/testing/machine/verification.ts +5 -3
  91. package/src/testing/MachineTest.ts +7 -7
  92. package/src/unstable/cluster/ClusterMachine.ts +1 -1
  93. package/src/unstable/reactivity/AtomMachine.ts +44 -10
@@ -18,6 +18,7 @@ import * as Scope from "effect/Scope"
18
18
  import * as Stream from "effect/Stream"
19
19
  import * as SynchronizedRef from "effect/SynchronizedRef"
20
20
  import type * as Take from "effect/Take"
21
+ import type { ActorRef } from "../../Machine.js"
21
22
  import { ChildAlreadyExistsError, StoppedError } from "./errors.js"
22
23
 
23
24
  type ChildDescriptor = {
@@ -284,12 +285,13 @@ export type RuntimeOutcome<State, Error = never, Output = never> =
284
285
  readonly snapshot: Extract<RuntimeSnapshot<State, Error, Output>, { readonly status: "stopped" }>
285
286
  }
286
287
 
287
- export interface MachineRef<out State, in Event, out Error = never, out Output = never> {
288
+ export interface MachineRef<out State, in Event, out Error = never, out Output = never, out Emitted = never> {
288
289
  readonly id: string
289
290
  readonly sessionId: string
290
291
  readonly state: Effect.Effect<State>
291
292
  readonly snapshot: Effect.Effect<RuntimeSnapshot<State, Error, Output>>
292
293
  readonly changes: Stream.Stream<RuntimeSnapshot<State, Error, Output>>
294
+ readonly emissions: Stream.Stream<Emitted>
293
295
  readonly join: Effect.Effect<Output, Error | StoppedError>
294
296
  readonly stop: Effect.Effect<void>
295
297
  readonly send: (event: Event) => Effect.Effect<void, StoppedError>
@@ -300,6 +302,22 @@ export interface MachineRef<out State, in Event, out Error = never, out Output =
300
302
  readonly childChanges: (child: any) => Stream.Stream<Option.Option<any>>
301
303
  }
302
304
 
305
+ export interface PreparedProcess<
306
+ out State,
307
+ in Event,
308
+ out Error,
309
+ out Output,
310
+ out Emitted,
311
+ out StartError,
312
+ StartRequirements
313
+ > {
314
+ readonly id: string
315
+ readonly sessionId: string
316
+ readonly changes: Stream.Stream<RuntimeSnapshot<State, Error, Output>, StartError>
317
+ readonly emissions: Stream.Stream<Emitted>
318
+ readonly start: Effect.Effect<MachineRef<State, Event, Error, Output, Emitted>, StartError, StartRequirements>
319
+ }
320
+
303
321
  interface ProcessAddress<in Event> {
304
322
  readonly id: string
305
323
  readonly sessionId: string
@@ -307,12 +325,19 @@ interface ProcessAddress<in Event> {
307
325
  readonly send: (event: Event) => Effect.Effect<void, StoppedError>
308
326
  }
309
327
 
328
+ const isProcessAddress = (value: unknown): value is ActorRef<unknown> =>
329
+ typeof value === "object" && value !== null && "send" in value && typeof value.send === "function"
330
+
310
331
  export interface ProcessScope<Event> {
311
332
  readonly self: ProcessAddress<Event>
312
333
  readonly parent: ProcessAddress<unknown> | undefined
313
334
  readonly spawn: ProcessSpawn
314
335
  readonly sendParent: (event: unknown) => Effect.Effect<void, StoppedError>
315
- readonly sendTo: (child: ChildSelector, event: unknown) => Effect.Effect<void, StoppedError>
336
+ readonly emit: (event: unknown) => Effect.Effect<void>
337
+ readonly sendTo: {
338
+ <TargetEvent>(target: ActorRef<TargetEvent>, event: TargetEvent): Effect.Effect<void, StoppedError>
339
+ (child: ChildSelector, event: unknown): Effect.Effect<void, StoppedError>
340
+ }
316
341
  readonly stopChild: (child: ChildSelector) => Effect.Effect<void>
317
342
  /** @internal */
318
343
  readonly failCause: (cause: Cause.Cause<unknown>) => Effect.Effect<void>
@@ -600,6 +625,8 @@ const makeProcessRuntime: Effect.Effect<ProcessRuntime> = Effect.sync(() => {
600
625
  interface StartInternalOptions {
601
626
  readonly detached?: boolean
602
627
  readonly id?: string
628
+ readonly sessionId?: string
629
+ readonly emissions?: EmissionRuntime
603
630
  readonly onOutcome?: (outcome: RuntimeOutcome<any, any, any>) => Effect.Effect<void>
604
631
  readonly onSnapshot?: (
605
632
  snapshot: Extract<RuntimeSnapshot<any, any, any>, { readonly status: "active" }>
@@ -708,6 +735,10 @@ class OwnedChildRuntimeImpl implements OwnedChildRuntime {
708
735
  ownerPath: options.path,
709
736
  ownerActive: true
710
737
  })
738
+ const parent: ProcessAddress<unknown> = {
739
+ ...this.self,
740
+ send: (event) => options.sendParent(isCurrent, event)
741
+ }
711
742
  const startOptions: StartInternalOptions = {
712
743
  detached: true,
713
744
  id: options.id,
@@ -722,7 +753,7 @@ class OwnedChildRuntimeImpl implements OwnedChildRuntime {
722
753
  },
723
754
  onStopSync: () => unregisterChild(this.registry, options.id, token),
724
755
  skipStoppedOutcome: true,
725
- parent: this.self,
756
+ parent,
726
757
  runtime: this.runtime
727
758
  }
728
759
  const execution = logic.execution
@@ -785,6 +816,57 @@ class OwnedChildRuntimeImpl implements OwnedChildRuntime {
785
816
 
786
817
  const noChildChanges = Stream.succeed(Option.none()).pipe(Stream.concat(Stream.never))
787
818
  const noParentSend = (_event: unknown): Effect.Effect<void, StoppedError> => Effect.void
819
+ const EmissionsClosed: unique symbol = Symbol("effect/Machine/EmissionsClosed")
820
+
821
+ type LazyEmissions = PubSub.PubSub<unknown> | typeof EmissionsClosed | undefined
822
+
823
+ interface EmissionRuntime {
824
+ readonly emit: (event: unknown) => Effect.Effect<void>
825
+ readonly close: () => Effect.Effect<void>
826
+ readonly stream: Stream.Stream<unknown>
827
+ }
828
+
829
+ const makeEmissionRuntime = (): EmissionRuntime => {
830
+ let emissions: LazyEmissions
831
+ const getOrCreate: Effect.Effect<PubSub.PubSub<unknown> | undefined> = Effect.suspend(() => {
832
+ const observed = emissions
833
+ if (observed === EmissionsClosed) return Effect.succeed(undefined)
834
+ if (observed !== undefined) return Effect.succeed(observed)
835
+ return PubSub.unbounded<unknown>().pipe(
836
+ Effect.flatMap((candidate) =>
837
+ Effect.sync(() => {
838
+ const latest = emissions
839
+ if (latest === EmissionsClosed) return [undefined, true] as const
840
+ if (latest !== undefined) return [latest, true] as const
841
+ emissions = candidate
842
+ return [candidate, false] as const
843
+ }).pipe(
844
+ Effect.flatMap(([selected, discard]) =>
845
+ discard ? PubSub.shutdown(candidate).pipe(Effect.as(selected)) : Effect.succeed(selected)
846
+ )
847
+ )
848
+ )
849
+ )
850
+ })
851
+ return {
852
+ emit: (event) =>
853
+ Effect.suspend(() =>
854
+ emissions === undefined || emissions === EmissionsClosed
855
+ ? Effect.void
856
+ : PubSub.publish(emissions, event).pipe(Effect.asVoid)
857
+ ),
858
+ close: () => {
859
+ const observed = emissions
860
+ emissions = EmissionsClosed
861
+ return observed === undefined || observed === EmissionsClosed ? Effect.void : PubSub.shutdown(observed)
862
+ },
863
+ stream: Stream.unwrap(
864
+ getOrCreate.pipe(
865
+ Effect.map((emissions) => emissions === undefined ? Stream.empty : Stream.fromPubSub(emissions))
866
+ )
867
+ )
868
+ }
869
+ }
788
870
 
789
871
  const childlessRuntime: ChildRuntime = {
790
872
  close: () => Effect.void,
@@ -1088,9 +1170,10 @@ const startGenericInternal: <
1088
1170
  | { readonly _tag: "Done"; readonly output: Output }
1089
1171
  | { readonly _tag: "Failure"; readonly cause: Cause.Cause<Error> }
1090
1172
 
1091
- const sessionId = yield* runtime.nextSessionId
1173
+ const sessionId = options.sessionId ?? (yield* runtime.nextSessionId)
1092
1174
  const id = requestedId ?? sessionId
1093
1175
  const queue = yield* Queue.unbounded<ProcessMessage<Event>>()
1176
+ const emissions = options.emissions ?? makeEmissionRuntime()
1094
1177
  const termination = yield* Deferred.make<ProcessTermination>()
1095
1178
  const done = yield* Deferred.make<Output, Error | StoppedError>()
1096
1179
  const awaitCompletion = Deferred.await(done).pipe(Effect.exit, Effect.asVoid)
@@ -1161,7 +1244,7 @@ const startGenericInternal: <
1161
1244
  }
1162
1245
  const cleanupStartupFailure = <A, E>(exit: Exit.Exit<A, E>): Effect.Effect<void> =>
1163
1246
  Exit.isFailure(exit)
1164
- ? closeChildren(exit)
1247
+ ? closeChildren(exit).pipe(Effect.andThen(emissions.close()))
1165
1248
  : Effect.void
1166
1249
  const cleanup = onStopSync === undefined ? onStop ?? Effect.void : Effect.sync(onStopSync)
1167
1250
  const sendParent = overrideSendParent ?? (parent === undefined ? noParentSend : parent.send)
@@ -1171,7 +1254,11 @@ const startGenericInternal: <
1171
1254
  parent,
1172
1255
  spawn,
1173
1256
  sendParent,
1174
- sendTo,
1257
+ emit: emissions.emit,
1258
+ sendTo: ((target: unknown, event: unknown) =>
1259
+ isProcessAddress(target) ? target.send(event) : sendTo(target as ChildSelector, event)) as ProcessScope<
1260
+ Event
1261
+ >["sendTo"],
1175
1262
  stopChild,
1176
1263
  failCause: (cause) =>
1177
1264
  Deferred.succeed(termination, {
@@ -1209,7 +1296,9 @@ const startGenericInternal: <
1209
1296
  const runtimeSnapshot = snapshot.snapshot
1210
1297
  return runtimeSnapshot.status !== "active"
1211
1298
  ? publish
1212
- : publish.pipe(Effect.tap(() => notifyActiveSnapshot(onSnapshot, runtimeSnapshot)))
1299
+ : publish.pipe(Effect.tap(() =>
1300
+ notifyActiveSnapshot(onSnapshot, runtimeSnapshot)
1301
+ ))
1213
1302
  }
1214
1303
 
1215
1304
  const completeChanges = (
@@ -1348,6 +1437,7 @@ const startGenericInternal: <
1348
1437
  Effect.andThen(Queue.shutdown(queue)),
1349
1438
  Effect.andThen(closeChildren(exit)),
1350
1439
  Effect.andThen(setAndPublishSnapshot(snapshot)),
1440
+ Effect.andThen(emissions.close()),
1351
1441
  Effect.andThen(Effect.sync(() => {
1352
1442
  if (Exit.isFailure(exit)) {
1353
1443
  failAcknowledgedMessage(inFlightMessage, exit.cause)
@@ -1504,6 +1594,7 @@ const startGenericInternal: <
1504
1594
  state: SynchronizedRef.get(current).pipe(Effect.map((current) => current.snapshot.state)),
1505
1595
  snapshot: SynchronizedRef.get(current).pipe(Effect.map((current) => current.snapshot)),
1506
1596
  changes: changesStream,
1597
+ emissions: emissions.stream as Stream.Stream<never>,
1507
1598
  join: Deferred.await(done),
1508
1599
  stop,
1509
1600
  send: self.send,
@@ -1657,6 +1748,8 @@ class CompiledProcess implements MachineRef<any, any, any, any> {
1657
1748
  private interruptRequested = false
1658
1749
  private offerRevision = 0
1659
1750
  private inFlightMessage: ProcessMessage<unknown> | undefined
1751
+ private readonly externalEmissions: EmissionRuntime | undefined
1752
+ private emissionsPubSub: LazyEmissions
1660
1753
 
1661
1754
  constructor(
1662
1755
  private readonly logic: ProcessLogic<any, any, any, any, any, any>,
@@ -1666,6 +1759,7 @@ class CompiledProcess implements MachineRef<any, any, any, any> {
1666
1759
  ) {
1667
1760
  this.sessionId = sessionId
1668
1761
  this.id = options.id ?? sessionId
1762
+ this.externalEmissions = options.emissions
1669
1763
  this.send = (event) => this.offerMessage(event)
1670
1764
  this.address = {
1671
1765
  id: this.id,
@@ -1690,7 +1784,11 @@ class CompiledProcess implements MachineRef<any, any, any, any> {
1690
1784
  parent,
1691
1785
  spawn: this.childRuntime.spawn,
1692
1786
  sendParent,
1693
- sendTo: this.childRuntime.sendTo,
1787
+ emit: (event) => this.emitEvent(event),
1788
+ sendTo: ((target: unknown, event: unknown) =>
1789
+ isProcessAddress(target)
1790
+ ? target.send(event)
1791
+ : this.childRuntime.sendTo(target as ChildSelector, event)) as ProcessScope<unknown>["sendTo"],
1694
1792
  stopChild: this.childRuntime.stop,
1695
1793
  failCause: (cause: Cause.Cause<unknown>) => this.failCause(cause)
1696
1794
  }
@@ -1743,7 +1841,11 @@ class CompiledProcess implements MachineRef<any, any, any, any> {
1743
1841
  parent,
1744
1842
  spawn: self.childRuntime.spawn,
1745
1843
  sendParent,
1746
- sendTo: self.childRuntime.sendTo,
1844
+ emit: (event) => self.emitEvent(event),
1845
+ sendTo: ((target: unknown, event: unknown) =>
1846
+ isProcessAddress(target)
1847
+ ? target.send(event)
1848
+ : self.childRuntime.sendTo(target as ChildSelector, event)) as ProcessScope<unknown>["sendTo"],
1747
1849
  stopChild: self.childRuntime.stop,
1748
1850
  failCause: (cause: Cause.Cause<unknown>) => self.failCause(cause)
1749
1851
  }
@@ -1838,6 +1940,10 @@ class CompiledProcess implements MachineRef<any, any, any, any> {
1838
1940
  return this.changesStream()
1839
1941
  }
1840
1942
 
1943
+ get emissions(): Stream.Stream<never> {
1944
+ return (this.externalEmissions?.stream ?? this.emissionsStream()) as Stream.Stream<never>
1945
+ }
1946
+
1841
1947
  get join(): Effect.Effect<unknown, unknown> {
1842
1948
  return Effect.suspend(() => {
1843
1949
  if (this.lifecycle === "Completed") {
@@ -2145,6 +2251,7 @@ class CompiledProcess implements MachineRef<any, any, any, any> {
2145
2251
  }).pipe(
2146
2252
  Effect.andThen(this.childRuntime.close(exit)),
2147
2253
  Effect.andThen(this.setAndPublishSnapshot(snapshot)),
2254
+ Effect.andThen(this.closeEmissions()),
2148
2255
  Effect.andThen(Effect.sync(() => {
2149
2256
  if (requested._tag === "Failure") {
2150
2257
  failAcknowledgedMessage(this.inFlightMessage, requested.cause)
@@ -2193,6 +2300,57 @@ class CompiledProcess implements MachineRef<any, any, any, any> {
2193
2300
  : PubSub.publish(snapshot.changes, Exit.succeed<void>(undefined)).pipe(Effect.asVoid)
2194
2301
  }
2195
2302
 
2303
+ private emitEvent(event: unknown): Effect.Effect<void> {
2304
+ if (this.externalEmissions !== undefined) return this.externalEmissions.emit(event)
2305
+ return Effect.suspend(() =>
2306
+ this.emissionsPubSub === undefined || this.emissionsPubSub === EmissionsClosed
2307
+ ? Effect.void
2308
+ : PubSub.publish(this.emissionsPubSub, event).pipe(Effect.asVoid)
2309
+ )
2310
+ }
2311
+
2312
+ shutdownEmissions(): Effect.Effect<void> {
2313
+ return this.closeEmissions()
2314
+ }
2315
+
2316
+ private closeEmissions(): Effect.Effect<void> {
2317
+ if (this.externalEmissions !== undefined) return this.externalEmissions.close()
2318
+ const observed = this.emissionsPubSub
2319
+ this.emissionsPubSub = EmissionsClosed
2320
+ return observed === undefined || observed === EmissionsClosed ? Effect.void : PubSub.shutdown(observed)
2321
+ }
2322
+
2323
+ private getOrCreateEmissions(): Effect.Effect<PubSub.PubSub<unknown> | undefined> {
2324
+ return Effect.suspend(() => {
2325
+ const observed = this.emissionsPubSub
2326
+ if (observed === EmissionsClosed) return Effect.succeed(undefined)
2327
+ if (observed !== undefined) return Effect.succeed(observed)
2328
+ return PubSub.unbounded<unknown>().pipe(
2329
+ Effect.flatMap((candidate) =>
2330
+ Effect.sync(() => {
2331
+ const latest = this.emissionsPubSub
2332
+ if (latest === EmissionsClosed) return [undefined, true] as const
2333
+ if (latest !== undefined) return [latest, true] as const
2334
+ this.emissionsPubSub = candidate
2335
+ return [candidate, false] as const
2336
+ }).pipe(
2337
+ Effect.flatMap(([selected, discard]) =>
2338
+ discard ? PubSub.shutdown(candidate).pipe(Effect.as(selected)) : Effect.succeed(selected)
2339
+ )
2340
+ )
2341
+ )
2342
+ )
2343
+ })
2344
+ }
2345
+
2346
+ private emissionsStream(): Stream.Stream<unknown> {
2347
+ return Stream.unwrap(
2348
+ this.getOrCreateEmissions().pipe(
2349
+ Effect.map((emissions) => emissions === undefined ? Stream.empty : Stream.fromPubSub(emissions))
2350
+ )
2351
+ )
2352
+ }
2353
+
2196
2354
  private setAndPublishSnapshot(snapshot: RuntimeSnapshot<unknown, unknown, unknown>): Effect.Effect<void> {
2197
2355
  return Effect.suspend(() => {
2198
2356
  this.flushPendingChanges()
@@ -2400,17 +2558,20 @@ const startCompactCompiledInternal: typeof startGenericInternal = Effect.fnUntra
2400
2558
  logic: ProcessLogic<any, any, any, any, any, any>,
2401
2559
  options: StartInternalOptions
2402
2560
  ) {
2403
- const sessionId = yield* options.runtime.nextSessionId
2561
+ const sessionId = options.sessionId ?? (yield* options.runtime.nextSessionId)
2404
2562
  const services = yield* Effect.context<any>()
2405
2563
  const execution = logic.execution as CompiledProcessExecution<any, any, any, any, any, any>
2406
2564
  const process = new CompiledProcess(logic, options, services, sessionId)
2407
2565
  // A compiled initializer is synchronous by construction. Only startup
2408
2566
  // callbacks that themselves return Effects need the generic initialization
2409
2567
  // program; the compiled drain is still provided the complete service context.
2410
- return yield* execution.initialSync !== undefined &&
2568
+ const initialize = execution.initialSync !== undefined &&
2411
2569
  options.onReady === undefined && options.onSnapshot === undefined
2412
2570
  ? process.initializeCompiledSync()
2413
2571
  : process.initialize()
2572
+ return yield* initialize.pipe(
2573
+ Effect.onExit((exit) => Exit.isFailure(exit) ? process.shutdownEmissions() : Effect.void)
2574
+ )
2414
2575
  }) as typeof startGenericInternal
2415
2576
 
2416
2577
  const startLogicInternal: typeof startGenericInternal = ((
@@ -2510,3 +2671,97 @@ export const startProcess: <
2510
2671
  }
2511
2672
  )
2512
2673
  })
2674
+
2675
+ const prepareProcessWithStrategy = Effect.fnUntraced(function*<
2676
+ State,
2677
+ Event,
2678
+ Error,
2679
+ Requirements,
2680
+ Output,
2681
+ InitialError,
2682
+ Emitted
2683
+ >(
2684
+ logic: ProcessLogic<State, Event, Error, Requirements, Output, InitialError>,
2685
+ strategy: ProcessRuntimeStrategy,
2686
+ options?: {
2687
+ readonly id?: string
2688
+ }
2689
+ ) {
2690
+ const runtime = yield* makeProcessRuntime
2691
+ const sessionId = yield* runtime.nextSessionId
2692
+ const emissions = makeEmissionRuntime()
2693
+ const started = yield* Deferred.make<MachineRef<State, Event, Error, Output, Emitted>, InitialError>()
2694
+ const internalOptions: StartInternalOptions = options === undefined
2695
+ ? {
2696
+ detached: true,
2697
+ emissions,
2698
+ runtime,
2699
+ sessionId
2700
+ }
2701
+ : {
2702
+ ...options,
2703
+ detached: true,
2704
+ emissions,
2705
+ runtime,
2706
+ sessionId
2707
+ }
2708
+ const initialize = strategy === "generic"
2709
+ ? startGenericInternal(logic, internalOptions)
2710
+ : strategy === "compiled"
2711
+ ? logic.execution?._tag === "Compiled"
2712
+ ? startCompactCompiledInternal(logic, internalOptions)
2713
+ : Effect.die(new Error("Machine cannot force the compiled runtime for generic process logic"))
2714
+ : startLogicInternal(logic, internalOptions)
2715
+ const start = yield* Effect.cached(
2716
+ initialize.pipe(
2717
+ Effect.onExit((exit) => Deferred.done(started, exit))
2718
+ ) as Effect.Effect<MachineRef<State, Event, Error, Output, Emitted>, InitialError, Requirements>
2719
+ )
2720
+ return {
2721
+ id: options?.id ?? sessionId,
2722
+ sessionId,
2723
+ changes: Stream.unwrap(
2724
+ Deferred.await(started).pipe(Effect.map((ref) => ref.changes))
2725
+ ),
2726
+ emissions: emissions.stream as Stream.Stream<Emitted>,
2727
+ start
2728
+ }
2729
+ })
2730
+
2731
+ export const prepareProcess: <
2732
+ State,
2733
+ Event,
2734
+ Error = never,
2735
+ Requirements = never,
2736
+ Output = never,
2737
+ InitialError = never,
2738
+ Emitted = never
2739
+ >(
2740
+ logic: ProcessLogic<State, Event, Error, Requirements, Output, InitialError>,
2741
+ options?: {
2742
+ readonly id?: string
2743
+ }
2744
+ ) => Effect.Effect<
2745
+ PreparedProcess<State, Event, Error, Output, Emitted, InitialError, Requirements>
2746
+ > =
2747
+ ((logic: ProcessLogic<any, any, any, any, any, any>, options?: { readonly id?: string }) =>
2748
+ prepareProcessWithStrategy(logic, "auto", options)) as any
2749
+
2750
+ /** @internal Test-only prepared startup strategy selection. */
2751
+ export const prepareProcessWithStrategyForTesting = <
2752
+ State,
2753
+ Event,
2754
+ Error = never,
2755
+ Requirements = never,
2756
+ Output = never,
2757
+ InitialError = never,
2758
+ Emitted = never
2759
+ >(
2760
+ logic: ProcessLogic<State, Event, Error, Requirements, Output, InitialError>,
2761
+ strategy: ProcessRuntimeStrategy,
2762
+ options?: {
2763
+ readonly id?: string
2764
+ }
2765
+ ): Effect.Effect<
2766
+ PreparedProcess<State, Event, Error, Output, Emitted, InitialError, Requirements>
2767
+ > => prepareProcessWithStrategy(logic, strategy, options) as any
@@ -21,6 +21,8 @@ export const HistoryTargetTypeId: unique symbol = Symbol("effect/Machine/History
21
21
 
22
22
  export const ChoiceTargetTypeId: unique symbol = Symbol("effect/Machine/ChoiceTarget")
23
23
 
24
+ export const NoTargetTypeId: unique symbol = Symbol("effect/Machine/NoTarget")
25
+
24
26
  interface StateInput {
25
27
  readonly [StateInputTypeId]: typeof StateInputTypeId
26
28
  readonly input: unknown
@@ -42,6 +44,19 @@ export interface ChoiceTarget {
42
44
  readonly values?: Readonly<Record<string, unknown>>
43
45
  }
44
46
 
47
+ /** Internal marker returned by an explicitly targetless transition. */
48
+ export interface NoTarget {
49
+ readonly [NoTargetTypeId]: typeof NoTargetTypeId
50
+ }
51
+
52
+ const noTarget = Object.freeze({
53
+ [NoTargetTypeId]: NoTargetTypeId
54
+ }) as NoTarget
55
+
56
+ export const makeNoTarget = (): Machine.NoTarget => noTarget
57
+
58
+ export const isNoTarget = (u: unknown): u is Machine.NoTarget => hasProperty(u, NoTargetTypeId)
59
+
45
60
  export const makeHistoryTarget = (path: string, parent: string): HistoryTarget => ({
46
61
  [HistoryTargetTypeId]: HistoryTargetTypeId,
47
62
  path,
@@ -1487,7 +1487,7 @@ export const compileModel = (model: FiniteModel): Machine.Machine.Any => {
1487
1487
  const initial = byPath.get(model.initial)!
1488
1488
  const machine = Machine.make({
1489
1489
  states: defined.states as any,
1490
- events: eventSchemas as any,
1490
+ events: Machine.events(...eventSchemas) as any,
1491
1491
  initial: () => selectSnapshot(defined.initial as any, initial.path, byPath, [initial.path], 0) as any
1492
1492
  })
1493
1493
  const transitions = new Map(model.transitions.map((transition) => [
@@ -15,6 +15,7 @@ import * as Stream from "effect/Stream"
15
15
  import { FastCheck, TestClock } from "effect/testing"
16
16
  import * as Machine from "../../../Machine.js"
17
17
  import type { CausalRuntimeEvidence, Probe, ProbeStep, RuntimeInvariant } from "../../../testing/MachineTest.js"
18
+ import * as Protocol from "../../machine/protocol.js"
18
19
  import { type SchemaArbitraryReport, toArbitraryWithReport } from "./arbitrary.js"
19
20
  import { assertRuntimeInvariants, type RuntimeInvariantError } from "./runtimeInvariant.js"
20
21
 
@@ -1523,7 +1524,7 @@ export const runtimeCommands = <M extends AnyMachine>(
1523
1524
 
1524
1525
  const reports: Array<SchemaArbitraryReport> = []
1525
1526
  const eventArbitraries = options.eventArbitrary === undefined
1526
- ? machine.events.map((schema) => {
1527
+ ? Protocol.inputEventSchemas(machine).map((schema) => {
1527
1528
  const derived = toArbitraryWithReport(schema)
1528
1529
  reports.push(derived.report)
1529
1530
  return derived.value as FastCheck.Arbitrary<Machine.Machine.InputEvent<M>>
@@ -35,6 +35,7 @@ import type {
35
35
  VerificationViolation,
36
36
  VerifyOptions
37
37
  } from "../../../testing/MachineTest.js"
38
+ import * as Protocol from "../../machine/protocol.js"
38
39
  import { toArbitraryWithReport } from "./arbitrary.js"
39
40
  import type { FiniteModel } from "./finiteModel.js"
40
41
  import * as ReferenceModel from "./referenceModel.js"
@@ -167,6 +168,7 @@ export const scenarios = <M extends AnyMachine>(
167
168
  machine: M,
168
169
  options: ScenarioOptions<M> = {} as ScenarioOptions<M>
169
170
  ): Scenarios<M> => {
171
+ const eventSchemas = Protocol.inputEventSchemas(machine)
170
172
  const minEvents = options.minEvents ?? 0
171
173
  const maxEvents = options.maxEvents ?? 50
172
174
  if (options.eventsArbitrary === undefined) {
@@ -175,7 +177,7 @@ export const scenarios = <M extends AnyMachine>(
175
177
  if (minEvents > maxEvents) {
176
178
  throw new Error("MachineTest.scenarios expected minEvents to be less than or equal to maxEvents")
177
179
  }
178
- if (machine.events.length === 0 && minEvents > 0) {
180
+ if (eventSchemas.length === 0 && minEvents > 0) {
179
181
  throw new Error(
180
182
  "MachineTest.scenarios cannot generate a non-empty event sequence for a machine without public events"
181
183
  )
@@ -184,7 +186,7 @@ export const scenarios = <M extends AnyMachine>(
184
186
 
185
187
  const diagnostics: Array<SchemaArbitraryDiagnostic> = []
186
188
  const eventArbitraries = options.eventsArbitrary === undefined
187
- ? machine.events.map((schema, index) => {
189
+ ? eventSchemas.map((schema, index) => {
188
190
  const derived = toArbitraryWithReport(schema)
189
191
  diagnostics.push({
190
192
  boundary: "event",
@@ -524,7 +526,7 @@ const publicEventTags = <M extends AnyMachine>(machine: M): {
524
526
  } => {
525
527
  const tags: Array<Machine.Machine.TagOf<Machine.Machine.InputEvents<M>[number]>> = []
526
528
  const diagnostics: Array<{ readonly schemaIndex: number; readonly message: string }> = []
527
- machine.events.forEach((schema, schemaIndex) => {
529
+ Protocol.inputEventSchemas(machine).forEach((schema, schemaIndex) => {
528
530
  const values = finiteTagValues(SchemaAST.toType(schema.ast))
529
531
  if (values === undefined) {
530
532
  diagnostics.push({
@@ -230,7 +230,7 @@ export interface Scenarios<M extends AnyMachine> {
230
230
  * const States = Machine.defineStates({ Idle })
231
231
  * const machine = Machine.make({
232
232
  * states: States.states,
233
- * events: [Reset],
233
+ * events: Machine.events(Reset),
234
234
  * initial: () => States.initial.Idle.from()
235
235
  * }).handle({ Idle: { on: { Reset: () => States.initial.Idle.from() } } })
236
236
  *
@@ -479,7 +479,7 @@ export { ProbeUnavailableError } from "../internal/testing/machine/verification.
479
479
  * const States = Machine.defineStates({ Idle })
480
480
  * const machine = Machine.make({
481
481
  * states: States.states,
482
- * events: [],
482
+ * events: Machine.events(),
483
483
  * initial: () => States.initial.Idle.from()
484
484
  * }).handle({ Idle: {} })
485
485
  *
@@ -1089,7 +1089,7 @@ export const Invariant: {
1089
1089
  * const States = Machine.defineStates({ Count })
1090
1090
  * const machine = Machine.make({
1091
1091
  * states: States.states,
1092
- * events: [],
1092
+ * events: Machine.events(),
1093
1093
  * initial: () => States.initial.Count(new Count({ value: 0 }))
1094
1094
  * }).handle({ Count: {} })
1095
1095
  *
@@ -1407,7 +1407,7 @@ export type ExploreOptions<M extends AnyMachine, Key extends ExplorationKey = Ex
1407
1407
  * const States = Machine.defineStates({ Count })
1408
1408
  * const machine = Machine.make({
1409
1409
  * states: States.states,
1410
- * events: [Increment],
1410
+ * events: Machine.events(Increment),
1411
1411
  * initial: () => States.initial.Count(new Count({ value: 0 }))
1412
1412
  * }).handle({
1413
1413
  * Count: { on: { Increment: ({ state }) =>
@@ -1591,7 +1591,7 @@ export type RunServices<M extends AnyMachine> = IsAny<
1591
1591
  * const States = Machine.defineStates({ Idle })
1592
1592
  * const machine = Machine.make({
1593
1593
  * states: States.states,
1594
- * events: [],
1594
+ * events: Machine.events(),
1595
1595
  * initial: () => States.initial.Idle.from()
1596
1596
  * }).handle({ Idle: {} })
1597
1597
  *
@@ -1818,7 +1818,7 @@ export interface Coverage<M extends AnyMachine> {
1818
1818
  * const States = Machine.defineStates({ Idle })
1819
1819
  * const machine = Machine.make({
1820
1820
  * states: States.states,
1821
- * events: [],
1821
+ * events: Machine.events(),
1822
1822
  * initial: () => States.initial.Idle.from()
1823
1823
  * }).handle({ Idle: {} })
1824
1824
  *
@@ -2034,7 +2034,7 @@ export interface VerifyOptions {
2034
2034
  * const States = Machine.defineStates({ Idle })
2035
2035
  * const machine = Machine.make({
2036
2036
  * states: States.states,
2037
- * events: [],
2037
+ * events: Machine.events(),
2038
2038
  * initial: () => States.initial.Idle.from()
2039
2039
  * }).handle({ Idle: {} })
2040
2040
  *
@@ -315,7 +315,7 @@ export const layerMemory: Layer.Layer<Storage> = internal.layerMemory
315
315
  * const States = Machine.defineStates({ Idle })
316
316
  * const machine = Machine.make({
317
317
  * states: States.states,
318
- * events: [],
318
+ * events: Machine.events(),
319
319
  * initial: () => States.initial.Idle.from()
320
320
  * }).handle({ Idle: {} })
321
321
  *