@@ -8,6 +8,7 @@ import * as Effect from "effect/Effect"
88import * as Exit from "effect/Exit"
99import * as HashMap from "effect/HashMap"
1010import * as Option from "effect/Option"
11+ import * as Queue from "effect/Queue"
1112import * as Ref from "effect/Ref"
1213import type * as Schema from "effect/Schema"
1314import * as Scope from "effect/Scope"
@@ -147,14 +148,14 @@ const makeProcessLogic: <
147148 run : ( context ) =>
148149 internalRuntime . provideMachineRuntime (
149150 Effect . gen ( function * ( ) {
150- const { receive, state, setState } = context
151+ const { mailbox , receive, state, setState } = context
151152 let terminal : { readonly output : Output } | undefined
152153
153- const initialState = yield * state
154- if ( internalPlanner . isFinalState ( machine , initialState ) ) {
154+ let current = yield * state
155+ if ( internalPlanner . isFinalState ( machine , current ) ) {
155156 return yield * internalPlanner . getFinalOutputEffect < States , Events , Output > (
156157 machine ,
157- initialState ,
158+ current ,
158159 internalPlanner . InitialEvent
159160 )
160161 }
@@ -165,26 +166,46 @@ const makeProcessLogic: <
165166 )
166167
167168 if ( ! hasInvokes ) {
169+ // A queued batch is produced entirely by this worker, so its
170+ // configuration is already validated. Drop both caches before
171+ // blocking again so idle machines retain only the public snapshot.
172+ let configuration : Model . ActiveConfiguration | undefined
173+ let pendingEvent : Option . Option < Machine . EventOf < Events > > = Option . none ( )
174+ let pollEvent : Effect . Effect < Option . Option < Machine . EventOf < Events > > > | undefined
168175 yield * Effect . whileLoop ( {
169176 while : ( ) => terminal === undefined ,
170177 body : ( ) =>
171178 Effect . gen ( function * ( ) {
172- const event = yield * receive
173- const current = yield * state
174- const planned = yield * internalPlanner . plan ( machine , current , event )
175- if ( planned . microsteps . length === 0 ) {
176- return
177- }
178-
179- yield * internalPlanner . runActions ( planned . actions , liveRuntime )
180- yield * setState ( planned . next )
181- yield * internalPlanner . runEmittedEvents (
182- planned . emittedEvents as ReadonlyArray < Machine . EmitOf < Emits > > ,
183- liveRuntime
179+ const event = Option . isSome ( pendingEvent ) ? pendingEvent . value : yield * receive
180+ pendingEvent = Option . none ( )
181+ const planned = yield * internalPlanner . planConfiguration (
182+ machine ,
183+ configuration ?? ( yield * Model . normalizeConfigurationEffect ( machine , current ) ) ,
184+ event
184185 )
186+ configuration = planned . next
187+
188+ if ( planned . microsteps . length > 0 ) {
189+ const next = Model . snapshotFromConfiguration < States > ( machine , planned . next )
190+ yield * internalPlanner . runActions ( planned . actions , liveRuntime )
191+ yield * setState ( next )
192+ current = next
193+ yield * internalPlanner . runEmittedEvents (
194+ planned . emittedEvents as ReadonlyArray < Machine . EmitOf < Emits > > ,
195+ liveRuntime
196+ )
185197
186- if ( planned . done ) {
187- terminal = { output : planned . output }
198+ if ( planned . done ) {
199+ terminal = { output : planned . output }
200+ }
201+ }
202+
203+ if ( terminal === undefined ) {
204+ pendingEvent = yield * ( pollEvent ??= Queue . poll ( mailbox ) )
205+ if ( Option . isNone ( pendingEvent ) ) {
206+ configuration = undefined
207+ pollEvent = undefined
208+ }
188209 }
189210 } ) ,
190211 step : ( ) => undefined
@@ -349,15 +370,14 @@ const makeProcessLogic: <
349370 yield * startInvokeWatchers ( config , child , key , token , scope )
350371 } )
351372 const startInvokes : (
352- state : Machine . Snapshot < States > ,
373+ configuration : Model . ActiveConfiguration ,
353374 paths : ReadonlyArray < string > ,
354375 event : Machine . LifecycleEvent < Events >
355376 ) => Effect . Effect < void , E | MachineSchemaDecodeError , R > = Effect . fnUntraced ( function * (
356- state : Machine . Snapshot < States > ,
377+ configuration : Model . ActiveConfiguration ,
357378 paths : ReadonlyArray < string > ,
358379 event : Machine . LifecycleEvent < Events >
359380 ) {
360- const configuration = yield * Model . normalizeConfigurationEffect ( machine , state )
361381 yield * Effect . all (
362382 internalPlanner . sortEntryPaths ( machine , paths )
363383 . filter ( ( path ) => configuration . active . has ( path ) )
@@ -393,53 +413,76 @@ const makeProcessLogic: <
393413 )
394414
395415 return yield * Effect . gen ( function * ( ) {
416+ let configuration : Model . ActiveConfiguration | undefined = yield * Model . normalizeConfigurationEffect (
417+ machine ,
418+ current
419+ )
396420 yield * startInvokes (
397- initialState ,
398- Model . getInitialEntryPaths ( machine , yield * Model . normalizeConfigurationEffect ( machine , initialState ) ) ,
421+ configuration ,
422+ Model . getInitialEntryPaths ( machine , configuration ) ,
399423 internalPlanner . InitialEvent
400424 )
425+ // As above, keep the normalized configuration only while this
426+ // worker can continue draining an already queued batch.
427+ configuration = undefined
428+ let pendingEvent : Option . Option < Machine . EventOf < Events > > = Option . none ( )
429+ let pollEvent : Effect . Effect < Option . Option < Machine . EventOf < Events > > > | undefined
401430
402431 yield * Effect . whileLoop ( {
403432 while : ( ) => terminal === undefined ,
404433 body : ( ) =>
405434 Effect . gen ( function * ( ) {
406- const event = yield * receive
407- const current = yield * state
408- const planned = yield * internalPlanner . plan ( machine , current , event )
409- if ( planned . microsteps . length === 0 ) {
410- return
411- }
412- const changed = planned . microsteps . some ( ( step ) => step . changed )
413- const exitPaths = planned . microsteps . flatMap ( ( step ) => step . exitPaths )
414- const entryEvents = new Map < string , Machine . LifecycleEvent < Events > > ( )
415- for ( const step of planned . microsteps ) {
416- if ( step . changed ) {
417- for ( const path of step . entryPaths ) {
418- entryEvents . set ( path , step . event as Machine . LifecycleEvent < Events > )
435+ const event = Option . isSome ( pendingEvent ) ? pendingEvent . value : yield * receive
436+ pendingEvent = Option . none ( )
437+ const planned = yield * internalPlanner . planConfiguration (
438+ machine ,
439+ configuration ?? ( yield * Model . normalizeConfigurationEffect ( machine , current ) ) ,
440+ event
441+ )
442+ configuration = planned . next
443+ if ( planned . microsteps . length > 0 ) {
444+ const changed = planned . microsteps . some ( ( step ) => step . changed )
445+ const exitPaths = planned . microsteps . flatMap ( ( step ) => step . exitPaths )
446+ const entryEvents = new Map < string , Machine . LifecycleEvent < Events > > ( )
447+ for ( const step of planned . microsteps ) {
448+ if ( step . changed ) {
449+ for ( const path of step . entryPaths ) {
450+ entryEvents . set ( path , step . event as Machine . LifecycleEvent < Events > )
451+ }
419452 }
420453 }
421- }
422-
423- yield * internalPlanner . runActions ( planned . actions , liveRuntime )
424- if ( changed ) {
425- yield * stopInvokes ( exitPaths )
426- }
427- yield * setState ( planned . next )
428- yield * internalPlanner . runEmittedEvents (
429- planned . emittedEvents as ReadonlyArray < Machine . EmitOf < Emits > > ,
430- liveRuntime
431- )
432454
433- if ( planned . done ) {
434- terminal = { output : planned . output }
435- yield * stopAllInvokes ( Exit . succeed ( planned . output ) )
436- } else {
455+ const next = Model . snapshotFromConfiguration < States > ( machine , planned . next )
456+ yield * internalPlanner . runActions ( planned . actions , liveRuntime )
437457 if ( changed ) {
438- for ( const [ path , entryEvent ] of entryEvents ) {
439- yield * startInvokes ( planned . next , [ path ] , entryEvent )
458+ yield * stopInvokes ( exitPaths )
459+ }
460+ yield * setState ( next )
461+ current = next
462+ yield * internalPlanner . runEmittedEvents (
463+ planned . emittedEvents as ReadonlyArray < Machine . EmitOf < Emits > > ,
464+ liveRuntime
465+ )
466+
467+ if ( planned . done ) {
468+ terminal = { output : planned . output }
469+ yield * stopAllInvokes ( Exit . succeed ( planned . output ) )
470+ } else {
471+ if ( changed ) {
472+ for ( const [ path , entryEvent ] of entryEvents ) {
473+ yield * startInvokes ( planned . next , [ path ] , entryEvent )
474+ }
440475 }
441476 }
442477 }
478+
479+ if ( terminal === undefined ) {
480+ pendingEvent = yield * ( pollEvent ??= Queue . poll ( mailbox ) )
481+ if ( Option . isNone ( pendingEvent ) ) {
482+ configuration = undefined
483+ pollEvent = undefined
484+ }
485+ }
443486 } ) ,
444487 step : ( ) => undefined
445488 } )
0 commit comments