From 9143c90b048231428196166521cc0ffe4c2db3cf Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 17 Sep 2026 22:36:18 +0200 Subject: [PATCH] more messy experimentation --- .../src/internal/client/reactivity/batch.js | 74 ++++++++++++++----- .../internal/client/reactivity/deriveds.js | 12 +-- .../svelte/src/internal/client/runtime.js | 67 ++++++++++++++++- 3 files changed, 124 insertions(+), 29 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 90378b90d5..6db15eb0c7 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -112,7 +112,7 @@ export class Batch { id = uid++; /** True as soon as `#process` was called */ - #started = false; + started = false; // TODO temporary get next() { @@ -125,6 +125,9 @@ export class Batch { linked = true; + /** @type {Map} */ + stale_effects = new Map(); + /** @type {Set} */ effects_ran = new Set(); @@ -225,6 +228,10 @@ export class Batch { */ #maybe_dirty_effects = new Set(); + get maybe_dirty_effects() { + return this.#maybe_dirty_effects; // TODO temporary + } + /** * Deferred derived effects that are DIRTY * @type {Set} @@ -378,7 +385,7 @@ export class Batch { } #process() { - this.#started = true; + this.started = true; if (DEV) { // track all the values that were updated during this flush, @@ -399,8 +406,10 @@ export class Batch { } for (const e of this.#maybe_dirty_effects) { - set_signal_status(e, MAYBE_DIRTY); - this.schedule(e); + if ((e.f & DIRTY) === 0) { + set_signal_status(e, MAYBE_DIRTY); + this.schedule(e); + } } for (const d of this.#dirty_deriveds) { @@ -417,7 +426,7 @@ export class Batch { // TODO does this make the similar logic in fork.commit below obsolete? // TODO feels correct but breaks many tests // for (const [s, [_, is_derived]] of this.current) { - // if (!is_derived) this.mark(s, DIRTY, true); + // if (!is_derived) this.mark(s, MAYBE_DIRTY, true); // } this.apply(); @@ -822,19 +831,12 @@ export class Batch { wv_values?.set(source, wv); } - let batch = this.#prev; - while (batch) { - if (batch.current.has(source)) { - this.dependent.add(batch); - break; - } - batch = batch.#prev; - } - + let batch = this.#next; let is_latest_value = !this.is_fork; - batch = this.#next; while (batch) { if (source.f & ASYNC) { + // TODO I think this is wrong IF the async source was already written to by a later batch; + // we gotta check if it's the source is also part of the later batch. const b = batch; const run = () => { if (b.mark(source, DIRTY)) { @@ -842,7 +844,7 @@ export class Batch { } }; if (this.is_fork) { - // this.on_fork_commit.set({}, run); // TODO + // this.on_fork_commit.set({}, run); // TODO done by mark in commit already? } else { queue_micro_task(run); } @@ -871,6 +873,34 @@ export class Batch { source.wv = wv; } + batch = first_batch; + while (batch) { + if (batch.id < this.id && batch.current.has(source)) { + this.dependent.add(batch); + } + if ( + batch.is_fork && + is_latest_value && + ((!batch.current.has(source) && !is_derived) || + /** @type {[any, boolean, number]} */ (batch.current.get(source))[0] !== value) && + ((source.f & ASYNC) === 0 || + !depends_on( + source.e, + [...batch.current.keys()].filter((s) => !this.current.has(s)), + new Map() + )) + ) { + batch.current.set(source, [value, is_derived, wv]); + const b = batch; + queue_micro_task(() => { + if (b.mark(source, DIRTY)) { + b.flush(); + } + }); + } + batch = batch.#next; + } + // if (!this.is_fork) { // let is_latest_value = true; // batch = this.#next; @@ -989,7 +1019,7 @@ export class Batch { ); // If not started yet or no sources to update (which is e.g. possible for the very first batch) then bail - if (!batch.#started || current.length === 0) continue; + if (!batch.started || current.length === 0) continue; // Re-run async/block effects that depend on distinct values changed in both batches (ignoring deriveds) var others = current.filter((source) => !this.current.has(source)); @@ -1161,7 +1191,7 @@ export class Batch { if (!is_processing && !is_flushing_sync) { queue_micro_task(() => { - if (!batch.#started) { + if (!batch.started) { batch.flush(); } }); @@ -1301,6 +1331,8 @@ export class Batch { */ export function flushSync(fn) { var was_flushing_sync = is_flushing_sync; + var prev_previous_batch = previous_batch; + previous_batch = null; is_flushing_sync = true; try { @@ -1325,6 +1357,7 @@ export function flushSync(fn) { } } finally { is_flushing_sync = was_flushing_sync; + previous_batch = prev_previous_batch; } } @@ -1708,13 +1741,14 @@ export function fork(fn) { // but has false positives (i.e. values not updated when they should). Needs a better mechanism // maybe current has a fourth entry, "outdated" boolean, and later batches set it for earlier ones? // if (wv >= source.wv) { - source.v = value; // Do not use cached wv here; real world might have executed a dependent derived and now have a later version // TODO we need to ensure that the version bumps happen "in order", e.g. in case of source1->derived2 we need to bump S last - source.wv = increment_write_version(); // } if (!is_derived) { + source.v = value; + source.wv = increment_write_version(); + // batch.mark(source, ...) TODO re-maybe-dirty- everything? // dirty those effects the fork did not see yet, e.g. because a later batch created new branches batch.mark(source, DIRTY, true); // TODO probably better to only DIRTY on first non-seen derived } diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index a15fed758b..d703054008 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -122,6 +122,10 @@ export function async_derived(fn, label, location) { var promise = /** @type {Promise} */ (/** @type {unknown} */ (undefined)); var signal = source(/** @type {V} */ (UNINITIALIZED)); + // Besides prod-logic this also helps in DEV to let this be printed + // as a derived when using `$inspect.trace()` + signal.f |= ASYNC; + if (DEV) signal.label = label ?? fn.toString(); // only suspend in async deriveds created on initialisation @@ -130,7 +134,8 @@ export function async_derived(fn, label, location) { /** @type {Set>>} */ var deferreds = new Set(); - async_effect(() => { + // TODO types; add to source, or special object to not have all the other objects contain another property? + signal.e = async_effect(() => { var effect = /** @type {Effect} */ (active_effect); if (DEV) { @@ -269,6 +274,7 @@ export function async_derived(fn, label, location) { }); } + debugger; internal_set(signal, value); } @@ -284,10 +290,6 @@ export function async_derived(fn, label, location) { } }); - // Besides prod-logic this also helps in DEV to let this be printed - // as a derived when using `$inspect.trace()` - signal.f |= ASYNC; - return new Promise((fulfil) => { /** @param {Promise} p */ function next(p) { diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 0de3758427..2ab74e0f9f 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -489,7 +489,64 @@ export function update_effect(effect) { execute_effect_teardown(effect); var teardown = update_reaction(effect); effect.teardown = typeof teardown === 'function' ? teardown : null; - effect.wv = write_version; + + // TODO consolidate with similar logic in batch.capture() + var own_batch = previous_batch ?? current_batch; + let is_latest_value = true; + // Can be falsy inside flush_eager_effects + if (own_batch) { + is_latest_value = !own_batch.is_fork; + var batch = own_batch.next; + while (batch) { + if ( + batch.started && // when flushing user effects writes sources which creates a new batch, then ignore that one + (!is_latest_value || + // Check derived's dependencies for outdated values. We only have to check one + // level because is_dirty etc will execute the top-most deriveds first, whose result + // the later deriveds can use to make a decision ("oh this derived's value is different to what I cached") + effect.deps?.some((d) => { + var other_current = /** @type {Batch} */ (batch).current; + var own_current = /** @type {Batch} */ (own_batch).current; + // const y = + // other_current.has(d) || + // (own_current.has(d) && + // /** @type {[any, boolean, number]} */ (own_current.get(d))[0] !== d.v); + // if (y) debugger; + // const x = + // other_current.has(d) && + // (!own_current.has(d) || + // /** @type {[any, boolean, number]} */ (own_current.get(d))[0] !== + // /** @type {[any, boolean, number]} */ (other_current.get(d))[0]); + // if (x) debugger; + const z = (own_current.get(d)?.[2] ?? d.wv) != d.wv; + return z; + })) + ) { + is_latest_value = false; + } + batch = batch.next; + } + } + + if (is_latest_value) { + effect.wv = write_version; + } else { + // console.log('setting', effect.wv, effect, 'to', write_version, is_latest_value); + // effect.wv = write_version; + // set_signal_status(effect, MAYBE_DIRTY); + // debugger; + if (!is_latest_value) { + /** @type {Batch} */ (own_batch).stale_effects.set(effect, write_version); + var batch = /** @type {Batch} */ (own_batch).next; + while (batch) { + batch.maybe_dirty_effects.add(effect); + batch = batch.next; + } + } + + // TODO add to maybe_dirty_effects in all subsequent batches here, + // removing need for other cross-batch rerun mechanisms / remove need for adding blocks to maybe_dirty? + } // In DEV, increment versions of any sources that were written to during the effect, // so that they are correctly marked as dirty when the effect re-runs @@ -719,9 +776,11 @@ export function get(signal) { } if ( - // TODO correct?! I think the failure can only occur in case we see new values for the first time while flushing (render)effects - (!first_time || - !previous_batch) /* || current_batch?.is_fork) || signal.v === UNINITIALIZED*/ && + // TODO correct?! I thought the failure can only occur in case we see new values for the first time while flushing (render)effects, + // but it can also occur when resolving async deriveds after creating them for the first time, which can happen outside + // the effects flush phase. + (!first_time || !previous_batch) && + // (!first_time || current_batch?.is_fork || signal.v === UNINITIALIZED) && batch_values?.has(signal) ) { return batch_values.get(signal);