@ -41,8 +41,11 @@ import { legacy_is_updating_store } from './store.js';
import { invariant } from '../../shared/dev.js' ;
import { invariant } from '../../shared/dev.js' ;
import { log _effect _tree } from '../dev/debug.js' ;
import { log _effect _tree } from '../dev/debug.js' ;
/** @type {Set<Batch>} */
/** @type {Batch | null} */
const batches = new Set ( ) ;
let first _batch = null ;
/** @type {Batch | null} */
let last _batch = null ;
/** @type {Batch | null} */
/** @type {Batch | null} */
export let current _batch = null ;
export let current _batch = null ;
@ -94,9 +97,20 @@ let uid = 1;
export class Batch {
export class Batch {
id = uid ++ ;
id = uid ++ ;
/** True as soon as `#process () ` was called */
/** True as soon as `#process ` was called */
# started = false ;
# started = false ;
linked = true ;
/** @type {Batch | null} */
# prev = null ;
/** @type {Batch | null} */
# next = null ;
/** @type {Map<Effect, ReturnType<typeof deferred<any>>>} */
async _deriveds = new Map ( ) ;
/ * *
/ * *
* The current values of any signals that are updated in this batch .
* The current values of any signals that are updated in this batch .
* Tuple format : [ value , is _derived ] ( note : is _derived is false for deriveds , too , if they were overridden via assignment )
* Tuple format : [ value , is _derived ] ( note : is _derived is false for deriveds , too , if they were overridden via assignment )
@ -199,33 +213,24 @@ export class Batch {
# decrement _queued = false ;
# decrement _queued = false ;
/** @type {Set<Batch>} */
# blockers = new Set ( ) ;
# is _deferred ( ) {
# is _deferred ( ) {
return this . is _fork || this . # blocking _pending . size > 0 ;
if ( this . is _fork ) return true ;
}
# is _blocked ( ) {
for ( const batch of this . # blockers ) {
for ( const effect of batch . # blocking _pending . keys ( ) ) {
if ( this . unblocked . has ( effect ) ) continue ;
var skipped = false ;
for ( const effect of this . # blocking _pending . keys ( ) ) {
var e = effect ;
var e = effect ;
var skipped = false ;
while ( e . parent !== null ) {
while ( e . parent !== null ) {
if ( this . # skipped _branches . has ( e ) ) {
if ( this . # skipped _branches . has ( e ) ) {
skipped = true ;
skipped = true ;
break ;
break ;
}
e = e . parent ;
}
}
if ( ! skipped ) {
e = e . parent ;
return true ;
}
}
if ( ! skipped ) {
return true ;
}
}
}
}
@ -271,7 +276,7 @@ export class Batch {
this . # started = true ;
this . # started = true ;
if ( flush _count ++ > 1000 ) {
if ( flush _count ++ > 1000 ) {
batches . delete ( this ) ;
this . # unlink ( ) ;
infinite _loop _guard ( ) ;
infinite _loop _guard ( ) ;
}
}
@ -337,7 +342,8 @@ export class Batch {
collected _effects = null ;
collected _effects = null ;
legacy _updates = null ;
legacy _updates = null ;
if ( this . # is _deferred ( ) || this . # is _blocked ( ) ) {
// if the batch has outstanding pending work, stash effects and bail
if ( this . # is _deferred ( ) ) {
this . # defer _effects ( render _effects ) ;
this . # defer _effects ( render _effects ) ;
this . # defer _effects ( effects ) ;
this . # defer _effects ( effects ) ;
@ -352,6 +358,13 @@ export class Batch {
return ;
return ;
}
}
const earlier _batch = this . # find _earlier _batch ( ) ;
if ( earlier _batch ) {
earlier _batch . # merge ( this ) ;
return ;
}
// clear effects. Those that are still needed will be rescheduled through unskipping the skipped branches.
// clear effects. Those that are still needed will be rescheduled through unskipping the skipped branches.
this . # dirty _effects . clear ( ) ;
this . # dirty _effects . clear ( ) ;
this . # maybe _dirty _effects . clear ( ) ;
this . # maybe _dirty _effects . clear ( ) ;
@ -369,11 +382,15 @@ export class Batch {
var next _batch = /** @type {Batch | null} */ ( /** @type {unknown} */ ( current _batch ) ) ;
var next _batch = /** @type {Batch | null} */ ( /** @type {unknown} */ ( current _batch ) ) ;
if ( this . linked && this . # pending === 0 ) {
this . # unlink ( ) ;
}
// Order matters here - we need to commit and THEN continue flushing new batches, not the other way around,
// Order matters here - we need to commit and THEN continue flushing new batches, not the other way around,
// else we could start flushing a new batch and then, if it has pending work, rebase it right afterwards, which is wrong.
// else we could start flushing a new batch and then, if it has pending work, rebase it right afterwards, which is wrong.
// In sync mode flushSync can cause #commit to wrongfully think that there needs to be a rebase, so we only do it in async mode
// In sync mode flushSync can cause #commit to wrongfully think that there needs to be a rebase, so we only do it in async mode
// TODO fix the underlying cause, otherwise this will likely regress when non-async mode is removed
// TODO fix the underlying cause, otherwise this will likely regress when non-async mode is removed
if ( async _mode _flag && this . # pending === 0 ) {
if ( async _mode _flag && ! this . linked ) {
this . # commit ( ) ;
this . # commit ( ) ;
// Rebases can activate other batches or null it out, therefore restore the new one here
// Rebases can activate other batches or null it out, therefore restore the new one here
current _batch = next _batch ;
current _batch = next _batch ;
@ -383,8 +400,12 @@ export class Batch {
// causing an effect and therefore a root to be scheduled again. We need to traverse the current batch
// causing an effect and therefore a root to be scheduled again. We need to traverse the current batch
// once more in that case - most of the time this will just clean up dirty branches.
// once more in that case - most of the time this will just clean up dirty branches.
if ( this . # roots . length > 0 ) {
if ( this . # roots . length > 0 ) {
const batch = ( next _batch ? ? = this ) ;
if ( next _batch === null ) {
batches . add ( batch ) ;
next _batch = this ;
this . # link ( ) ;
}
const batch = next _batch ;
batch . # roots . push ( ... this . # roots . filter ( ( r ) => ! batch . # roots . includes ( r ) ) ) ;
batch . # roots . push ( ... this . # roots . filter ( ( r ) => ! batch . # roots . includes ( r ) ) ) ;
}
}
@ -445,6 +466,82 @@ export class Batch {
}
}
}
}
# find _earlier _batch ( ) {
var batch = this . # prev ;
while ( batch !== null ) {
if ( ! batch . is _fork ) {
// if the batches are connected, break
for ( const [ value , [ , is _derived ] ] of this . current ) {
if ( batch . current . has ( value ) && ! is _derived ) {
return batch ;
}
}
}
batch = batch . # prev ;
}
return null ;
}
/ * *
* @ param { Batch } batch
* /
# merge ( batch ) {
for ( const [ source , value ] of batch . current ) {
if ( ! this . previous . has ( source ) && batch . previous . has ( source ) ) {
this . previous . set ( source , batch . previous . get ( source ) ) ;
}
this . current . set ( source , value ) ;
}
for ( const [ effect , deferred ] of batch . async _deriveds ) {
const d = this . async _deriveds . get ( effect ) ;
if ( d ) deferred . promise . then ( d . resolve ) ;
}
/ * *
* mark all effects that depend on ` batch.current ` , except the
* async effects that we just resolved ( TODO unless they depend
* on values in this batch that are NOT in the later batch ? ) .
* Through this we also will populate the correct # skipped _branches ,
* oncommit callbacks etc , so we don ' t need to merge them separately .
* @ param { Value } value
* /
const mark = ( value ) => {
var reactions = value . reactions ;
if ( reactions === null ) return ;
for ( const reaction of reactions ) {
var flags = reaction . f ;
if ( ( flags & DERIVED ) !== 0 ) {
mark ( /** @type {Derived} */ ( reaction ) ) ;
} else {
var effect = /** @type {Effect} */ ( reaction ) ;
if ( flags & ( ASYNC | BLOCK _EFFECT ) && ! this . async _deriveds . has ( effect ) ) {
this . # maybe _dirty _effects . delete ( effect ) ;
set _signal _status ( effect , DIRTY ) ;
this . schedule ( effect ) ;
}
}
}
} ;
for ( const source of this . current . keys ( ) ) {
mark ( source ) ;
}
this . oncommit ( ( ) => batch . discard ( ) ) ;
batch . # unlink ( ) ;
current _batch = this ;
this . # process ( ) ;
}
/ * *
/ * *
* @ param { Effect [ ] } effects
* @ param { Effect [ ] } effects
* /
* /
@ -521,7 +618,7 @@ export class Batch {
this . # discard _callbacks . clear ( ) ;
this . # discard _callbacks . clear ( ) ;
this . # fork _commit _callbacks . clear ( ) ;
this . # fork _commit _callbacks . clear ( ) ;
batches . delete ( this ) ;
this . # unlink ( ) ;
}
}
/ * *
/ * *
@ -532,13 +629,13 @@ export class Batch {
}
}
# commit ( ) {
# commit ( ) {
batches . delete ( this ) ;
this . # unlink ( ) ;
// 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
// recent value for a given source
// recent value for a given source
for ( const batch of batches ) {
for ( let batch = first _batch ; batch !== null ; batch = batch . # next ) {
var is _earlier = batch . id < this . id ;
var is _earlier = batch . id < this . id ;
/** @type {Source[]} */
/** @type {Source[]} */
@ -561,6 +658,15 @@ export class Batch {
sources . push ( source ) ;
sources . push ( source ) ;
}
}
if ( is _earlier ) {
// TODO do we need to restart these in some cases, instead of
// immediately resolving them? Likely not because of how this.apply() works.
for ( const [ effect , deferred ] of this . async _deriveds ) {
const d = batch . async _deriveds . get ( effect ) ;
if ( d ) deferred . promise . then ( d . resolve ) ;
}
}
if ( ! batch . # started ) continue ;
if ( ! batch . # started ) continue ;
// 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
@ -638,17 +744,6 @@ export class Batch {
batch . deactivate ( ) ;
batch . deactivate ( ) ;
}
}
}
}
for ( const batch of batches ) {
if ( batch . # blockers . has ( this ) ) {
batch . # blockers . delete ( this ) ;
if ( batch . # blockers . size === 0 && ! batch . # is _deferred ( ) ) {
batch . activate ( ) ;
batch . # process ( ) ;
}
}
}
}
}
/ * *
/ * *
@ -687,7 +782,7 @@ export class Batch {
queue _micro _task ( ( ) => {
queue _micro _task ( ( ) => {
this . # decrement _queued = false ;
this . # decrement _queued = false ;
if ( batches . has ( this ) ) {
if ( this . linked ) {
this . flush ( ) ;
this . flush ( ) ;
}
}
} ) ;
} ) ;
@ -737,7 +832,7 @@ export class Batch {
static ensure ( ) {
static ensure ( ) {
if ( current _batch === null ) {
if ( current _batch === null ) {
const batch = ( current _batch = new Batch ( ) ) ;
const batch = ( current _batch = new Batch ( ) ) ;
batch es. add ( batch ) ;
batch . # link ( ) ;
if ( ! is _processing && ! is _flushing _sync ) {
if ( ! is _processing && ! is _flushing _sync ) {
queue _micro _task ( ( ) => {
queue _micro _task ( ( ) => {
@ -752,7 +847,7 @@ export class Batch {
}
}
apply ( ) {
apply ( ) {
if ( ! async _mode _flag || ( ! this . is _fork && batches . size === 1 ) ) {
if ( ! async _mode _flag || ( ! this . is _fork && this . # prev === null && this . # next === null ) ) {
batch _values = null ;
batch _values = null ;
return ;
return ;
}
}
@ -764,28 +859,33 @@ export class Batch {
batch _values . set ( source , value ) ;
batch _values . set ( source , value ) ;
}
}
// ...and undo changes belonging to other batches unless they block this one
// ...and undo changes belonging to other batches unless they intersect
for ( const batch of batches ) {
for ( let batch = first _batch ; batch !== null ; batch = batch . # next ) {
if ( batch === this || batch . is _fork ) continue ;
if ( batch === this || batch . is _fork ) continue ;
// A batch is blocked on an earlier batch if it overlaps with the earlier batch's changes but is not a superset
// If two batches intersect, the latter batch will be merged into the earlier batch,
// and we should treat them as a single set of changes
var intersects = false ;
var intersects = false ;
var differs = false ;
if ( batch . id < this . id ) {
if ( batch . id < this . id ) {
for ( const [ source , [ , is _derived ] ] of batch . current ) {
for ( const [ source , [ , is _derived ] ] of batch . current ) {
// Derived values don't partake in the blocking mechanism, because a derived could
// Derived values don't partake in the intersection mechanism, because a derived could
// be triggered in one batch already but not the other one yet, causing a false-positive
// be triggered in one batch already but not the other one yet, causing a false-positive
if ( is _derived ) continue ;
if ( is _derived ) continue ;
intersects || = this . current . has ( source ) ;
if ( this . current . has ( source ) ) {
differs || = ! this . current . has ( source ) ;
intersects = true ;
break ;
}
}
}
}
}
if ( intersects && differs ) {
// Since the latter batch merges into the earlier (if it resolves before the earlier one),
this . # blockers . add ( batch ) ;
// we treat the earlier values as "already applied". This way we don't need to rerun async
} else {
// effects of the earlier batch in case they are merged.
// As a result you can think of batch_values as having the latest values of all intersecting
// batches up until this batch.
if ( ! intersects ) {
for ( const [ source , previous ] of batch . previous ) {
for ( const [ source , previous ] of batch . previous ) {
if ( ! batch _values . has ( source ) ) {
if ( ! batch _values . has ( source ) ) {
batch _values . set ( source , previous ) ;
batch _values . set ( source , previous ) ;
@ -851,6 +951,36 @@ export class Batch {
this . # roots . push ( e ) ;
this . # roots . push ( e ) ;
}
}
# link ( ) {
if ( last _batch === null ) {
first _batch = last _batch = this ;
} else {
last _batch . # next = this ;
this . # prev = last _batch ;
}
last _batch = this ;
}
# unlink ( ) {
var prev = this . # prev ;
var next = this . # next ;
if ( prev === null ) {
first _batch = next ;
} else {
prev . # next = next ;
}
if ( next === null ) {
last _batch = prev ;
} else {
next . # prev = prev ;
}
this . linked = false ;
}
}
}
// TODO Svelte@6 think about removing the callback argument.
// TODO Svelte@6 think about removing the callback argument.
@ -1234,7 +1364,7 @@ export function fork(fn) {
return ;
return ;
}
}
if ( ! batch es. has ( batch ) ) {
if ( ! batch . linked ) {
e . fork _discarded ( ) ;
e . fork _discarded ( ) ;
}
}
@ -1280,7 +1410,7 @@ export function fork(fn) {
source . wv = increment _write _version ( ) ;
source . wv = increment _write _version ( ) ;
}
}
if ( ! committed && batch es. has ( batch ) ) {
if ( ! committed && batch . linked ) {
batch . discard ( ) ;
batch . discard ( ) ;
}
}
}
}
@ -1291,5 +1421,5 @@ export function fork(fn) {
* Forcibly remove all current batches , to prevent cross - talk between tests
* Forcibly remove all current batches , to prevent cross - talk between tests
* /
* /
export function clear ( ) {
export function clear ( ) {
batches. clear ( ) ;
first_batch = last _batch = null ;
}
}