@ -11,7 +11,8 @@ import {
RENDER _EFFECT ,
RENDER _EFFECT ,
ROOT _EFFECT ,
ROOT _EFFECT ,
MAYBE _DIRTY ,
MAYBE _DIRTY ,
DERIVED
DERIVED ,
BOUNDARY _EFFECT
} from '#client/constants' ;
} from '#client/constants' ;
import { async _mode _flag } from '../../flags/index.js' ;
import { async _mode _flag } from '../../flags/index.js' ;
import { deferred , define _property } from '../../shared/utils.js' ;
import { deferred , define _property } from '../../shared/utils.js' ;
@ -31,6 +32,16 @@ import { invoke_error_boundary } from '../error-handling.js';
import { old _values , source , update } from './sources.js' ;
import { old _values , source , update } from './sources.js' ;
import { inspect _effect , unlink _effect } from './effects.js' ;
import { inspect _effect , unlink _effect } from './effects.js' ;
/ * *
* @ typedef { {
* parent : EffectTarget | null ;
* effect : Effect | null ;
* effects : Effect [ ] ;
* render _effects : Effect [ ] ;
* block _effects : Effect [ ] ;
* } } EffectTarget
* /
/** @type {Set<Batch>} */
/** @type {Set<Batch>} */
const batches = new Set ( ) ;
const batches = new Set ( ) ;
@ -65,6 +76,8 @@ let is_flushing = false;
export let is _flushing _sync = false ;
export let is _flushing _sync = false ;
export class Batch {
export class Batch {
committed = false ;
/ * *
/ * *
* The current values of any sources that are updated in this batch
* The current values of any sources that are updated in this batch
* They keys of this map are identical to ` this.#previous `
* They keys of this map are identical to ` this.#previous `
@ -91,6 +104,11 @@ export class Batch {
* /
* /
# pending = 0 ;
# pending = 0 ;
/ * *
* The number of async effects that are currently in flight , _not _ inside a pending boundary
* /
# blocking _pending = 0 ;
/ * *
/ * *
* A deferred that resolves when the batch is committed , used with ` settled() `
* A deferred that resolves when the batch is committed , used with ` settled() `
* TODO replace with Promise . withResolvers once supported widely enough
* TODO replace with Promise . withResolvers once supported widely enough
@ -98,26 +116,6 @@ export class Batch {
* /
* /
# deferred = null ;
# deferred = null ;
/ * *
* Template effects and ` $ effect.pre ` effects , which run when
* a batch is committed
* @ type { Effect [ ] }
* /
# render _effects = [ ] ;
/ * *
* The same as ` #render_effects ` , but for ` $ effect ` ( which runs after )
* @ type { Effect [ ] }
* /
# effects = [ ] ;
/ * *
* Block effects , which may need to re - run on subsequent flushes
* in order to update internal sources ( e . g . each block items )
* @ type { Effect [ ] }
* /
# block _effects = [ ] ;
/ * *
/ * *
* Deferred effects ( which run after async work has completed ) that are DIRTY
* Deferred effects ( which run after async work has completed ) that are DIRTY
* @ type { Effect [ ] }
* @ type { Effect [ ] }
@ -148,41 +146,37 @@ export class Batch {
this . apply ( ) ;
this . apply ( ) ;
/** @type {EffectTarget} */
var target = {
parent : null ,
effect : null ,
effects : [ ] ,
render _effects : [ ] ,
block _effects : [ ]
} ;
for ( const root of root _effects ) {
for ( const root of root _effects ) {
this . # traverse _effect _tree ( root ) ;
this . # traverse _effect _tree ( root , target );
}
}
// if there is no outstanding async work, commit
this . # resolve ( ) ;
if ( this . # pending === 0 ) {
// TODO we need this because we commit _then_ flush effects...
// maybe there's a way we can reverse the order?
var previous _batch _sources = batch _values ;
this . # commit ( ) ;
var render _effects = this . # render _effects ;
if ( this . # blocking _pending > 0 ) {
var effects = this . # effects ;
this . # defer _effects ( target . effects ) ;
this . # defer _effects ( target . render _effects ) ;
this . # render_effects = [ ] ;
this . # defer _effects ( target . block _effects ) ;
this . # effects = [ ] ;
} else {
this . # block _effects = [ ] ;
// TODO append/detach blocks here, not in #commit
// If sources are written to, then work needs to happen in a separate batch, else prior sources would be mixed with
// If sources are written to, then work needs to happen in a separate batch, else prior sources would be mixed with
// newly updated sources, which could lead to infinite loops when effects run over and over again.
// newly updated sources, which could lead to infinite loops when effects run over and over again.
previous _batch = this ;
previous _batch = this ;
current _batch = null ;
current _batch = null ;
batch _values = previous _batch _sources ;
flush _queued _effects ( target . render _effects ) ;
flush _queued _effects ( render _effects ) ;
flush _queued _effects ( target . effects ) ;
flush _queued _effects ( effects ) ;
previous _batch = null ;
previous _batch = null ;
this . # deferred ? . resolve ( ) ;
} else {
this . # defer _effects ( this . # render _effects ) ;
this . # defer _effects ( this . # effects ) ;
this . # defer _effects ( this . # block _effects ) ;
}
}
batch _values = null ;
batch _values = null ;
@ -192,8 +186,9 @@ export class Batch {
* Traverse the effect tree , executing effects or stashing
* Traverse the effect tree , executing effects or stashing
* them for later execution as appropriate
* them for later execution as appropriate
* @ param { Effect } root
* @ param { Effect } root
* @ param { EffectTarget } target
* /
* /
# traverse _effect _tree ( root ) {
# traverse _effect _tree ( root , target ) {
root . f ^= CLEAN ;
root . f ^= CLEAN ;
var effect = root . first ;
var effect = root . first ;
@ -205,15 +200,25 @@ export class Batch {
var skip = is _skippable _branch || ( flags & INERT ) !== 0 || this . skipped _effects . has ( effect ) ;
var skip = is _skippable _branch || ( flags & INERT ) !== 0 || this . skipped _effects . has ( effect ) ;
if ( ( effect . f & BOUNDARY _EFFECT ) !== 0 && effect . b ? . is _pending ( ) ) {
target = {
parent : target ,
effect ,
effects : [ ] ,
render _effects : [ ] ,
block _effects : [ ]
} ;
}
if ( ! skip && effect . fn !== null ) {
if ( ! skip && effect . fn !== null ) {
if ( is _branch ) {
if ( is _branch ) {
effect . f ^= CLEAN ;
effect . f ^= CLEAN ;
} else if ( ( flags & EFFECT ) !== 0 ) {
} else if ( ( flags & EFFECT ) !== 0 ) {
this . # effects . push ( effect ) ;
target . effects . push ( effect ) ;
} else if ( async _mode _flag && ( flags & RENDER _EFFECT ) !== 0 ) {
} else if ( async _mode _flag && ( flags & RENDER _EFFECT ) !== 0 ) {
this . # render _effects . push ( effect ) ;
target . render _effects . push ( effect ) ;
} else if ( is _dirty ( effect ) ) {
} else if ( is _dirty ( effect ) ) {
if ( ( effect . f & BLOCK _EFFECT ) !== 0 ) this . # block _effects . push ( effect ) ;
if ( ( effect . f & BLOCK _EFFECT ) !== 0 ) target . block _effects . push ( effect ) ;
update _effect ( effect ) ;
update _effect ( effect ) ;
}
}
@ -229,6 +234,17 @@ export class Batch {
effect = effect . next ;
effect = effect . next ;
while ( effect === null && parent !== null ) {
while ( effect === null && parent !== null ) {
if ( parent === target . effect ) {
// TODO rather than traversing into pending boundaries and deferring the effects,
// could we just attach the effects _to_ the pending boundary and schedule them
// once the boundary is ready?
this . # defer _effects ( target . effects ) ;
this . # defer _effects ( target . render _effects ) ;
this . # defer _effects ( target . block _effects ) ;
target = /** @type {EffectTarget} */ ( target . parent ) ;
}
effect = parent . next ;
effect = parent . next ;
parent = parent . parent ;
parent = parent . parent ;
}
}
@ -246,8 +262,6 @@ export class Batch {
// mark as clean so they get scheduled if they depend on pending async state
// mark as clean so they get scheduled if they depend on pending async state
set _signal _status ( e , CLEAN ) ;
set _signal _status ( e , CLEAN ) ;
}
}
effects . length = 0 ;
}
}
/ * *
/ * *
@ -283,8 +297,8 @@ export class Batch {
// this can happen if a new batch was created during `flush_effects()`
// this can happen if a new batch was created during `flush_effects()`
return ;
return ;
}
}
} else if ( this . # pending === 0 ) {
} else {
this . # commit ( ) ;
this . # resolve ( ) ;
}
}
this . deactivate ( ) ;
this . deactivate ( ) ;
@ -300,16 +314,19 @@ export class Batch {
}
}
}
}
/ * *
# resolve ( ) {
* Append and remove branches to / from the DOM
if ( this . # blocking _pending === 0 ) {
* /
// append/remove branches
# commit ( ) {
for ( const fn of this . # callbacks ) fn ( ) ;
for ( const fn of this . # callbacks ) {
this . # callbacks . clear ( ) ;
fn ( ) ;
}
}
this . # callbacks . clear ( ) ;
if ( this . # pending === 0 ) {
this . # commit ( ) ;
}
}
# commit ( ) {
// If there are other pending batches, they now need to be 'rebased' —
// If there are other pending batches, they now need to be 'rebased' —
// in other words, we re-run block/async effects with the newly
// in other words, we re-run block/async effects with the newly
// committed state, unless the batch in question has a more
// committed state, unless the batch in question has a more
@ -317,7 +334,17 @@ export class Batch {
if ( batches . size > 1 ) {
if ( batches . size > 1 ) {
this . # previous . clear ( ) ;
this . # previous . clear ( ) ;
let is _earlier = true ;
var previous _batch _values = batch _values ;
var is _earlier = true ;
/** @type {EffectTarget} */
var dummy _target = {
parent : null ,
effect : null ,
effects : [ ] ,
render _effects : [ ] ,
block _effects : [ ]
} ;
for ( const batch of batches ) {
for ( const batch of batches ) {
if ( batch === this ) {
if ( batch === this ) {
@ -350,8 +377,12 @@ export class Batch {
// Re-run async/block effects that depend on distinct values changed in both batches
// Re-run async/block effects that depend on distinct values changed in both batches
const others = [ ... batch . current . keys ( ) ] . filter ( ( s ) => ! this . current . has ( s ) ) ;
const others = [ ... batch . current . keys ( ) ] . filter ( ( s ) => ! this . current . has ( s ) ) ;
if ( others . length > 0 ) {
if ( others . length > 0 ) {
/** @type {Set<Value>} */
const marked = new Set ( ) ;
/** @type {Map<Reaction, boolean>} */
const checked = new Map ( ) ;
for ( const source of sources ) {
for ( const source of sources ) {
mark _effects ( source , others ) ;
mark _effects ( source , others , marked , checked );
}
}
if ( queued _root _effects . length > 0 ) {
if ( queued _root _effects . length > 0 ) {
@ -359,9 +390,11 @@ export class Batch {
batch . apply ( ) ;
batch . apply ( ) ;
for ( const root of queued _root _effects ) {
for ( const root of queued _root _effects ) {
batch . # traverse _effect _tree ( root );
batch . # traverse _effect _tree ( root , dummy _target );
}
}
// TODO do we need to do anything with `target`? defer block effects?
queued _root _effects = [ ] ;
queued _root _effects = [ ] ;
batch . deactivate ( ) ;
batch . deactivate ( ) ;
}
}
@ -369,17 +402,31 @@ export class Batch {
}
}
current _batch = null ;
current _batch = null ;
batch _values = previous _batch _values ;
}
}
this . committed = true ;
batches . delete ( this ) ;
batches . delete ( this ) ;
this . # deferred ? . resolve ( ) ;
}
}
increment ( ) {
/ * *
*
* @ param { boolean } blocking
* /
increment ( blocking ) {
this . # pending += 1 ;
this . # pending += 1 ;
if ( blocking ) this . # blocking _pending += 1 ;
}
}
decrement ( ) {
/ * *
*
* @ param { boolean } blocking
* /
decrement ( blocking ) {
this . # pending -= 1 ;
this . # pending -= 1 ;
if ( blocking ) this . # blocking _pending -= 1 ;
for ( const e of this . # dirty _effects ) {
for ( const e of this . # dirty _effects ) {
set _signal _status ( e , DIRTY ) ;
set _signal _status ( e , DIRTY ) ;
@ -391,6 +438,9 @@ export class Batch {
schedule _effect ( e ) ;
schedule _effect ( e ) ;
}
}
this . # dirty _effects = [ ] ;
this . # maybe _dirty _effects = [ ] ;
this . flush ( ) ;
this . flush ( ) ;
}
}
@ -561,7 +611,7 @@ function infinite_loop_guard() {
}
}
}
}
/** @type { Effect[] | null} */
/** @type { Set<Effect> | null} */
export let eager _block _effects = null ;
export let eager _block _effects = null ;
/ * *
/ * *
@ -578,7 +628,7 @@ function flush_queued_effects(effects) {
var effect = effects [ i ++ ] ;
var effect = effects [ i ++ ] ;
if ( ( effect . f & ( DESTROYED | INERT ) ) === 0 && is _dirty ( effect ) ) {
if ( ( effect . f & ( DESTROYED | INERT ) ) === 0 && is _dirty ( effect ) ) {
eager _block _effects = [ ] ;
eager _block _effects = new Set ( ) ;
update _effect ( effect ) ;
update _effect ( effect ) ;
@ -601,15 +651,34 @@ function flush_queued_effects(effects) {
// If update_effect() has a flushSync() in it, we may have flushed another flush_queued_effects(),
// If update_effect() has a flushSync() in it, we may have flushed another flush_queued_effects(),
// which already handled this logic and did set eager_block_effects to null.
// which already handled this logic and did set eager_block_effects to null.
if ( eager _block _effects ? . length > 0 ) {
if ( eager _block _effects ? . size > 0 ) {
// TODO this feels incorrect! it gets the tests passing
old _values . clear ( ) ;
old _values . clear ( ) ;
for ( const e of eager _block _effects ) {
for ( const e of eager _block _effects ) {
update _effect ( e ) ;
// Skip eager effects that have already been unmounted
if ( ( e . f & ( DESTROYED | INERT ) ) !== 0 ) continue ;
// Run effects in order from ancestor to descendant, else we could run into nullpointers
/** @type {Effect[]} */
const ordered _effects = [ e ] ;
let ancestor = e . parent ;
while ( ancestor !== null ) {
if ( eager _block _effects . has ( ancestor ) ) {
eager _block _effects . delete ( ancestor ) ;
ordered _effects . push ( ancestor ) ;
}
ancestor = ancestor . parent ;
}
for ( let j = ordered _effects . length - 1 ; j >= 0 ; j -- ) {
const e = ordered _effects [ j ] ;
// Skip eager effects that have already been unmounted
if ( ( e . f & ( DESTROYED | INERT ) ) !== 0 ) continue ;
update _effect ( e ) ;
}
}
}
eager _block _effects = [ ] ;
eager _block _effects . clear ( ) ;
}
}
}
}
}
}
@ -623,15 +692,24 @@ function flush_queued_effects(effects) {
* these effects can re - run after another batch has been committed
* these effects can re - run after another batch has been committed
* @ param { Value } value
* @ param { Value } value
* @ param { Source [ ] } sources
* @ param { Source [ ] } sources
* @ param { Set < Value > } marked
* @ param { Map < Reaction , boolean > } checked
* /
* /
function mark _effects ( value , sources ) {
function mark _effects ( value , sources , marked , checked ) {
if ( marked . has ( value ) ) return ;
marked . add ( value ) ;
if ( value . reactions !== null ) {
if ( value . reactions !== null ) {
for ( const reaction of value . reactions ) {
for ( const reaction of value . reactions ) {
const flags = reaction . f ;
const flags = reaction . f ;
if ( ( flags & DERIVED ) !== 0 ) {
if ( ( flags & DERIVED ) !== 0 ) {
mark _effects ( /** @type {Derived} */ ( reaction ) , sources ) ;
mark _effects ( /** @type {Derived} */ ( reaction ) , sources , marked , checked ) ;
} else if ( ( flags & ( ASYNC | BLOCK _EFFECT ) ) !== 0 && depends _on ( reaction , sources ) ) {
} else if (
( flags & ( ASYNC | BLOCK _EFFECT ) ) !== 0 &&
( flags & DIRTY ) === 0 && // we may have scheduled this one already
depends _on ( reaction , sources , checked )
) {
set _signal _status ( reaction , DIRTY ) ;
set _signal _status ( reaction , DIRTY ) ;
schedule _effect ( /** @type {Effect} */ ( reaction ) ) ;
schedule _effect ( /** @type {Effect} */ ( reaction ) ) ;
}
}
@ -642,20 +720,27 @@ function mark_effects(value, sources) {
/ * *
/ * *
* @ param { Reaction } reaction
* @ param { Reaction } reaction
* @ param { Source [ ] } sources
* @ param { Source [ ] } sources
* @ param { Map < Reaction , boolean > } checked
* /
* /
function depends _on ( reaction , sources ) {
function depends _on ( reaction , sources , checked ) {
const depends = checked . get ( reaction ) ;
if ( depends !== undefined ) return depends ;
if ( reaction . deps !== null ) {
if ( reaction . deps !== null ) {
for ( const dep of reaction . deps ) {
for ( const dep of reaction . deps ) {
if ( sources . includes ( dep ) ) {
if ( sources . includes ( dep ) ) {
return true ;
return true ;
}
}
if ( ( dep . f & DERIVED ) !== 0 && depends _on ( /** @type {Derived} */ ( dep ) , sources ) ) {
if ( ( dep . f & DERIVED ) !== 0 && depends _on ( /** @type {Derived} */ ( dep ) , sources , checked ) ) {
checked . set ( /** @type {Derived} */ ( dep ) , true ) ;
return true ;
return true ;
}
}
}
}
}
}
checked . set ( reaction , false ) ;
return false ;
return false ;
}
}