@@ -54,17 +54,50 @@ export const compiledProcess: unique symbol = Symbol.for("effect/Machine/compile
5454/** @internal */
5555export const sendParentOverride : unique symbol = Symbol . for ( "effect/Machine/sendParentOverride" )
5656
57- interface ChildRegistrySnapshot {
58- readonly closed : boolean
59- readonly revision : number
60- readonly children : ReadonlyMap < ChildKey , ChildEntry >
57+ type ChildObservation = Option . Option < MachineRef < any , any , any , any > >
58+ type ChildObservationBatch = [ ChildObservation , ...Array < ChildObservation > ]
59+
60+ interface ChildObserver {
61+ readonly child : ChildSelector
62+ readonly id : string
63+ values : ChildObservationBatch | undefined
64+ waiter : Deferred . Deferred < void > | undefined
65+ }
66+
67+ const offerChildObservation = (
68+ observer : ChildObserver ,
69+ value : ChildObservation
70+ ) : void => {
71+ if ( observer . values === undefined ) {
72+ observer . values = [ value ]
73+ } else {
74+ observer . values . push ( value )
75+ }
76+ if ( observer . waiter !== undefined ) {
77+ const waiter = observer . waiter
78+ observer . waiter = undefined
79+ Deferred . doneUnsafe ( waiter , Effect . void )
80+ }
6181}
6282
83+ const takeChildObservations = (
84+ observer : ChildObserver
85+ ) : Effect . Effect < ChildObservationBatch > =>
86+ Effect . suspend ( ( ) => {
87+ if ( observer . values !== undefined ) {
88+ const values = observer . values
89+ observer . values = undefined
90+ return Effect . succeed ( values )
91+ }
92+ const waiter = Deferred . makeUnsafe < void > ( )
93+ observer . waiter = waiter
94+ return Deferred . await ( waiter ) . pipe ( Effect . andThen ( takeChildObservations ( observer ) ) )
95+ } )
96+
6397interface ChildRegistry {
6498 closed : boolean
65- revision : number
6699 readonly children : Map < ChildKey , ChildEntry >
67- changes : PubSub . PubSub < ChildRegistrySnapshot > | undefined
100+ observers : Set < ChildObserver > | undefined
68101 scope : Scope . Closeable | undefined
69102}
70103
@@ -395,24 +428,44 @@ const makeChildRuntime = (
395428 Effect . sync ( ( ) => {
396429 // Child-registry decisions are synchronous and every access below runs in
397430 // one Effect.sync / Effect.suspend step. Keep the unobserved representation
398- // compact; a replay PubSub is installed only when childChanges is used.
431+ // compact; selector-specific handoffs are installed only while
432+ // childChanges streams are running.
399433 const registry : ChildRegistry = {
400434 closed : false ,
401- revision : 0 ,
402435 children : new Map ( ) ,
403- changes : undefined ,
436+ observers : undefined ,
404437 scope : undefined
405438 }
406439
407- const snapshot = ( registry : ChildRegistry ) : ChildRegistrySnapshot => ( {
408- closed : registry . closed ,
409- revision : registry . revision ,
410- children : new Map ( registry . children )
411- } )
440+ const matches = (
441+ entry : ChildEntry ,
442+ child : ChildSelector
443+ ) : entry is Extract < ChildEntry , { readonly _tag : "Started" } > =>
444+ entry . _tag === "Started" && ( typeof child === "string" || (
445+ entry . descriptor !== undefined &&
446+ entry . descriptor . id === child . id &&
447+ entry . descriptor . machine === child . machine
448+ ) )
449+
450+ const selectChild = (
451+ id : string ,
452+ child : ChildSelector
453+ ) : ChildObservation => {
454+ if ( registry . closed ) {
455+ return Option . none ( )
456+ }
457+ const entry = registry . children . get ( id )
458+ return entry !== undefined && matches ( entry , child )
459+ ? Option . some ( entry . ref )
460+ : Option . none ( )
461+ }
412462
413463 const publishRegistryChange = ( ) : void => {
414- if ( registry . changes !== undefined ) {
415- PubSub . publishUnsafe ( registry . changes , snapshot ( registry ) )
464+ if ( registry . observers === undefined ) {
465+ return
466+ }
467+ for ( const observer of registry . observers ) {
468+ offerChildObservation ( observer , selectChild ( observer . id , observer . child ) )
416469 }
417470 }
418471
@@ -478,9 +531,6 @@ const makeChildRuntime = (
478531 return
479532 }
480533 const observable = typeof key === "string"
481- if ( observable ) {
482- registry . revision += 1
483- }
484534 registry . children . delete ( key )
485535 if ( observable ) {
486536 publishRegistryChange ( )
@@ -504,9 +554,6 @@ const makeChildRuntime = (
504554 return false
505555 }
506556 const observable = typeof key === "string"
507- if ( observable ) {
508- registry . revision += 1
509- }
510557 registry . children . delete ( key )
511558 registry . children . set ( key , { _tag : "Started" , token, descriptor, ref } )
512559 if ( observable ) {
@@ -515,16 +562,6 @@ const makeChildRuntime = (
515562 return true
516563 } )
517564
518- const matches = (
519- entry : ChildEntry ,
520- child : ChildSelector
521- ) : entry is Extract < ChildEntry , { readonly _tag : "Started" } > =>
522- entry . _tag === "Started" && ( typeof child === "string" || (
523- entry . descriptor !== undefined &&
524- entry . descriptor . id === child . id &&
525- entry . descriptor . machine === child . machine
526- ) )
527-
528565 const get : ChildRuntime [ "get" ] = ( child ) => {
529566 const id = typeof child === "string" ? child : child . id
530567 return Effect . sync ( ( ) => {
@@ -540,51 +577,34 @@ const makeChildRuntime = (
540577
541578 const changes : ChildRuntime [ "changes" ] = ( child ) => {
542579 const id = typeof child === "string" ? child : child . id
543- return Stream . unwrap (
544- Effect . suspend ( ( ) => {
545- if ( registry . closed ) {
546- return Effect . succeed ( undefined )
547- }
548- if ( registry . changes !== undefined ) {
549- return Effect . succeed ( registry . changes )
550- }
551- return PubSub . unbounded < ChildRegistrySnapshot > ( { replay : 1 } ) . pipe (
552- Effect . flatMap ( ( candidate ) =>
553- Effect . sync ( ( ) => {
554- if ( registry . closed ) {
555- return [ undefined , true ] as const
556- }
557- if ( registry . changes !== undefined ) {
558- return [ registry . changes , true ] as const
580+ return Stream . fromChannel (
581+ Channel . fromTransform ( ( _ , streamScope ) =>
582+ Effect . sync ( ( ) : ChildObserver => ( { child, id, values : undefined , waiter : undefined } ) ) . pipe (
583+ Effect . flatMap ( ( observer ) => {
584+ const removeObserver = Effect . sync ( ( ) => {
585+ if ( registry . observers !== undefined ) {
586+ registry . observers . delete ( observer )
587+ if ( registry . observers . size === 0 ) {
588+ registry . observers = undefined
589+ }
559590 }
560- registry . changes = candidate
561- PubSub . publishUnsafe ( candidate , snapshot ( registry ) )
562- return [ candidate , false ] as const
563- } ) . pipe (
564- Effect . flatMap ( ( [ changes , discardCandidate ] ) =>
565- discardCandidate
566- ? PubSub . shutdown ( candidate ) . pipe ( Effect . as ( changes ) )
567- : Effect . succeed ( changes )
568- )
591+ observer . values = undefined
592+ observer . waiter = undefined
593+ } )
594+ return Scope . addFinalizer ( streamScope , removeObserver ) . pipe (
595+ Effect . andThen (
596+ Effect . sync ( ( ) => {
597+ if ( ! registry . closed && streamScope . state . _tag !== "Closed" ) {
598+ registry . observers ??= new Set ( )
599+ registry . observers . add ( observer )
600+ }
601+ offerChildObservation ( observer , selectChild ( id , child ) )
602+ } )
603+ ) ,
604+ Effect . as ( takeChildObservations ( observer ) )
569605 )
570- )
606+ } )
571607 )
572- } ) . pipe (
573- Effect . flatMap ( ( changes ) => {
574- if ( changes === undefined ) {
575- return Effect . succeed ( noChildChanges )
576- }
577- const select = ( registry : ChildRegistrySnapshot ) => {
578- if ( registry . closed ) {
579- return Option . none ( )
580- }
581- const entry = registry . children . get ( id )
582- return entry !== undefined && matches ( entry , child )
583- ? Option . some ( entry . ref )
584- : Option . none ( )
585- }
586- return Effect . succeed ( Stream . fromPubSub ( changes ) . pipe ( Stream . map ( select ) ) )
587- } )
588608 )
589609 )
590610 }
0 commit comments