From 302dff234b000df1ba9c6006624b66fc473008a1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 1 Jun 2025 21:05:09 -0400 Subject: [PATCH] don't write values to deriveds when time travelling --- .../src/internal/client/reactivity/batch.js | 54 +++++++++++------ .../svelte/src/internal/client/runtime.js | 21 ++++++- .../async-with-sync-derived/_config.js | 59 +++++++++++++++++++ .../async-with-sync-derived/main.svelte | 19 ++++++ 4 files changed, 133 insertions(+), 20 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/main.svelte diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index d705846477..9e1fcf7075 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -1,4 +1,4 @@ -/** @import { Effect, Source } from '#client' */ +/** @import { Derived, Effect, Source } from '#client' */ import { CLEAN, DIRTY } from '#client/constants'; import { flush_queued_effects, @@ -23,7 +23,8 @@ function update_pending() { internal_set(pending, batches.size > 0); } -let uid = 1; +/** @type {Map | null} */ +export let batch_deriveds = null; export class Batch { /** @type {Map} */ @@ -60,21 +61,34 @@ export class Batch { process(root_effects) { set_queued_root_effects([]); - /** @type {Map} */ - var current_values = new Map(); + /** @type {Map | null} */ + var current_values = null; + var time_travelling = false; - for (const [source, current] of this.#current) { - current_values.set(source, { v: source.v, wv: source.wv }); - source.v = current; + for (const batch of batches) { + if (batch !== this) { + time_travelling = true; + break; + } } - for (const batch of batches) { - if (batch === this) continue; + if (time_travelling) { + current_values = new Map(); + batch_deriveds = new Map(); + + for (const [source, current] of this.#current) { + current_values.set(source, { v: source.v, wv: source.wv }); + source.v = current; + } - for (const [source, previous] of batch.#previous) { - if (!current_values.has(source)) { - current_values.set(source, { v: source.v, wv: source.wv }); - source.v = previous; + for (const batch of batches) { + if (batch === this) continue; + + for (const [source, previous] of batch.#previous) { + if (!current_values.has(source)) { + current_values.set(source, { v: source.v, wv: source.wv }); + source.v = previous; + } } } } @@ -144,12 +158,16 @@ export class Batch { for (const e of this.effects) set_signal_status(e, CLEAN); } - for (const [source, { v, wv }] of current_values) { - // reset the source to the current value (unless - // it got a newer value as a result of effects running) - if (source.wv <= wv) { - source.v = v; + if (current_values) { + for (const [source, { v, wv }] of current_values) { + // reset the source to the current value (unless + // it got a newer value as a result of effects running) + if (source.wv <= wv) { + source.v = v; + } } + + batch_deriveds = null; } for (const effect of this.async_effects) { diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 39b27f9f73..76c18e6362 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -39,6 +39,7 @@ import { flush_tasks } from './dom/task.js'; import { internal_set, old_values } from './reactivity/sources.js'; import { destroy_derived_effects, + execute_derived, from_async_derived, recent_async_deriveds, update_derived @@ -57,7 +58,7 @@ import { import { Boundary } from './dom/blocks/boundary.js'; import * as w from './warnings.js'; import { is_firefox } from './dom/operations.js'; -import { current_batch, Batch } from './reactivity/batch.js'; +import { current_batch, Batch, batch_deriveds } from './reactivity/batch.js'; import { log_effect_tree, root } from './dev/debug.js'; // Used for DEV time error handling @@ -986,7 +987,10 @@ export function get(signal) { } } - if (is_derived) { + // if this is a derived, we may need to update it, but + // not if `batch_deriveds` is not null (meaning we're + // currently time travelling)) + if (is_derived && batch_deriveds === null) { derived = /** @type {Derived} */ (signal); if (check_dirtiness(derived)) { @@ -1032,6 +1036,19 @@ export function get(signal) { return old_values.get(signal); } + // if we're time travelling, we don't want to update the + // intrinsic value of the derived — we want to compute it + // once and stash it for the duration of batch processing + if (is_derived && batch_deriveds !== null) { + derived = /** @type {Derived} */ (signal); + + if (!batch_deriveds.has(derived)) { + batch_deriveds.set(derived, execute_derived(derived)); + } + + return batch_deriveds.get(derived); + } + return signal.v; } diff --git a/packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/_config.js new file mode 100644 index 0000000000..c09d448f9c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/_config.js @@ -0,0 +1,59 @@ +import { flushSync, settled, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

loading...

`, + + async test({ assert, target }) { + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

1

+

1

+

1

+ ` + ); + + const [log, x, other] = target.querySelectorAll('button'); + + flushSync(() => x.click()); + flushSync(() => other.click()); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

1

+

1

+

1

+ ` + ); + + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

2

+

2

+

2

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/main.svelte new file mode 100644 index 0000000000..764007e082 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/main.svelte @@ -0,0 +1,19 @@ + + + + + + + +

{x}

+

{await x}

+

{y}

+ + {#snippet pending()} +

loading...

+ {/snippet} +