tweak equality comparisons; keep dependencies not only for forks, and not only reactions but also deps

async-another-try-pt-3
Simon Holthausen 10 hours ago
parent 61f58a5566
commit d654c969d3
No known key found for this signature in database

@ -119,8 +119,17 @@ export function read_batch_local_value(reaction) {
* @returns {{ v: any }} * @returns {{ v: any }}
*/ */
export function own_value(signal) { export function own_value(signal) {
var own = batch_values === null ? undefined : current_batch?.current.get(signal); if (batch_values === null) return signal;
return own === undefined ? signal : own;
var own = current_batch?.current.get(signal);
if (own !== undefined) return own;
// Outside of effect flushes, the view is the one of the current batch: it may show another value, e.g. a later
// batch's previous one. While effects are flushed, the view is the one of the flushed batch, but writes belong
// to a new batch (and deriveds are captured into both)
return previous_batch === null && batch_values.has(signal)
? { v: batch_values.get(signal) }
: signal;
} }
/** @type {Effect | null} */ /** @type {Effect | null} */

@ -427,7 +427,7 @@ export function update_derived(derived) {
var value = execute_derived(derived); var value = execute_derived(derived);
var d = own_value(derived); var d = own_value(derived);
if (d.v === derived.v ? !derived.equals(value) : !derived.equals.call(d, value)) { if (d === derived ? !derived.equals(value) : !derived.equals.call(d, value)) {
if (current_batch !== null || previous_batch !== null) { if (current_batch !== null || previous_batch !== null) {
// `capture` decides whether the underlying value is updated (it isn't in a fork, // `capture` decides whether the underlying value is updated (it isn't in a fork,
// or if a later batch holds a newer value) and records it in the batch either way. // or if a later batch holds a newer value) and records it in the batch either way.
@ -436,9 +436,8 @@ export function update_derived(derived) {
// to the previous batch, not the new one (which can already exist if an earlier // to the previous batch, not the new one (which can already exist if an earlier
// effect wrote to a source). Not adding it to either can cause bugs, so we add it to both. // effect wrote to a source). Not adding it to either can cause bugs, so we add it to both.
// See https://github.com/sveltejs/svelte/pull/18117 for more details. // See https://github.com/sveltejs/svelte/pull/18117 for more details.
// Order matters: current_batch first so it gets the correct previous value. previous_batch?.capture(derived, value, true);
current_batch?.capture(derived, value, true); current_batch?.capture(derived, value, true);
previous_batch?.capture(derived, value, true, current_batch !== null);
} else { } else {
derived.v = value; derived.v = value;
derived.wv = increment_write_version(); derived.wv = increment_write_version();

@ -191,7 +191,8 @@ var count_deps = 0;
*/ */
export function internal_set(source, value, updated_during_traversal = null) { export function internal_set(source, value, updated_during_traversal = null) {
var s = own_value(source); var s = own_value(source);
if (s.v === source.v ? !source.equals(value) : !source.equals.call(s, value)) {
if (s === source ? !source.equals(value) : !source.equals.call(s, value)) {
if (is_destroying_effect) { if (is_destroying_effect) {
old_values.set(source, value); old_values.set(source, value);
} else if (!old_values.has(source)) { } else if (!old_values.has(source)) {

@ -52,6 +52,7 @@ import {
Batch, Batch,
batch_values, batch_values,
current_batch, current_batch,
first_batch,
flushSync, flushSync,
held_sources, held_sources,
previous_batch, previous_batch,
@ -342,17 +343,17 @@ export function update_reaction(reaction) {
function update_dependencies(reaction) { function update_dependencies(reaction) {
var deps = reaction.deps; var deps = reaction.deps;
// Don't remove reactions during fork;
// they must remain for when fork is discarded
var is_fork = current_batch?.is_fork;
if (new_deps !== null) { if (new_deps !== null) {
var i; var i;
if (!is_fork) { if (deps !== null && skipped_deps < deps.length && keep_dependencies(deps)) {
remove_reactions(reaction, skipped_deps); for (i = skipped_deps; i < deps.length; i++) {
if (!includes.call(new_deps, deps[i])) new_deps.push(deps[i]);
}
} }
remove_reactions(reaction, skipped_deps);
if (deps !== null && skipped_deps > 0) { if (deps !== null && skipped_deps > 0) {
deps.length = skipped_deps + new_deps.length; deps.length = skipped_deps + new_deps.length;
for (i = 0; i < new_deps.length; i++) { for (i = 0; i < new_deps.length; i++) {
@ -367,7 +368,7 @@ function update_dependencies(reaction) {
(deps[i].reactions ??= []).push(reaction); (deps[i].reactions ??= []).push(reaction);
} }
} }
} else if (!is_fork && deps !== null && skipped_deps < deps.length) { } else if (deps !== null && skipped_deps < deps.length && !keep_dependencies(deps)) {
remove_reactions(reaction, skipped_deps); remove_reactions(reaction, skipped_deps);
deps.length = skipped_deps; deps.length = skipped_deps;
} }
@ -375,6 +376,31 @@ function update_dependencies(reaction) {
return deps; return deps;
} }
/**
* Whether a reaction should keep the dependencies it didn't read this time. While several batches exist (or in
* a fork), they're needed if it read a value that some batch changes: other batches (or the real world, once the
* fork is discarded) see another value for it, so the reaction may take another path there (e.g. `a || b` with a
* different `a`), in which it depends on them
* @param {Value[]} deps the previous dependencies, of which the first `skipped_deps` were read again
*/
function keep_dependencies(deps) {
if (batch_values === null) return false;
for (var batch = first_batch; batch !== null; batch = batch.next) {
for (var i = 0; i < skipped_deps; i++) {
if (batch.current.has(deps[i])) return true;
}
if (new_deps !== null) {
for (i = 0; i < new_deps.length; i++) {
if (batch.current.has(new_deps[i])) return true;
}
}
}
return false;
}
/** /**
* @template V * @template V
* @param {Reaction} signal * @param {Reaction} signal

@ -0,0 +1,40 @@
import { tick } from 'svelte';
import { test } from '../../test';
const buttons =
'<button>b = 1</button><button>b = 2, c = 1</button><button>resolve latest pos</button><button>resolve oldest pos</button><button>resolve all</button>';
// Like `async-equal-result-dropped-later-batch`, but the result is read by something that doesn't wait
// for it: a batch's async result equals the real value, which a later (still pending) batch wrote. The
// batch itself sees that later batch's previous value though, so the result changes its world, and
// whatever read the previous value has to update
export default test({
mode: ['client'],
async test({ assert, target }) {
await tick();
const [b1, b2c1, resolve_latest, resolve_oldest, resolve_all] =
target.querySelectorAll('button');
assert.htmlEqual(target.innerHTML, `${buttons}<p>zero</p><span>0</span><i>c0</i>`);
b1.click();
await tick();
b2c1.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<p>zero</p><span>0</span><i>c0</i>`);
// the later batch's result becomes the real value, but that batch still waits for c1
resolve_latest.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<p>zero</p><span>0</span><i>c0</i>`);
// the earlier batch's equal result completes it
resolve_oldest.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<p>pos</p><span>1</span><i>c0</i>`);
resolve_all.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<p>pos</p><span>2</span><i>c1</i>`);
}
});

@ -0,0 +1,39 @@
<script>
let b = $state(0);
let c = $state(0);
/** @type {Map<string, Array<() => void>>} */
const pending = new Map();
let initial = true;
/** @param {string} value */
function f(value) {
if (initial) return value;
return new Promise((fulfil) => {
if (!pending.has(value)) pending.set(value, []);
pending.get(value)?.push(() => fulfil(value));
});
}
$effect(() => {
initial = false;
});
function resolve_all() {
for (const list of pending.values()) {
for (const fn of list.splice(0)) fn();
}
}
let p = $derived(await f(b > 0 ? 'pos' : 'zero'));
</script>
<button onclick={() => (b = 1)}>b = 1</button>
<button onclick={() => ((b = 2), (c = 1))}>b = 2, c = 1</button>
<button onclick={() => pending.get('pos')?.pop()?.()}>resolve latest pos</button>
<button onclick={() => pending.get('pos')?.shift()?.()}>resolve oldest pos</button>
<button onclick={resolve_all}>resolve all</button>
<p>{p}</p>
{#if true}<span>{b}</span>{/if}
<i>{await f('c' + c)}</i>
Loading…
Cancel
Save