@@ -11,7 +11,8 @@ import {
1111 RENDER_EFFECT ,
1212 ROOT_EFFECT ,
1313 MAYBE_DIRTY ,
14- DERIVED
14+ DERIVED ,
15+ BOUNDARY_EFFECT
1516} from '#client/constants' ;
1617import { async_mode_flag } from '../../flags/index.js' ;
1718import { deferred , define_property } from '../../shared/utils.js' ;
@@ -31,6 +32,16 @@ import { invoke_error_boundary } from '../error-handling.js';
3132import { old_values , source , update } from './sources.js' ;
3233import { inspect_effect , unlink_effect } from './effects.js' ;
3334
35+ /**
36+ * @typedef {{
37+ * parent: EffectTarget | null;
38+ * effect: Effect | null;
39+ * effects: Effect[];
40+ * render_effects: Effect[];
41+ * block_effects: Effect[];
42+ * }} EffectTarget
43+ */
44+
3445/** @type {Set<Batch> } */
3546const batches = new Set ( ) ;
3647
@@ -65,6 +76,8 @@ let is_flushing = false;
6576export let is_flushing_sync = false ;
6677
6778export class Batch {
79+ committed = false ;
80+
6881 /**
6982 * The current values of any sources that are updated in this batch
7083 * They keys of this map are identical to `this.#previous`
@@ -91,33 +104,18 @@ export class Batch {
91104 */
92105 #pending = 0 ;
93106
107+ /**
108+ * The number of async effects that are currently in flight, _not_ inside a pending boundary
109+ */
110+ #blocking_pending = 0 ;
111+
94112 /**
95113 * A deferred that resolves when the batch is committed, used with `settled()`
96114 * TODO replace with Promise.withResolvers once supported widely enough
97115 * @type {{ promise: Promise<void>, resolve: (value?: any) => void, reject: (reason: unknown) => void } | null }
98116 */
99117 #deferred = null ;
100118
101- /**
102- * Template effects and `$effect.pre` effects, which run when
103- * a batch is committed
104- * @type {Effect[] }
105- */
106- #render_effects = [ ] ;
107-
108- /**
109- * The same as `#render_effects`, but for `$effect` (which runs after)
110- * @type {Effect[] }
111- */
112- #effects = [ ] ;
113-
114- /**
115- * Block effects, which may need to re-run on subsequent flushes
116- * in order to update internal sources (e.g. each block items)
117- * @type {Effect[] }
118- */
119- #block_effects = [ ] ;
120-
121119 /**
122120 * Deferred effects (which run after async work has completed) that are DIRTY
123121 * @type {Effect[] }
@@ -148,41 +146,37 @@ export class Batch {
148146
149147 this . apply ( ) ;
150148
149+ /** @type {EffectTarget } */
150+ var target = {
151+ parent : null ,
152+ effect : null ,
153+ effects : [ ] ,
154+ render_effects : [ ] ,
155+ block_effects : [ ]
156+ } ;
157+
151158 for ( const root of root_effects ) {
152- this . #traverse_effect_tree( root ) ;
159+ this . #traverse_effect_tree( root , target ) ;
153160 }
154161
155- // if there is no outstanding async work, commit
156- if ( this . #pending === 0 ) {
157- // TODO we need this because we commit _then_ flush effects...
158- // maybe there's a way we can reverse the order?
159- var previous_batch_sources = batch_values ;
162+ this . #resolve( ) ;
160163
161- this . #commit( ) ;
162-
163- var render_effects = this . #render_effects;
164- var effects = this . #effects;
165-
166- this . #render_effects = [ ] ;
167- this . #effects = [ ] ;
168- this . #block_effects = [ ] ;
164+ if ( this . #blocking_pending > 0 ) {
165+ this . #defer_effects( target . effects ) ;
166+ this . #defer_effects( target . render_effects ) ;
167+ this . #defer_effects( target . block_effects ) ;
168+ } else {
169+ // TODO append/detach blocks here, not in #commit
169170
170171 // If sources are written to, then work needs to happen in a separate batch, else prior sources would be mixed with
171172 // newly updated sources, which could lead to infinite loops when effects run over and over again.
172173 previous_batch = this ;
173174 current_batch = null ;
174175
175- batch_values = previous_batch_sources ;
176- flush_queued_effects ( render_effects ) ;
177- flush_queued_effects ( effects ) ;
176+ flush_queued_effects ( target . render_effects ) ;
177+ flush_queued_effects ( target . effects ) ;
178178
179179 previous_batch = null ;
180-
181- this . #deferred?. resolve ( ) ;
182- } else {
183- this . #defer_effects( this . #render_effects) ;
184- this . #defer_effects( this . #effects) ;
185- this . #defer_effects( this . #block_effects) ;
186180 }
187181
188182 batch_values = null ;
@@ -192,8 +186,9 @@ export class Batch {
192186 * Traverse the effect tree, executing effects or stashing
193187 * them for later execution as appropriate
194188 * @param {Effect } root
189+ * @param {EffectTarget } target
195190 */
196- #traverse_effect_tree( root ) {
191+ #traverse_effect_tree( root , target ) {
197192 root . f ^= CLEAN ;
198193
199194 var effect = root . first ;
@@ -205,15 +200,25 @@ export class Batch {
205200
206201 var skip = is_skippable_branch || ( flags & INERT ) !== 0 || this . skipped_effects . has ( effect ) ;
207202
203+ if ( ( effect . f & BOUNDARY_EFFECT ) !== 0 && effect . b ?. is_pending ( ) ) {
204+ target = {
205+ parent : target ,
206+ effect,
207+ effects : [ ] ,
208+ render_effects : [ ] ,
209+ block_effects : [ ]
210+ } ;
211+ }
212+
208213 if ( ! skip && effect . fn !== null ) {
209214 if ( is_branch ) {
210215 effect . f ^= CLEAN ;
211216 } else if ( ( flags & EFFECT ) !== 0 ) {
212- this . # effects. push ( effect ) ;
217+ target . effects . push ( effect ) ;
213218 } else if ( async_mode_flag && ( flags & RENDER_EFFECT ) !== 0 ) {
214- this . # render_effects. push ( effect ) ;
219+ target . render_effects . push ( effect ) ;
215220 } else if ( is_dirty ( effect ) ) {
216- if ( ( effect . f & BLOCK_EFFECT ) !== 0 ) this . # block_effects. push ( effect ) ;
221+ if ( ( effect . f & BLOCK_EFFECT ) !== 0 ) target . block_effects . push ( effect ) ;
217222 update_effect ( effect ) ;
218223 }
219224
@@ -229,6 +234,17 @@ export class Batch {
229234 effect = effect . next ;
230235
231236 while ( effect === null && parent !== null ) {
237+ if ( parent === target . effect ) {
238+ // TODO rather than traversing into pending boundaries and deferring the effects,
239+ // could we just attach the effects _to_ the pending boundary and schedule them
240+ // once the boundary is ready?
241+ this . #defer_effects( target . effects ) ;
242+ this . #defer_effects( target . render_effects ) ;
243+ this . #defer_effects( target . block_effects ) ;
244+
245+ target = /** @type {EffectTarget } */ ( target . parent ) ;
246+ }
247+
232248 effect = parent . next ;
233249 parent = parent . parent ;
234250 }
@@ -246,8 +262,6 @@ export class Batch {
246262 // mark as clean so they get scheduled if they depend on pending async state
247263 set_signal_status ( e , CLEAN ) ;
248264 }
249-
250- effects . length = 0 ;
251265 }
252266
253267 /**
@@ -283,8 +297,8 @@ export class Batch {
283297 // this can happen if a new batch was created during `flush_effects()`
284298 return ;
285299 }
286- } else if ( this . #pending === 0 ) {
287- this . #commit ( ) ;
300+ } else {
301+ this . #resolve ( ) ;
288302 }
289303
290304 this . deactivate ( ) ;
@@ -300,24 +314,37 @@ export class Batch {
300314 }
301315 }
302316
303- /**
304- * Append and remove branches to/from the DOM
305- */
306- #commit( ) {
307- for ( const fn of this . #callbacks) {
308- fn ( ) ;
317+ #resolve( ) {
318+ if ( this . #blocking_pending === 0 ) {
319+ // append/remove branches
320+ for ( const fn of this . #callbacks) fn ( ) ;
321+ this . #callbacks. clear ( ) ;
309322 }
310323
311- this . #callbacks. clear ( ) ;
324+ if ( this . #pending === 0 ) {
325+ this . #commit( ) ;
326+ }
327+ }
312328
329+ #commit( ) {
313330 // If there are other pending batches, they now need to be 'rebased' —
314331 // in other words, we re-run block/async effects with the newly
315332 // committed state, unless the batch in question has a more
316333 // recent value for a given source
317334 if ( batches . size > 1 ) {
318335 this . #previous. clear ( ) ;
319336
320- let is_earlier = true ;
337+ var previous_batch_values = batch_values ;
338+ var is_earlier = true ;
339+
340+ /** @type {EffectTarget } */
341+ var dummy_target = {
342+ parent : null ,
343+ effect : null ,
344+ effects : [ ] ,
345+ render_effects : [ ] ,
346+ block_effects : [ ]
347+ } ;
321348
322349 for ( const batch of batches ) {
323350 if ( batch === this ) {
@@ -359,27 +386,43 @@ export class Batch {
359386 batch . apply ( ) ;
360387
361388 for ( const root of queued_root_effects ) {
362- batch . #traverse_effect_tree( root ) ;
389+ batch . #traverse_effect_tree( root , dummy_target ) ;
363390 }
364391
392+ // TODO do we need to do anything with `target`? defer block effects?
393+
365394 queued_root_effects = [ ] ;
366395 batch . deactivate ( ) ;
367396 }
368397 }
369398 }
370399
371400 current_batch = null ;
401+ batch_values = previous_batch_values ;
372402 }
373403
404+ this . committed = true ;
374405 batches . delete ( this ) ;
406+
407+ this . #deferred?. resolve ( ) ;
375408 }
376409
377- increment ( ) {
410+ /**
411+ *
412+ * @param {boolean } blocking
413+ */
414+ increment ( blocking ) {
378415 this . #pending += 1 ;
416+ if ( blocking ) this . #blocking_pending += 1 ;
379417 }
380418
381- decrement ( ) {
419+ /**
420+ *
421+ * @param {boolean } blocking
422+ */
423+ decrement ( blocking ) {
382424 this . #pending -= 1 ;
425+ if ( blocking ) this . #blocking_pending -= 1 ;
383426
384427 for ( const e of this . #dirty_effects) {
385428 set_signal_status ( e , DIRTY ) ;
@@ -391,6 +434,9 @@ export class Batch {
391434 schedule_effect ( e ) ;
392435 }
393436
437+ this . #dirty_effects = [ ] ;
438+ this . #maybe_dirty_effects = [ ] ;
439+
394440 this . flush ( ) ;
395441 }
396442
0 commit comments