diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 8d1a0692d6..7c051079df 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -18,19 +18,20 @@ import { update_reaction, increment_write_version, set_active_effect, - handle_error + handle_error, + flush_sync } from '../runtime.js'; import { equals, safe_equals } from './equality.js'; import * as e from '../errors.js'; import * as w from '../warnings.js'; -import { block, destroy_effect, render_effect } from './effects.js'; +import { destroy_effect, render_effect } from './effects.js'; import { inspect_effects, internal_set, set_inspect_effects, source } from './sources.js'; import { get_stack } from '../dev/tracing.js'; import { tracing_mode_flag } from '../../flags/index.js'; -import { capture, suspend } from '../dom/blocks/boundary.js'; +import { capture } from '../dom/blocks/boundary.js'; import { component_context } from '../context.js'; -import { noop } from '../../shared/utils.js'; import { UNINITIALIZED } from '../../../constants.js'; +import { active_fork } from './forks.js'; /** @type {Effect | null} */ export let from_async_derived = null; @@ -105,16 +106,19 @@ export function async_derived(fn, location) { // only suspend in async deriveds created on initialisation var should_suspend = !active_reaction; - /** @type {(() => void) | null} */ - var unsuspend = null; - render_effect(() => { if (DEV) from_async_derived = active_effect; - var current = (promise = fn()); + promise = fn(); if (DEV) from_async_derived = null; var restore = capture(); - if (should_suspend) unsuspend ??= suspend(); + + var fork = active_fork; + + if (should_suspend) { + // TODO if nearest pending boundary is not ready, attach to the boundary + fork?.increment(); + } promise.then( (v) => { @@ -122,33 +126,36 @@ export function async_derived(fn, location) { return; } - if (promise === current) { - restore(); - from_async_derived = null; + restore(); + from_async_derived = null; + if (should_suspend) { + fork?.decrement(); + } + + if (fork !== null) { + fork?.enable(); + flush_sync(() => { + internal_set(signal, v); + }); + fork?.disable(); + } else { internal_set(signal, v); + } - if (DEV && location !== undefined) { - recent_async_deriveds.add(signal); - - setTimeout(() => { - if (recent_async_deriveds.has(signal)) { - w.await_waterfall(location); - recent_async_deriveds.delete(signal); - } - }); - } - - // TODO we should probably null out active effect here, - // rather than inside `restore()` - unsuspend?.(); - unsuspend = null; + if (DEV && location !== undefined) { + recent_async_deriveds.add(signal); + + setTimeout(() => { + if (recent_async_deriveds.has(signal)) { + w.await_waterfall(location); + recent_async_deriveds.delete(signal); + } + }); } }, (e) => { - if (promise === current) { - handle_error(e, parent, null, parent.ctx); - } + handle_error(e, parent, null, parent.ctx); } ); }, EFFECT_ASYNC | EFFECT_PRESERVED); diff --git a/packages/svelte/src/internal/client/reactivity/forks.js b/packages/svelte/src/internal/client/reactivity/forks.js new file mode 100644 index 0000000000..2529772b9c --- /dev/null +++ b/packages/svelte/src/internal/client/reactivity/forks.js @@ -0,0 +1,61 @@ +/** @import { Effect, Source } from '#client' */ + +/** @type {Set} */ +const forks = new Set(); + +/** @type {Fork | null} */ +export let active_fork = null; + +let uid = 1; + +export class Fork { + id = uid++; + + /** @type {Map} */ + previous = new Map(); + + /** @type {Set} */ + skipped_effects = new Set(); + + #pending = 0; + + /** + * @param {Source} source + * @param {any} value + */ + capture(source, value) { + if (!this.previous.has(source)) { + this.previous.set(source, value); + } + } + + enable() { + active_fork = this; + // TODO revert other forks + } + + disable() { + active_fork = null; + // TODO restore state + } + + increment() { + this.#pending += 1; + } + + decrement() { + this.#pending -= 1; + } + + settled() { + return this.#pending === 0; + } + + static ensure() { + return (active_fork ??= new Fork()); + } + + static unset() { + active_fork = null; + } +} diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index efc5aa20fe..33a23251ea 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -35,6 +35,7 @@ import * as e from '../errors.js'; import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js'; import { get_stack } from '../dev/tracing.js'; import { component_context, is_runes } from '../context.js'; +import { active_fork, Fork } from './forks.js'; export let inspect_effects = new Set(); @@ -174,6 +175,9 @@ export function internal_set(source, value) { source.v = value; source.wv = increment_write_version(); + const fork = Fork.ensure(); + fork.capture(source, old_value); + if (DEV && tracing_mode_flag) { source.updated = get_stack('UpdatedAt'); if (active_effect != null) { @@ -260,7 +264,7 @@ export function update_pre(source, d = 1) { * @param {number} status should be DIRTY or MAYBE_DIRTY * @returns {void} */ -function mark_reactions(signal, status) { +export function mark_reactions(signal, status) { var reactions = signal.reactions; if (reactions === null) return; @@ -271,9 +275,6 @@ function mark_reactions(signal, status) { var reaction = reactions[i]; var flags = reaction.f; - // Skip any effects that are already dirty - if ((flags & DIRTY) !== 0) continue; - // In legacy mode, skip the current effect to prevent infinite loops if (!runes && reaction === active_effect) continue; @@ -285,13 +286,10 @@ function mark_reactions(signal, status) { set_signal_status(reaction, status); - // If the signal a) was previously clean or b) is an unowned derived, then mark it - if ((flags & (CLEAN | UNOWNED)) !== 0) { - if ((flags & DERIVED) !== 0) { - mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY); - } else { - schedule_effect(/** @type {Effect} */ (reaction)); - } + if ((flags & DERIVED) !== 0) { + mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY); + } else { + schedule_effect(/** @type {Effect} */ (reaction)); } } } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index b352d1a75f..d738ffe40f 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -53,6 +53,8 @@ import { import { Boundary } from './dom/blocks/boundary.js'; import * as w from './warnings.js'; import { is_firefox } from './dom/operations.js'; +import { active_fork, Fork } from './reactivity/forks.js'; +import { log_effect_tree } from './dev/debug.js'; const FLUSH_MICROTASK = 0; const FLUSH_SYNC = 1; @@ -702,10 +704,14 @@ function flush_queued_root_effects(root_effects) { } var collected_effects = process_effects(effect); - flush_queued_effects(collected_effects); + + if (/** @type {Fork} */ (active_fork).settled()) { + flush_queued_effects(collected_effects); + } } } finally { is_flushing_effect = previously_flushing_effect; + Fork.unset(); } } @@ -805,14 +811,16 @@ export function schedule_effect(signal) { * effects to be flushed. * * @param {Effect} effect - * @param {Effect[]} effects - * @param {Boundary} [boundary] * @returns {Effect[]} */ -function process_effects(effect, effects = [], boundary) { +function process_effects(effect) { var current_effect = effect.first; - var current_effect = effect.first; + /** @type {Effect[]} */ + var render_effects = []; + + /** @type {Effect[]} */ + var effects = []; main_loop: while (current_effect !== null) { var flags = current_effect.f; @@ -820,64 +828,24 @@ function process_effects(effect, effects = [], boundary) { var is_skippable_branch = is_branch && (flags & CLEAN) !== 0; var sibling = current_effect.next; - if (!is_skippable_branch && (flags & INERT) === 0) { - if (boundary !== undefined && (flags & (BLOCK_EFFECT | BRANCH_EFFECT | EFFECT_ASYNC)) === 0) { - // Inside a boundary, defer everything except block/branch effects - boundary.add_effect(current_effect); - } else if ((flags & BOUNDARY_EFFECT) !== 0) { - var b = /** @type {Boundary} */ (current_effect.b); + var skip = + is_skippable_branch || + (flags & INERT) !== 0 || + active_fork?.skipped_effects.has(current_effect); - process_effects(current_effect, effects, b); - - if (!b.suspended) { - // no more async work to happen - b.commit(); + if (!skip) { + if ((flags & (BLOCK_EFFECT | EFFECT_ASYNC)) !== 0) { + if (check_dirtiness(current_effect)) { + update_effect(current_effect); } } else if ((flags & RENDER_EFFECT) !== 0) { if (is_branch) { current_effect.f ^= CLEAN; } else { - // Ensure we set the effect to be the active reaction - // to ensure that unowned deriveds are correctly tracked - // because we're flushing the current effect - var previous_active_reaction = active_reaction; - try { - active_reaction = current_effect; - if (check_dirtiness(current_effect)) { - update_effect(current_effect); - } - } catch (error) { - handle_error(error, current_effect, null, current_effect.ctx); - } finally { - active_reaction = previous_active_reaction; - } - } - - var child = current_effect.first; - - if (child !== null) { - current_effect = child; - continue; + render_effects.push(current_effect); } } else if ((flags & EFFECT) !== 0) { effects.push(current_effect); - } else if (is_branch) { - current_effect.f ^= CLEAN; - } else { - // Ensure we set the effect to be the active reaction - // to ensure that unowned deriveds are correctly tracked - // because we're flushing the current effect - var previous_active_reaction = active_reaction; - try { - active_reaction = current_effect; - if (check_dirtiness(current_effect)) { - update_effect(current_effect); - } - } catch (error) { - handle_error(error, current_effect, null, current_effect.ctx); - } finally { - active_reaction = previous_active_reaction; - } } var child = current_effect.first; @@ -908,7 +876,7 @@ function process_effects(effect, effects = [], boundary) { current_effect = sibling; } - return effects; + return [...render_effects, ...effects]; } /**