@@ -119,6 +119,13 @@ const makeProcessLogic: <
119119 entry : ProcessEntry < States , Input >
120120) => {
121121 const hasInvokes = hasInvokeCapability ( machine )
122+ // A process logic value may be reused for multiple starts. Key retained
123+ // invoke state by the stable process address so each running instance owns
124+ // its own table even when an invoke wrapper decorates the context object.
125+ const invokeSessionsByContext = new WeakMap < object , {
126+ readonly sessions : Map < string , InvokeSession >
127+ initialized : boolean
128+ } > ( )
122129 return ( {
123130 [ internalRuntime . childlessProcess ] : hasInvokes ? undefined : true ,
124131 [ internalRuntime . compiledProcess ] : true ,
@@ -143,6 +150,300 @@ const makeProcessLogic: <
143150 } ) ,
144151 scope
145152 ) ,
153+ drain : ( context : internalRuntime . ProcessContext < Machine . Snapshot < States > , Machine . EventOf < Events > > ) =>
154+ internalRuntime . provideMachineRuntime (
155+ Effect . gen ( function * ( ) {
156+ const { mailbox, state, setState } = context
157+ let current = yield * state
158+ if ( internalPlanner . isFinalState ( machine , current ) ) {
159+ return Option . some (
160+ yield * internalPlanner . getFinalOutputEffect < States , Events , Output > (
161+ machine ,
162+ current ,
163+ internalPlanner . InitialEvent
164+ )
165+ )
166+ }
167+
168+ let configuration : Model . ActiveConfiguration | undefined
169+ let liveRuntime : Runtime < Machine . EventOf < Events > , Machine . EmitOf < Emits > > | undefined
170+
171+ let stopAllInvokes : Effect . Effect < void > | undefined
172+ let startInvokes :
173+ | ( (
174+ configuration : Model . ActiveConfiguration ,
175+ paths : ReadonlyArray < string > ,
176+ event : Machine . LifecycleEvent < Events >
177+ ) => Effect . Effect < void , E | MachineSchemaDecodeError , R > )
178+ | undefined
179+ let stopInvokes : ( ( paths : ReadonlyArray < string > ) => Effect . Effect < void > ) | undefined
180+
181+ if ( hasInvokes ) {
182+ let session = invokeSessionsByContext . get ( context . self )
183+ if ( session === undefined ) {
184+ session = { sessions : new Map ( ) , initialized : false }
185+ invokeSessionsByContext . set ( context . self , session )
186+ }
187+ const invokeSessions = session . sessions
188+ const makeInvokeSessionKey = ( path : string , id : string ) : string => `${ path . length } :${ path } ${ id } `
189+ const makeInvokeChildId = ( path : string , id : string ) : string =>
190+ `Machine.invoke:${ makeInvokeSessionKey ( path , id ) } `
191+ const isCurrentInvoke = ( key : string , token : symbol ) : Effect . Effect < boolean > =>
192+ Effect . sync ( ( ) => invokeSessions . get ( key ) ?. token === token )
193+ const stopInvokeSession = ( session : InvokeSession ) : Effect . Effect < void > =>
194+ context . stopChild ( session . childId )
195+ const removeInvoke = (
196+ key : string ,
197+ token : symbol | undefined
198+ ) : Effect . Effect < void > =>
199+ Effect . sync ( ( ) => {
200+ const current = invokeSessions . get ( key )
201+ if ( current === undefined || ( token !== undefined && current . token !== token ) ) {
202+ return undefined
203+ }
204+ invokeSessions . delete ( key )
205+ return current
206+ } ) . pipe (
207+ Effect . flatMap ( ( session ) =>
208+ session === undefined
209+ ? Effect . void
210+ : stopInvokeSession ( session )
211+ )
212+ )
213+ const stopInvoke = ( key : string ) : Effect . Effect < void > => removeInvoke ( key , undefined )
214+ stopAllInvokes = Effect . sync ( ( ) => {
215+ const sessions = Array . from ( invokeSessions . values ( ) )
216+ invokeSessions . clear ( )
217+ return sessions
218+ } ) . pipe (
219+ Effect . flatMap ( ( sessions ) =>
220+ Effect . all (
221+ sessions . map ( ( session ) => stopInvokeSession ( session ) ) ,
222+ { discard : true , concurrency : "unbounded" }
223+ )
224+ )
225+ )
226+ const handleInvokeOutcome = (
227+ config : AnyInvokeConfig ,
228+ key : string ,
229+ token : symbol ,
230+ outcome : internalRuntime . RuntimeOutcome < any , any , any >
231+ ) : Effect . Effect < void > => {
232+ if ( outcome . _tag === "Stopped" ) {
233+ return Effect . void
234+ }
235+ return isCurrentInvoke ( key , token ) . pipe (
236+ Effect . flatMap ( ( isCurrent ) => {
237+ if ( ! isCurrent ) {
238+ return Effect . void
239+ }
240+ if ( outcome . _tag === "Done" ) {
241+ const mappedEvent = config . onDone === undefined
242+ ? outcome . output
243+ : config . onDone ( { id : config . id , output : outcome . output } )
244+ return mappedEvent === undefined
245+ ? Effect . void
246+ : context . self . send ( mappedEvent as Machine . EventOf < Events > ) . pipe (
247+ Effect . catchTag ( "StoppedError" , ( ) => Effect . void )
248+ )
249+ }
250+ return context . failCause ( outcome . cause )
251+ } )
252+ )
253+ }
254+ const handleInvokeSnapshot = (
255+ config : AnyInvokeConfig ,
256+ key : string ,
257+ token : symbol ,
258+ snapshot : Extract < internalRuntime . RuntimeSnapshot < any , any , any > , { readonly status : "active" } >
259+ ) : Effect . Effect < void > =>
260+ isCurrentInvoke ( key , token ) . pipe (
261+ Effect . flatMap ( ( isCurrent ) => {
262+ if ( ! isCurrent || config . snapshot === undefined ) {
263+ return Effect . void
264+ }
265+ const mappedEvent = config . snapshot ( { id : config . id , snapshot } )
266+ return mappedEvent === undefined
267+ ? Effect . void
268+ : context . self . send ( mappedEvent as Machine . EventOf < Events > ) . pipe (
269+ Effect . catchTag ( "StoppedError" , ( ) => Effect . void )
270+ )
271+ } )
272+ )
273+ const startInvoke = Effect . fnUntraced ( function * < StateId extends Machine . StateIdentifier < States > > (
274+ path : StateId ,
275+ config : AnyInvokeConfig
276+ ) {
277+ const token = Symbol ( )
278+ const invokeId = String ( config . id )
279+ const key = makeInvokeSessionKey ( path , invokeId )
280+ const childId = config . address === undefined ? makeInvokeChildId ( path , invokeId ) : String ( config . address )
281+ const reserved = yield * Effect . sync ( ( ) => {
282+ if ( invokeSessions . has ( key ) ) {
283+ return false
284+ }
285+ invokeSessions . set ( key , { token, childId, path } )
286+ return true
287+ } )
288+ if ( ! reserved ) {
289+ return yield * Effect . fail ( new ChildAlreadyExistsError ( { id : invokeId } ) )
290+ }
291+ const logic = config . src ( )
292+ const processLogic = logic as internalRuntime . ProcessLogic < any , any , any , any , any , any >
293+ const sendParent = ( event : unknown ) : Effect . Effect < void , StoppedError > =>
294+ isCurrentInvoke ( key , token ) . pipe (
295+ Effect . flatMap ( ( isCurrent ) =>
296+ isCurrent ? context . self . send ( event as Machine . EventOf < Events > ) : Effect . void
297+ )
298+ )
299+ yield * context . spawn (
300+ {
301+ ...( processLogic [ internalRuntime . childlessProcess ] === true
302+ ? { [ internalRuntime . childlessProcess ] : true as const }
303+ : undefined ) ,
304+ ...( processLogic [ internalRuntime . compiledProcess ] === true
305+ ? { [ internalRuntime . compiledProcess ] : true as const }
306+ : undefined ) ,
307+ initial : ( childScope ) => logic . initial ( { ...childScope , sendParent } ) ,
308+ run : ( childContext ) => logic . run ( { ...childContext , sendParent } ) ,
309+ ...( processLogic . drain === undefined ? undefined : {
310+ drain : ( childContext : internalRuntime . ProcessContext < any , any > ) =>
311+ processLogic . drain ! ( { ...childContext , sendParent } )
312+ } )
313+ } ,
314+ {
315+ id : childId ,
316+ ...( config . descriptor === undefined ? undefined : { descriptor : config . descriptor } ) ,
317+ onOutcome : ( outcome ) => handleInvokeOutcome ( config , key , token , outcome ) ,
318+ ...( config . snapshot === undefined ? undefined : {
319+ [ internalRuntime . activeSnapshotObserver ] : ( snapshot ) =>
320+ handleInvokeSnapshot ( config , key , token , snapshot )
321+ } )
322+ }
323+ ) . pipe (
324+ Effect . onExit ( ( exit ) =>
325+ Exit . isFailure ( exit )
326+ ? removeInvoke ( key , token )
327+ : Effect . void
328+ )
329+ )
330+ } )
331+ startInvokes = Effect . fnUntraced ( function * (
332+ configuration : Model . ActiveConfiguration ,
333+ paths : ReadonlyArray < string > ,
334+ event : Machine . LifecycleEvent < Events >
335+ ) {
336+ yield * Effect . all (
337+ internalPlanner . sortEntryPaths ( machine , paths )
338+ . filter ( ( path ) => configuration . active . has ( path ) )
339+ . flatMap ( ( path ) =>
340+ getInvokes ( Model . getStateConfigByPath ( machine , path ) , {
341+ state : Model . getActiveValue ( configuration , path ) ,
342+ parent : Model . getParentValue ( machine , configuration , path ) ,
343+ parents : Model . getParentValues ( machine , configuration , path ) ,
344+ event
345+ } ) . map ( ( config ) =>
346+ startInvoke (
347+ path as Machine . StateIdentifier < States > ,
348+ config
349+ ) as Effect . Effect < void , E | MachineSchemaDecodeError , R >
350+ )
351+ ) ,
352+ { discard : true }
353+ )
354+ } )
355+ stopInvokes = ( paths ) =>
356+ Effect . sync ( ( ) =>
357+ internalPlanner . sortExitPaths ( machine , paths ) . flatMap ( ( path ) =>
358+ Array . from ( invokeSessions . entries ( ) )
359+ . filter ( ( [ , session ] ) => session . path === path )
360+ . map ( ( [ key ] ) => key )
361+ )
362+ ) . pipe (
363+ Effect . flatMap ( ( keys ) =>
364+ Effect . all (
365+ keys . map ( stopInvoke ) ,
366+ { discard : true , concurrency : "unbounded" }
367+ )
368+ )
369+ )
370+
371+ configuration = Model . normalizeConfigurationSync ( machine , current )
372+ if ( ! session . initialized ) {
373+ yield * startInvokes (
374+ configuration ,
375+ Model . getInitialEntryPaths ( machine , configuration ) ,
376+ internalPlanner . InitialEvent
377+ )
378+ session . initialized = true
379+ }
380+ }
381+
382+ while ( true ) {
383+ const pending = yield * Queue . poll ( mailbox )
384+ if ( Option . isNone ( pending ) ) {
385+ return Option . none < Output > ( )
386+ }
387+ const event = pending . value
388+ let planned
389+ try {
390+ planned = internalPlanner . planConfiguration (
391+ machine ,
392+ configuration ?? Model . normalizeConfigurationSync ( machine , current ) ,
393+ event
394+ )
395+ } catch ( error ) {
396+ if ( error instanceof InfiniteTransitionError || error instanceof MachineSchemaDecodeError ) {
397+ return yield * error
398+ }
399+ throw error
400+ }
401+ configuration = planned . next
402+
403+ if ( planned . microsteps . length > 0 ) {
404+ let changed = false
405+ let exitPaths : ReadonlyArray < string > = [ ]
406+ let entryEvents : Map < string , Machine . LifecycleEvent < Events > > | undefined
407+ if ( hasInvokes ) {
408+ changed = planned . microsteps . some ( ( step ) => step . changed )
409+ exitPaths = planned . microsteps . flatMap ( ( step ) => step . exitPaths )
410+ entryEvents = new Map ( )
411+ for ( const step of planned . microsteps ) {
412+ if ( step . changed ) {
413+ for ( const path of step . entryPaths ) {
414+ entryEvents . set ( path , step . event as Machine . LifecycleEvent < Events > )
415+ }
416+ }
417+ }
418+ }
419+ const next = Model . snapshotFromConfiguration < States > ( machine , planned . next )
420+ yield * internalPlanner . runCommands ( planned . commands , context )
421+ if ( changed ) {
422+ yield * stopInvokes ! ( exitPaths )
423+ }
424+ yield * setState ( next )
425+ current = next
426+ if ( planned . emittedEvents . length > 0 ) {
427+ yield * internalPlanner . runEmittedEvents (
428+ planned . emittedEvents as ReadonlyArray < Machine . EmitOf < Emits > > ,
429+ liveRuntime ??= internalPlanner . makeLiveRuntime ( machine , context )
430+ )
431+ }
432+ if ( planned . done ) {
433+ if ( stopAllInvokes !== undefined ) {
434+ yield * stopAllInvokes
435+ }
436+ return Option . some ( planned . output as Output )
437+ } else if ( changed ) {
438+ for ( const [ path , entryEvent ] of entryEvents ! ) {
439+ yield * startInvokes ! ( planned . next , [ path ] , entryEvent )
440+ }
441+ }
442+ }
443+ }
444+ } ) ,
445+ context
446+ ) ,
146447 run : ( context ) =>
147448 internalRuntime . provideMachineRuntime (
148449 Effect . gen ( function * ( ) {
@@ -344,7 +645,11 @@ const makeProcessLogic: <
344645 ? { [ internalRuntime . compiledProcess ] : true as const }
345646 : undefined ) ,
346647 initial : ( childScope ) => logic . initial ( { ...childScope , sendParent } ) ,
347- run : ( childContext ) => logic . run ( { ...childContext , sendParent } )
648+ run : ( childContext ) => logic . run ( { ...childContext , sendParent } ) ,
649+ ...( processLogic . drain === undefined ? undefined : {
650+ drain : ( childContext : internalRuntime . ProcessContext < any , any > ) =>
651+ processLogic . drain ! ( { ...childContext , sendParent } )
652+ } )
348653 } ,
349654 {
350655 id : childId ,
0 commit comments