don't write values to deriveds when time travelling

pull/16197/head
Rich Harris 4 months ago
parent 3cf0b9e077
commit 302dff234b

@ -1,4 +1,4 @@
/** @import { Effect, Source } from '#client' */ /** @import { Derived, Effect, Source } from '#client' */
import { CLEAN, DIRTY } from '#client/constants'; import { CLEAN, DIRTY } from '#client/constants';
import { import {
flush_queued_effects, flush_queued_effects,
@ -23,7 +23,8 @@ function update_pending() {
internal_set(pending, batches.size > 0); internal_set(pending, batches.size > 0);
} }
let uid = 1; /** @type {Map<Derived, any> | null} */
export let batch_deriveds = null;
export class Batch { export class Batch {
/** @type {Map<Source, any>} */ /** @type {Map<Source, any>} */
@ -60,21 +61,34 @@ export class Batch {
process(root_effects) { process(root_effects) {
set_queued_root_effects([]); set_queued_root_effects([]);
/** @type {Map<Source, { v: unknown, wv: number }>} */ /** @type {Map<Source, { v: unknown, wv: number }> | null} */
var current_values = new Map(); var current_values = null;
var time_travelling = false;
for (const [source, current] of this.#current) { for (const batch of batches) {
current_values.set(source, { v: source.v, wv: source.wv }); if (batch !== this) {
source.v = current; time_travelling = true;
break;
}
} }
for (const batch of batches) { if (time_travelling) {
if (batch === this) continue; 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) { for (const batch of batches) {
if (!current_values.has(source)) { if (batch === this) continue;
current_values.set(source, { v: source.v, wv: source.wv });
source.v = previous; 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 e of this.effects) set_signal_status(e, CLEAN);
} }
for (const [source, { v, wv }] of current_values) { if (current_values) {
// reset the source to the current value (unless for (const [source, { v, wv }] of current_values) {
// it got a newer value as a result of effects running) // reset the source to the current value (unless
if (source.wv <= wv) { // it got a newer value as a result of effects running)
source.v = v; if (source.wv <= wv) {
source.v = v;
}
} }
batch_deriveds = null;
} }
for (const effect of this.async_effects) { for (const effect of this.async_effects) {

@ -39,6 +39,7 @@ import { flush_tasks } from './dom/task.js';
import { internal_set, old_values } from './reactivity/sources.js'; import { internal_set, old_values } from './reactivity/sources.js';
import { import {
destroy_derived_effects, destroy_derived_effects,
execute_derived,
from_async_derived, from_async_derived,
recent_async_deriveds, recent_async_deriveds,
update_derived update_derived
@ -57,7 +58,7 @@ import {
import { Boundary } from './dom/blocks/boundary.js'; import { Boundary } from './dom/blocks/boundary.js';
import * as w from './warnings.js'; import * as w from './warnings.js';
import { is_firefox } from './dom/operations.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'; import { log_effect_tree, root } from './dev/debug.js';
// Used for DEV time error handling // 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); derived = /** @type {Derived} */ (signal);
if (check_dirtiness(derived)) { if (check_dirtiness(derived)) {
@ -1032,6 +1036,19 @@ export function get(signal) {
return old_values.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; return signal.v;
} }

@ -0,0 +1,59 @@
import { flushSync, settled, tick } from 'svelte';
import { test } from '../../test';
export default test({
html: `<p>loading...</p>`,
async test({ assert, target }) {
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,
`
<button>log</button>
<button>x 1</button>
<button>other 1</button>
<p>1</p>
<p>1</p>
<p>1</p>
`
);
const [log, x, other] = target.querySelectorAll('button');
flushSync(() => x.click());
flushSync(() => other.click());
assert.htmlEqual(
target.innerHTML,
`
<button>log</button>
<button>x 1</button>
<button>other 2</button>
<p>1</p>
<p>1</p>
<p>1</p>
`
);
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,
`
<button>log</button>
<button>x 2</button>
<button>other 2</button>
<p>2</p>
<p>2</p>
<p>2</p>
`
);
}
});

@ -0,0 +1,19 @@
<script>
let x = $state(1);
let y = $derived(x);
let other = $state(1);
</script>
<svelte:boundary>
<button onclick={() => console.log({ x, y })}>log</button>
<button onclick={() => x += 1}>x {x}</button>
<button onclick={() => other += 1}>other {other}</button>
<p>{x}</p>
<p>{await x}</p>
<p>{y}</p>
{#snippet pending()}
<p>loading...</p>
{/snippet}
</svelte:boundary>
Loading…
Cancel
Save