@typeonce/effect-machine 0.10.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 (33) hide show
  1. package/README.md +43 -4
  2. package/dist/Machine.d.ts +159 -47
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +29 -0
  5. package/dist/Machine.js.map +1 -1
  6. package/dist/internal/machine/atom.d.ts +1 -1
  7. package/dist/internal/machine/atom.d.ts.map +1 -1
  8. package/dist/internal/machine/atom.js +38 -7
  9. package/dist/internal/machine/atom.js.map +1 -1
  10. package/dist/internal/machine/machine.d.ts +1 -0
  11. package/dist/internal/machine/machine.d.ts.map +1 -1
  12. package/dist/internal/machine/machine.js +4 -0
  13. package/dist/internal/machine/machine.js.map +1 -1
  14. package/dist/internal/machine/process.d.ts +3 -0
  15. package/dist/internal/machine/process.d.ts.map +1 -1
  16. package/dist/internal/machine/process.js +3 -0
  17. package/dist/internal/machine/process.js.map +1 -1
  18. package/dist/internal/machine/runtime.d.ts +14 -0
  19. package/dist/internal/machine/runtime.d.ts.map +1 -1
  20. package/dist/internal/machine/runtime.js +82 -32
  21. package/dist/internal/machine/runtime.js.map +1 -1
  22. package/dist/unstable/reactivity/AtomMachine.d.ts +4 -2
  23. package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
  24. package/dist/unstable/reactivity/AtomMachine.js +4 -2
  25. package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
  26. package/docs/agent-guide.md +50 -8
  27. package/package.json +1 -1
  28. package/src/Machine.ts +661 -53
  29. package/src/internal/machine/atom.ts +50 -52
  30. package/src/internal/machine/machine.ts +6 -0
  31. package/src/internal/machine/process.ts +57 -0
  32. package/src/internal/machine/runtime.ts +172 -43
  33. package/src/unstable/reactivity/AtomMachine.ts +4 -2
@@ -7,11 +7,12 @@
7
7
  import * as Data from "effect/Data"
8
8
  import * as Effect from "effect/Effect"
9
9
  import * as Equal from "effect/Equal"
10
+ import * as Fiber from "effect/Fiber"
10
11
  import * as Option from "effect/Option"
11
12
  import type * as Schema from "effect/Schema"
12
13
  import type * as Scope from "effect/Scope"
13
14
  import * as Stream from "effect/Stream"
14
- import { AsyncResult, Atom, type AtomRegistry } from "effect/unstable/reactivity"
15
+ import { AsyncResult, Atom, AtomRegistry } from "effect/unstable/reactivity"
15
16
  import type * as Machine from "../../Machine.js"
16
17
  import type { Bound, ChildMachineAtom, MachineAtom } from "../../unstable/reactivity/AtomMachine.js"
17
18
  import * as internalMachine from "./machine.js"
@@ -72,6 +73,11 @@ type MachineStartError<InitialE, E, InitialR, R, RuntimeError = never> =
72
73
  | Machine.StoppedError
73
74
  | RuntimeError
74
75
 
76
+ const preparedByMachineAtom = new WeakMap<
77
+ object,
78
+ Atom.Atom<AsyncResult.AsyncResult<Machine.Prepared<any, any, any, any, any, any, any>, any>>
79
+ >()
80
+
75
81
  const runMachineAtomEffect = <State, Event, Error, Output, Emitted, StartError, Requirements>(
76
82
  get: Atom.AtomContext,
77
83
  start: Effect.Effect<Machine.MachineRef<State, Event, Error, Output, Emitted>, StartError, Requirements>
@@ -83,53 +89,16 @@ const runMachineAtomEffect = <State, Event, Error, Output, Emitted, StartError,
83
89
  )
84
90
  )
85
91
 
86
- const startMachineAtomEffect = <
87
- const States extends Machine.Machine.StateSchemas,
88
- const Events extends ReadonlyArray<Machine.Machine.TaggedSchema>,
89
- const Emits extends ReadonlyArray<Machine.Machine.TaggedSchema> = any,
90
- const Input extends Schema.Top = typeof Schema.Void,
91
- UnhandledStates extends Machine.Machine.StateIdentifier<States> = Machine.Machine.StateIdentifier<States>,
92
- E = never,
93
- R = never,
94
- InitialE = never,
95
- InitialR = never,
96
- FinalStates extends Machine.Machine.StateIdentifier<States> = never,
97
- Output = never,
98
- OutputStates extends Machine.Machine.StateIdentifier<States> = never,
99
- InputEvents extends ReadonlyArray<Machine.Machine.TaggedSchema> = Events,
100
- ParentEvents extends ReadonlyArray<Machine.Machine.TaggedSchema> = readonly []
101
- >(
92
+ const startPreparedMachineAtomEffect = <State, Event, Error, Output, Emitted, StartError, Requirements>(
102
93
  get: Atom.AtomContext,
103
- machine:
104
- & Machine.Machine<
105
- States,
106
- Events,
107
- Input,
108
- UnhandledStates,
109
- E,
110
- R,
111
- InitialE,
112
- InitialR,
113
- FinalStates,
114
- Output,
115
- Emits,
116
- OutputStates,
117
- InputEvents,
118
- ParentEvents
94
+ prepared: Atom.Atom<
95
+ AsyncResult.AsyncResult<
96
+ Machine.Prepared<State, Event, Error, Output, Emitted, StartError, Requirements>,
97
+ never
119
98
  >
120
- & EnsureExecutable<States, UnhandledStates, OutputStates>,
121
- args: [...Machine.Machine.InputArgs<Input>]
122
- ): Effect.Effect<
123
- Machine.MachineRef<
124
- Machine.Machine.Snapshot<States>,
125
- Machine.Machine.EventInputOf<InputEvents>,
126
- MachineRuntimeError<E, R>,
127
- Output,
128
- Machine.Machine.EmittedEventOf<Emits>
129
- >,
130
- MachineStartError<InitialE, E, InitialR, R>,
131
- MachineRequirements<InitialR, R, Machine.Machine.EventOf<Events>, Machine.Machine.EmitOf<Emits>>
132
- > => runMachineAtomEffect(get, internalMachine.start(machine as any, ...args) as any)
99
+ >
100
+ ): Effect.Effect<never, StartError, Requirements> =>
101
+ runMachineAtomEffect(get, get.result(prepared).pipe(Effect.flatMap((prepared) => prepared.start)))
133
102
 
134
103
  const resumeMachineAtomEffect = (
135
104
  get: Atom.AtomContext,
@@ -144,8 +113,31 @@ type RefEmitted<Ref> = Ref extends Machine.MachineRef<any, any, any, any, infer
144
113
 
145
114
  export const emissions = <State, Event, Error, Output, StartError, Emitted>(
146
115
  self: MachineAtom<State, Event, Error, Output, StartError, Emitted>
147
- ): Stream.Stream<Emitted, StartError, AtomRegistry.AtomRegistry> =>
148
- Atom.toStreamResult(self.ref).pipe(Stream.flatMap((ref) => ref.emissions))
116
+ ): Stream.Stream<Emitted, StartError, AtomRegistry.AtomRegistry> => {
117
+ const prepared = preparedByMachineAtom.get(self as object)
118
+ if (prepared === undefined) {
119
+ return Atom.toStreamResult(self.ref).pipe(Stream.flatMap((ref) => ref.emissions))
120
+ }
121
+ return Stream.unwrap(
122
+ Effect.gen(function*() {
123
+ const registry = yield* AtomRegistry.AtomRegistry
124
+ const releasePrepared = yield* Effect.sync(() => registry.mount(prepared))
125
+ yield* Effect.addFinalizer(() => Effect.sync(releasePrepared))
126
+ const machine = yield* Atom.getResult(prepared)
127
+ const pull = yield* Stream.toPull(machine.emissions as Stream.Stream<Emitted>)
128
+ const firstPull = yield* pull.pipe(Effect.forkScoped({ startImmediately: true }))
129
+ const releaseRef = yield* Effect.sync(() => registry.mount(self.ref))
130
+ yield* Effect.addFinalizer(() => Effect.sync(releaseRef))
131
+ yield* Atom.getResult(self.ref)
132
+ let first = true
133
+ return Stream.fromPull(Effect.succeed(Effect.suspend(() => {
134
+ if (!first) return pull
135
+ first = false
136
+ return Fiber.join(firstPull)
137
+ })))
138
+ })
139
+ )
140
+ }
149
141
 
150
142
  export const childEmissions = <Child extends Machine.ChildMachine.Any, StartError>(
151
143
  self: ChildMachineAtom<Child, StartError>
@@ -666,8 +658,11 @@ export const make: {
666
658
  Machine.Machine.EmittedEventOf<Emits>
667
659
  >
668
660
  } = ((machine: Machine.Machine.Any, ...args: ReadonlyArray<unknown>) => {
669
- const ref = Atom.make((get) => startMachineAtomEffect(get, machine as any, args as []))
670
- return makeFromRefAtom(ref as any)
661
+ const prepared = Atom.make(() => internalMachine.prepare(machine as any, ...(args as [])))
662
+ const ref = Atom.make((get) => startPreparedMachineAtomEffect(get, prepared as any))
663
+ const result = makeFromRefAtom(ref as any)
664
+ preparedByMachineAtom.set(result, prepared as any)
665
+ return result
671
666
  }) as any
672
667
 
673
668
  export const resume: {
@@ -688,8 +683,11 @@ const makeWithRuntime = (
688
683
  machine: Machine.Machine.Any,
689
684
  args: ReadonlyArray<unknown>
690
685
  ): MachineAtom<any, any, any, any, any> => {
691
- const ref = runtime.atom((get) => startMachineAtomEffect(get, machine as any, args as []))
692
- return makeFromRefAtom(ref as any)
686
+ const prepared = runtime.atom(() => internalMachine.prepare(machine as any, ...(args as [])))
687
+ const ref = runtime.atom((get) => startPreparedMachineAtomEffect(get, prepared as any))
688
+ const result = makeFromRefAtom(ref as any)
689
+ preparedByMachineAtom.set(result, prepared as any)
690
+ return result
693
691
  }
694
692
 
695
693
  const resumeWithRuntime = (
@@ -104,6 +104,8 @@ const Proto = {
104
104
  }
105
105
  }
106
106
 
107
+ const makeBoundInvoke = (config: unknown): unknown => config
108
+
107
109
  const cloneWithHandlers = (
108
110
  self: Machine.Any,
109
111
  handlers: Machine.StateConfigs<any, any, any, any, any, any, any>
@@ -121,6 +123,7 @@ const cloneWithHandlers = (
121
123
  machine.makeTargetBuilder = self.makeTargetBuilder
122
124
  machine.handlers = handlers
123
125
  machine.handle = makeHandle(machine)
126
+ machine.invoke = makeBoundInvoke
124
127
  Protocol.copyProtocol(self, machine)
125
128
  return machine
126
129
  }
@@ -879,6 +882,7 @@ export const make: Make = (<
879
882
  self.makeTargetBuilder = makeTargetBuilder(config.states, self.stateNodes)
880
883
  self.handlers = Object.create(null)
881
884
  self.handle = makeHandle(self)
885
+ self.invoke = makeBoundInvoke
882
886
  Protocol.setProtocol(self)
883
887
  return self
884
888
  }) as Make
@@ -1358,6 +1362,8 @@ export const watch = <State, Event, Error = never, Output = never>(
1358
1362
  ref: MachineRef<State, Event, Error, Output>
1359
1363
  ): Stream.Stream<RuntimeOutcome<State, Error, Output>> => internalRuntime.watch(ref)
1360
1364
 
1365
+ export const prepare = internalProcess.prepare
1366
+
1361
1367
  export const start: <
1362
1368
  const States extends Machine.StateSchemas,
1363
1369
  const Events extends ReadonlyArray<Machine.TaggedSchema>,
@@ -825,6 +825,18 @@ export const startWithRuntimeStrategyForTesting = (
825
825
  machine.id === undefined ? undefined : { id: machine.id }
826
826
  )
827
827
 
828
+ /** @internal Test-only prepared startup strategy selection for a fresh machine. */
829
+ export const prepareWithRuntimeStrategyForTesting = (
830
+ machine: Machine.Any,
831
+ strategy: internalRuntime.ProcessRuntimeStrategy,
832
+ ...args: ReadonlyArray<unknown>
833
+ ): Effect.Effect<internalRuntime.PreparedProcess<any, any, any, any, any, any, any>, any, any> =>
834
+ internalRuntime.prepareProcessWithStrategyForTesting(
835
+ (toProcessLogic as any)(machine, ...args),
836
+ strategy,
837
+ machine.id === undefined ? undefined : { id: machine.id }
838
+ )
839
+
828
840
  /** @internal Test-only runtime strategy selection for a resumed machine. */
829
841
  export const resumeWithRuntimeStrategyForTesting = (
830
842
  machine: Machine.Any,
@@ -881,6 +893,51 @@ export const start: <
881
893
  machine.id === undefined ? undefined : { id: machine.id }
882
894
  ) as any
883
895
 
896
+ export const prepare: <
897
+ const States extends Machine.StateSchemas,
898
+ const Events extends ReadonlyArray<Machine.TaggedSchema>,
899
+ const Emits extends ReadonlyArray<Machine.TaggedSchema> = readonly [],
900
+ const Input extends Schema.Top = typeof Schema.Void,
901
+ UnhandledStates extends Machine.StateIdentifier<States> = Machine.StateIdentifier<States>,
902
+ E = never,
903
+ R = never,
904
+ InitialE = never,
905
+ InitialR = never,
906
+ FinalStates extends Machine.StateIdentifier<States> = never,
907
+ Output = never
908
+ >(
909
+ machine: Machine<States, Events, Input, UnhandledStates, E, R, InitialE, InitialR, FinalStates, Output, Emits>,
910
+ ...args: [...Machine.InputArgs<Input>]
911
+ ) => Effect.Effect<
912
+ internalRuntime.PreparedProcess<
913
+ Machine.Snapshot<States>,
914
+ Machine.EventOf<Events>,
915
+ | E
916
+ | ActionError<R>
917
+ | InfiniteTransitionError
918
+ | MachineSchemaDecodeError
919
+ | StoppedError,
920
+ Output,
921
+ Machine.EmittedEventOf<Emits>,
922
+ | InitialE
923
+ | E
924
+ | ActionError<InitialR | R>
925
+ | InfiniteTransitionError
926
+ | MachineSchemaDecodeError
927
+ | StartupError
928
+ | StoppedError,
929
+ ExcludeCompatibleRuntime<
930
+ Exclude<ExecutionServices<InitialR | R>, internalRuntime.MachineRuntime>,
931
+ Machine.EventOf<Events>,
932
+ Machine.EmitOf<Emits>
933
+ >
934
+ >
935
+ > = (machine, ...args) =>
936
+ internalRuntime.prepareProcess(
937
+ toProcessLogic(machine, ...args),
938
+ machine.id === undefined ? undefined : { id: machine.id }
939
+ ) as any
940
+
884
941
  export const resume: <
885
942
  const States extends Machine.StateSchemas,
886
943
  const Events extends ReadonlyArray<Machine.TaggedSchema>,
@@ -302,6 +302,22 @@ export interface MachineRef<out State, in Event, out Error = never, out Output =
302
302
  readonly childChanges: (child: any) => Stream.Stream<Option.Option<any>>
303
303
  }
304
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
+
305
321
  interface ProcessAddress<in Event> {
306
322
  readonly id: string
307
323
  readonly sessionId: string
@@ -609,6 +625,8 @@ const makeProcessRuntime: Effect.Effect<ProcessRuntime> = Effect.sync(() => {
609
625
  interface StartInternalOptions {
610
626
  readonly detached?: boolean
611
627
  readonly id?: string
628
+ readonly sessionId?: string
629
+ readonly emissions?: EmissionRuntime
612
630
  readonly onOutcome?: (outcome: RuntimeOutcome<any, any, any>) => Effect.Effect<void>
613
631
  readonly onSnapshot?: (
614
632
  snapshot: Extract<RuntimeSnapshot<any, any, any>, { readonly status: "active" }>
@@ -802,6 +820,54 @@ const EmissionsClosed: unique symbol = Symbol("effect/Machine/EmissionsClosed")
802
820
 
803
821
  type LazyEmissions = PubSub.PubSub<unknown> | typeof EmissionsClosed | undefined
804
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
+ }
870
+
805
871
  const childlessRuntime: ChildRuntime = {
806
872
  close: () => Effect.void,
807
873
  spawn: (() => Effect.die(new Error("Childless machine logic cannot spawn a process"))) as ProcessSpawn,
@@ -1104,41 +1170,10 @@ const startGenericInternal: <
1104
1170
  | { readonly _tag: "Done"; readonly output: Output }
1105
1171
  | { readonly _tag: "Failure"; readonly cause: Cause.Cause<Error> }
1106
1172
 
1107
- const sessionId = yield* runtime.nextSessionId
1173
+ const sessionId = options.sessionId ?? (yield* runtime.nextSessionId)
1108
1174
  const id = requestedId ?? sessionId
1109
1175
  const queue = yield* Queue.unbounded<ProcessMessage<Event>>()
1110
- let emissions: LazyEmissions
1111
- const getOrCreateEmissions: Effect.Effect<PubSub.PubSub<unknown> | undefined> = Effect.suspend(() => {
1112
- const observed = emissions
1113
- if (observed === EmissionsClosed) return Effect.succeed(undefined)
1114
- if (observed !== undefined) return Effect.succeed(observed)
1115
- return PubSub.unbounded<unknown>().pipe(
1116
- Effect.flatMap((candidate) =>
1117
- Effect.sync(() => {
1118
- const latest = emissions
1119
- if (latest === EmissionsClosed) return [undefined, true] as const
1120
- if (latest !== undefined) return [latest, true] as const
1121
- emissions = candidate
1122
- return [candidate, false] as const
1123
- }).pipe(
1124
- Effect.flatMap(([selected, discard]) =>
1125
- discard ? PubSub.shutdown(candidate).pipe(Effect.as(selected)) : Effect.succeed(selected)
1126
- )
1127
- )
1128
- )
1129
- )
1130
- })
1131
- const closeEmissions = (): Effect.Effect<void> => {
1132
- const observed = emissions
1133
- emissions = EmissionsClosed
1134
- return observed === undefined || observed === EmissionsClosed ? Effect.void : PubSub.shutdown(observed)
1135
- }
1136
- const emit = (event: unknown): Effect.Effect<void> =>
1137
- Effect.suspend(() =>
1138
- emissions === undefined || emissions === EmissionsClosed
1139
- ? Effect.void
1140
- : PubSub.publish(emissions, event).pipe(Effect.asVoid)
1141
- )
1176
+ const emissions = options.emissions ?? makeEmissionRuntime()
1142
1177
  const termination = yield* Deferred.make<ProcessTermination>()
1143
1178
  const done = yield* Deferred.make<Output, Error | StoppedError>()
1144
1179
  const awaitCompletion = Deferred.await(done).pipe(Effect.exit, Effect.asVoid)
@@ -1209,7 +1244,7 @@ const startGenericInternal: <
1209
1244
  }
1210
1245
  const cleanupStartupFailure = <A, E>(exit: Exit.Exit<A, E>): Effect.Effect<void> =>
1211
1246
  Exit.isFailure(exit)
1212
- ? closeChildren(exit).pipe(Effect.andThen(closeEmissions()))
1247
+ ? closeChildren(exit).pipe(Effect.andThen(emissions.close()))
1213
1248
  : Effect.void
1214
1249
  const cleanup = onStopSync === undefined ? onStop ?? Effect.void : Effect.sync(onStopSync)
1215
1250
  const sendParent = overrideSendParent ?? (parent === undefined ? noParentSend : parent.send)
@@ -1219,7 +1254,7 @@ const startGenericInternal: <
1219
1254
  parent,
1220
1255
  spawn,
1221
1256
  sendParent,
1222
- emit,
1257
+ emit: emissions.emit,
1223
1258
  sendTo: ((target: unknown, event: unknown) =>
1224
1259
  isProcessAddress(target) ? target.send(event) : sendTo(target as ChildSelector, event)) as ProcessScope<
1225
1260
  Event
@@ -1402,7 +1437,7 @@ const startGenericInternal: <
1402
1437
  Effect.andThen(Queue.shutdown(queue)),
1403
1438
  Effect.andThen(closeChildren(exit)),
1404
1439
  Effect.andThen(setAndPublishSnapshot(snapshot)),
1405
- Effect.andThen(closeEmissions()),
1440
+ Effect.andThen(emissions.close()),
1406
1441
  Effect.andThen(Effect.sync(() => {
1407
1442
  if (Exit.isFailure(exit)) {
1408
1443
  failAcknowledgedMessage(inFlightMessage, exit.cause)
@@ -1559,11 +1594,7 @@ const startGenericInternal: <
1559
1594
  state: SynchronizedRef.get(current).pipe(Effect.map((current) => current.snapshot.state)),
1560
1595
  snapshot: SynchronizedRef.get(current).pipe(Effect.map((current) => current.snapshot)),
1561
1596
  changes: changesStream,
1562
- emissions: Stream.unwrap(
1563
- getOrCreateEmissions.pipe(
1564
- Effect.map((emissions) => emissions === undefined ? Stream.empty : Stream.fromPubSub(emissions))
1565
- )
1566
- ) as Stream.Stream<never>,
1597
+ emissions: emissions.stream as Stream.Stream<never>,
1567
1598
  join: Deferred.await(done),
1568
1599
  stop,
1569
1600
  send: self.send,
@@ -1717,6 +1748,7 @@ class CompiledProcess implements MachineRef<any, any, any, any> {
1717
1748
  private interruptRequested = false
1718
1749
  private offerRevision = 0
1719
1750
  private inFlightMessage: ProcessMessage<unknown> | undefined
1751
+ private readonly externalEmissions: EmissionRuntime | undefined
1720
1752
  private emissionsPubSub: LazyEmissions
1721
1753
 
1722
1754
  constructor(
@@ -1727,6 +1759,7 @@ class CompiledProcess implements MachineRef<any, any, any, any> {
1727
1759
  ) {
1728
1760
  this.sessionId = sessionId
1729
1761
  this.id = options.id ?? sessionId
1762
+ this.externalEmissions = options.emissions
1730
1763
  this.send = (event) => this.offerMessage(event)
1731
1764
  this.address = {
1732
1765
  id: this.id,
@@ -1908,7 +1941,7 @@ class CompiledProcess implements MachineRef<any, any, any, any> {
1908
1941
  }
1909
1942
 
1910
1943
  get emissions(): Stream.Stream<never> {
1911
- return this.emissionsStream() as Stream.Stream<never>
1944
+ return (this.externalEmissions?.stream ?? this.emissionsStream()) as Stream.Stream<never>
1912
1945
  }
1913
1946
 
1914
1947
  get join(): Effect.Effect<unknown, unknown> {
@@ -2268,6 +2301,7 @@ class CompiledProcess implements MachineRef<any, any, any, any> {
2268
2301
  }
2269
2302
 
2270
2303
  private emitEvent(event: unknown): Effect.Effect<void> {
2304
+ if (this.externalEmissions !== undefined) return this.externalEmissions.emit(event)
2271
2305
  return Effect.suspend(() =>
2272
2306
  this.emissionsPubSub === undefined || this.emissionsPubSub === EmissionsClosed
2273
2307
  ? Effect.void
@@ -2280,6 +2314,7 @@ class CompiledProcess implements MachineRef<any, any, any, any> {
2280
2314
  }
2281
2315
 
2282
2316
  private closeEmissions(): Effect.Effect<void> {
2317
+ if (this.externalEmissions !== undefined) return this.externalEmissions.close()
2283
2318
  const observed = this.emissionsPubSub
2284
2319
  this.emissionsPubSub = EmissionsClosed
2285
2320
  return observed === undefined || observed === EmissionsClosed ? Effect.void : PubSub.shutdown(observed)
@@ -2523,7 +2558,7 @@ const startCompactCompiledInternal: typeof startGenericInternal = Effect.fnUntra
2523
2558
  logic: ProcessLogic<any, any, any, any, any, any>,
2524
2559
  options: StartInternalOptions
2525
2560
  ) {
2526
- const sessionId = yield* options.runtime.nextSessionId
2561
+ const sessionId = options.sessionId ?? (yield* options.runtime.nextSessionId)
2527
2562
  const services = yield* Effect.context<any>()
2528
2563
  const execution = logic.execution as CompiledProcessExecution<any, any, any, any, any, any>
2529
2564
  const process = new CompiledProcess(logic, options, services, sessionId)
@@ -2636,3 +2671,97 @@ export const startProcess: <
2636
2671
  }
2637
2672
  )
2638
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
@@ -167,8 +167,10 @@ type RefOutput<Ref> = Ref extends Machine.MachineRef<any, any, any, infer Output
167
167
  type RefEmitted<Ref> = Ref extends Machine.MachineRef<any, any, any, any, infer Emitted> ? Emitted : never
168
168
 
169
169
  /**
170
- * Observes ephemeral notifications from the actor owned by a machine atom.
171
- * The stream requires an `AtomRegistry` and does not replay earlier emissions.
170
+ * Observes ephemeral notifications from the running machine owned by a machine
171
+ * atom. When this stream activates a fresh bridge, it subscribes before machine
172
+ * initialization and observes initial-entry emissions. It never replays
173
+ * emissions from a machine that was already running.
172
174
  *
173
175
  * @category getters
174
176
  * @since 0.10.0