we need the "has pending batches" check to keep deriveds (maybe)dirty in more places after all

teemingc-hydration-regression-test
Simon Holthausen 3 days ago
parent 458397909c
commit 9f12fdd61f
No known key found for this signature in database

@ -132,6 +132,23 @@ export function own_value(signal) {
: signal;
}
/**
* Whether some batch's view of the world (may) differ from the real one: there's a batch that is going to be
* processed (again) in its own view, and another batch, or it's a fork. The batch that is being committed and
* the one that hasn't been processed yet don't count, their view is the real one. Deriveds that are computed
* from the real values (e.g. in an event handler) must not be marked clean then, as those batches wouldn't
* re-validate them in their view (see `update_derived`)
*/
export function has_pending_batches() {
for (var batch = first_batch; batch !== null; batch = batch.next) {
if (batch !== previous_batch && batch !== current_batch) {
return batch.is_fork || /** @type {Batch} */ (first_batch).next !== null;
}
}
return false;
}
/** @type {Effect | null} */
let last_scheduled_effect = null;

@ -48,6 +48,7 @@ import {
batch_values,
current_batch,
first_batch,
has_pending_batches,
own_value,
previous_batch,
read_batch_local_value
@ -467,6 +468,13 @@ export function update_derived(derived) {
} else if (derived.v !== UNINITIALIZED) {
set_signal_status(derived, MAYBE_DIRTY);
}
} else if (has_pending_batches()) {
// Computed from the real values (e.g. in an event handler) while other batches are in flight.
// Don't mark it clean: those batches see other values for its dependencies, and unless they
// stashed a reaction that reads it, they wouldn't re-validate it and would use whatever value
// they hold for it — which can be stale, e.g. if the batch that dirtied it never evaluated it
// (see `async-derived-read-keeps-maybe-dirty`)
set_signal_status(derived, MAYBE_DIRTY);
} else {
update_derived_status(derived);
}

@ -38,6 +38,7 @@ import {
schedule_effect,
legacy_updates,
current_batch,
has_pending_batches,
own_value
} from './batch.js';
import { proxy } from '../proxy.js';
@ -208,7 +209,11 @@ export function internal_set(source, value, updated_during_traversal = null) {
// During time traveling we don't want to reset the status so that
// traversal of the graph in the other batches still happens
if (batch_values === null) {
if (
batch_values === null &&
// could also be "read outside of reactivity", e.g. in an event handler, while other batches are in flight
!has_pending_batches()
) {
update_derived_status(derived);
}
}

@ -53,6 +53,7 @@ import {
current_batch,
first_batch,
flushSync,
has_pending_batches,
held_sources,
previous_batch,
read_batch_local_value,
@ -188,8 +189,9 @@ export function is_dirty(reaction) {
// During time traveling we don't want to reset the status so that
// traversal of the graph in the other batches still happens. Effects
// can be reset because block/async effects execute right away and others
// are deferred and re-dirtied as needed.
(batch_values === null || (flags & DERIVED) === 0)
// are deferred and re-dirtied as needed. Deriveds also mustn't be reset
// outside of a batch's view while other batches are in flight (see `update_derived`)
((flags & DERIVED) === 0 || (batch_values === null && !has_pending_batches()))
) {
set_signal_status(reaction, CLEAN);
}

@ -0,0 +1,30 @@
import { tick } from 'svelte';
import { test } from '../../test';
// Like `async-derived-read-keeps-maybe-dirty`, but the `<span>` also reads `s`, which makes it DIRTY
// rather than MAYBE_DIRTY, so A doesn't evaluate `d` even when checking the `<span>` effect eagerly
// (`is_dirty` returns right away). C's read of `d` must still not mark it as clean
export default test({
mode: ['client'],
async test({ assert, target, logs }) {
await tick();
const [s, u, d, , resolve_b] = target.querySelectorAll('button');
// batch A: `d` becomes 10, but is not evaluated yet (the `<span>` is deferred)
s.click();
await tick();
// batch B
u.click();
await tick();
// batch C writes `s` and reads `d` (20) while A and B are pending
d.click();
await tick();
// B sees A's change (but not C's), so `d` is 10 in its world
resolve_b.click();
await tick();
assert.equal(logs[0], 'probe 10');
}
});

@ -0,0 +1,44 @@
<script>
let s = $state(0);
let u = $state(0);
let w = $state(0);
let show = $state(true);
let d = $derived(s * 10);
const pending = new Map();
function delay(name, value) {
if (!value) return value;
return new Promise((resolve) => pending.set(name, () => resolve(value)));
}
function probe(value) {
console.log(`probe ${value}`);
return value;
}
</script>
<button onclick={() => s++}>s</button>
<button onclick={() => u++}>u</button>
<button
onclick={() => {
s++;
d;
w++;
}}>d</button
>
<button onclick={() => pending.get('a')?.()}>resolve a</button>
<button onclick={() => pending.get('b')?.()}>resolve b</button>
<button onclick={() => pending.get('c')?.()}>resolve c</button>
<p>{await delay('a', s)}</p>
<p>{await delay('c', w)}</p>
<!-- inside a branch, so that B's traversal doesn't reach (and stash) the effect reading `d` -->
{#if show}
<!-- reads `s` directly, so the effect is DIRTY, so even if we were to eagerly do is_dirty in batch A it wouldn't evaluate `d` -->
<span>{s}-{d}</span>
{/if}
{#if await delay('b', u)}
<i>{await probe(d)}</i>
{/if}

@ -0,0 +1,31 @@
import { tick } from 'svelte';
import { test } from '../../test';
// Like `async-derived-assign-keeps-maybe-dirty`, but C reads `d` in the event handler instead of
// assigning to it. That records `d`'s stale value (A never evaluated it) as C's previous value, and
// must not mark `d` as clean: B never stashed a reaction that reads `d`, so it wouldn't re-validate
// it and would use that stale value
export default test({
mode: ['client'],
async test({ assert, target, logs }) {
await tick();
const [s, u, d, , resolve_b] = target.querySelectorAll('button');
// batch A: `d` becomes 10, but is not evaluated yet (the `<span>` is deferred)
s.click();
await tick();
// batch B
u.click();
await tick();
// batch C writes `s` and reads `d` (20) while A and B are pending
d.click();
await tick();
// B sees A's change (but not C's), so `d` is 10 in its world
resolve_b.click();
await tick();
assert.equal(logs[0], 'probe 10');
}
});

@ -0,0 +1,43 @@
<script>
let s = $state(0);
let u = $state(0);
let w = $state(0);
let show = $state(true);
let d = $derived(s * 10);
const pending = new Map();
function delay(name, value) {
if (!value) return value;
return new Promise((resolve) => pending.set(name, () => resolve(value)));
}
function probe(value) {
console.log(`probe ${value}`);
return value;
}
</script>
<button onclick={() => s++}>s</button>
<button onclick={() => u++}>u</button>
<button
onclick={() => {
s++;
d;
w++;
}}>d</button
>
<button onclick={() => pending.get('a')?.()}>resolve a</button>
<button onclick={() => pending.get('b')?.()}>resolve b</button>
<button onclick={() => pending.get('c')?.()}>resolve c</button>
<p>{await delay('a', s)}</p>
<p>{await delay('c', w)}</p>
<!-- inside a branch, so that B's traversal doesn't reach (and stash) the effect reading `d` -->
{#if show}
<span>{d}</span>
{/if}
{#if await delay('b', u)}
<i>{await probe(d)}</i>
{/if}
Loading…
Cancel
Save