avoid breakage at the cost of showing latest value early (let's see if we can avoid some of this later)

entangle-batches-3
Simon Holthausen 2 weeks ago
parent 8e98c03e25
commit 5839bd89a9
No known key found for this signature in database

@ -759,17 +759,55 @@ export function get(signal) {
// we saw turns out to differ from the committed one)
var override_owner = override[1];
if (override_owner !== null && active_reaction !== null && !untracking) {
override_owner = override_owner.resolved();
if (override_owner !== null) {
if (active_reaction === null) {
// reads outside a reaction during a flush happen in one-shot init
// code (e.g. a component initialising inside a newly-created
// branch). They have no dependency history and no re-run
// mechanism, so they see the latest value
if ((signal.f & DERIVED) === 0) {
if ((signal.f & ERROR_VALUE) !== 0) {
throw signal.v;
}
return signal.v;
}
} else if (!untracking) {
override_owner = override_owner.resolved();
var readers = (override_owner.stale_readers ??= new Map());
var seen = readers.get(active_reaction);
var readers = (override_owner.stale_readers ??= new Map());
var seen = readers.get(active_reaction);
if (seen === undefined) {
readers.set(active_reaction, (seen = new Map()));
}
if (seen === undefined) {
readers.set(active_reaction, (seen = new Map()));
}
// a reader keeps seeing the value it first observed while the owner is pending
if (seen.has(signal)) {
return seen.get(signal);
}
seen.set(signal, override[0]);
var override_value = override[0];
if (
(active_reaction.f & REACTION_IS_UPDATING) !== 0 &&
(signal.f & DERIVED) === 0 &&
(active_reaction.deps === null || !includes.call(active_reaction.deps, signal))
) {
// the reaction never depended on this signal before the owner's write —
// the pre-write world never contained this combination of values,
// so read the latest value instead
if ((signal.f & ERROR_VALUE) !== 0) {
throw signal.v;
}
override_value = signal.v;
}
seen.set(signal, override_value);
return override_value;
}
}
return override[0];

@ -17,17 +17,17 @@ export default test({
await tick();
assert.deepEqual(logs, []);
// an independent batch runs the effect, which reads `x` through the
// pending batch's overlay (seeing the held-back value 0)
// an independent batch runs the effect, which newly depends on `x` —
// it reads the latest value (1) rather than the held-back one (0)
y.click();
await tick();
assert.deepEqual(logs, ['effect 0 1']);
assert.deepEqual(logs, ['effect 1 1']);
// the pending batch settles and commits x === 1 — the effect saw a
// stale value and must re-run with the real one
// the pending batch settles and commits x === 1 — exactly the value
// the effect already saw, so it should not re-run
shift.click();
await tick();
assert.deepEqual(logs, ['effect 0 1', 'effect 1 1']);
assert.deepEqual(logs, ['effect 1 1']);
assert.htmlEqual(
target.innerHTML,
'<p>1</p><button>x</button><button>y</button><button>shift</button>'

@ -13,22 +13,22 @@ export default test({
await tick();
assert.deepEqual(logs, ['effect _ 0']);
// the effect reads `x` through the pending batch's overlay
// (seeing the held-back value 0)
// the effect newly depends on `x` — it reads the latest value (1)
// rather than the held-back one (0)
y.click();
await tick();
assert.deepEqual(logs, ['effect _ 0', 'effect 0 1']);
assert.deepEqual(logs, ['effect _ 0', 'effect 1 1']);
// the effect re-runs and no longer depends on `x` at all
y.click();
await tick();
assert.deepEqual(logs, ['effect _ 0', 'effect 0 1', 'effect _ 2']);
assert.deepEqual(logs, ['effect _ 0', 'effect 1 1', 'effect _ 2']);
// the pending batch settles and commits x = 1 — the effect no longer
// depends on `x`, so it should not re-run
shift.click();
await tick();
assert.deepEqual(logs, ['effect _ 0', 'effect 0 1', 'effect _ 2']);
assert.deepEqual(logs, ['effect _ 0', 'effect 1 1', 'effect _ 2']);
assert.htmlEqual(
target.innerHTML,
'<p>1</p><button>x</button><button>y</button><button>shift</button>'

@ -10,26 +10,23 @@ export default test({
y.click();
await tick();
// the new branch's reactive reads of `x` are new dependencies on a value the
// pending batch has written, so they see the latest value ('universe')
assert.htmlEqual(
target.innerHTML,
`
<button>x</button>
<button>y++</button>
<button>resolve</button>
world
` // if this does not show world - that would also be ok
universe
`
);
resolve.click();
await tick();
assert.deepEqual(logs, [
'universe',
'world',
'$effect: world',
'$effect: universe',
'$effect: universe'
]);
// assert.deepEqual(logs, ['universe', 'universe', '$effect: universe', '$effect: universe']); // this would also be ok
// the init-time console.log runs outside a reaction and sees the latest value;
// the second child's $effect already saw the committed value, so it does not re-run
assert.deepEqual(logs, ['universe', 'universe', '$effect: universe', '$effect: universe']);
assert.htmlEqual(
target.innerHTML,
`

@ -10,6 +10,8 @@ export default test({
y.click();
await tick();
// the new branch's reads of `x` are new dependencies on a value the
// pending batch has written, so they see the latest value ('universe')
assert.htmlEqual(
target.innerHTML,
`
@ -17,13 +19,13 @@ export default test({
<button>y++</button>
<button>resolve</button>
<hr>
world
"world"
world
world
world
"world"
` // if this does not show world "world" world world world "world" - then this would also be ok
universe
"universe"
universe
universe
universe
"universe"
`
);
resolve.click();

@ -22,6 +22,8 @@ export default test({
resolve.click();
await tick();
// the new branch's async expression read `x` as a new dependency on a value
// the pending batch had written, so it saw the latest value ('universe')
assert.htmlEqual(
target.innerHTML,
`
@ -29,13 +31,13 @@ export default test({
<button>y++</button>
<button>resolve</button>
<hr>
world
"world"
world
world
world
"world"
` // if this does not show world "world" world world world "world" - then this would also be ok
universe
"universe"
universe
universe
universe
"universe"
`
);
resolve.click();

@ -0,0 +1,7 @@
<script>
let { x } = $props();
console.log(x.x);
$effect(() => console.log('$effect: '+ x.x))
</script>
{x.x}

@ -0,0 +1,44 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
const [x, y, resolve] = target.querySelectorAll('button');
x.click();
await tick();
assert.deepEqual(logs, ['universe']);
y.click();
await tick();
// the new branch's reads of `x` are new dependencies on a value the
// pending batch has written, so they see the latest value ('universe') —
// including the init-time console.log, which runs outside a reaction
assert.deepEqual(logs, ['universe', 'universe', '$effect: universe']);
assert.htmlEqual(
target.innerHTML,
`
<button>x</button>
<button>y++</button>
<button>resolve</button>
universe
`
);
resolve.click();
await tick();
// the second child's $effect already saw the committed value, so it does not re-run
assert.deepEqual(logs, ['universe', 'universe', '$effect: universe', '$effect: universe']);
assert.htmlEqual(
target.innerHTML,
`
<button>x</button>
<button>y++</button>
<button>resolve</button>
universe
universe
universe
`
);
}
});

@ -0,0 +1,28 @@
<script>
import Child from './Child.svelte';
let x = $state();
let y = $state(0);
let deferred = [];
function delay(s) {
const d = Promise.withResolvers();
deferred.push(() => d.resolve(s))
return d.promise;
}
</script>
<button onclick={() => x = {x:'universe'}}>x</button>
<button onclick={() => y++}>y++</button>
<button onclick={() => deferred.shift()()}>resolve</button>
{#if x?.x === 'universe'}
{await delay(x.x)}
<Child {x} />
{/if}
{#if y > 0}
<Child {x} />
{/if}

@ -11,26 +11,24 @@ export default test({
y.click();
await tick();
assert.deepEqual(logs, ['universe', 'world', '$effect: world']);
// the new branch's reads of `x` are new dependencies on a value the
// pending batch has written, so they see the latest value ('universe') —
// including the init-time console.log, which runs outside a reaction
assert.deepEqual(logs, ['universe', 'universe', '$effect: universe']);
assert.htmlEqual(
target.innerHTML,
`
<button>x</button>
<button>y++</button>
<button>resolve</button>
world
universe
`
);
resolve.click();
await tick();
assert.deepEqual(logs, [
'universe',
'world',
'$effect: world',
'$effect: universe',
'$effect: universe'
]);
// the second child's $effect already saw the committed value, so it does not re-run
assert.deepEqual(logs, ['universe', 'universe', '$effect: universe', '$effect: universe']);
assert.htmlEqual(
target.innerHTML,
`

@ -0,0 +1,41 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client'],
async test({ assert, target }) {
await tick();
const [update, show, resolve] = target.querySelectorAll('button');
update.click();
await tick();
show.click();
await tick();
// the template effect newly depends on `value`, which the pending batch
// has written — it reads the latest value rather than the pre-write one
assert.htmlEqual(
target.innerHTML,
`
<button>update</button>
<button>show</button>
<button>resolve</button>
<p>1</p>
`
);
resolve.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>update</button>
<button>show</button>
<button>resolve</button>
<p>1</p>
`
);
}
});

@ -0,0 +1,17 @@
<script>
let value = $state();
let show = $state(false);
const deferred = Promise.withResolvers();
function wait(value) {
return value === undefined ? '' : deferred.promise;
}
</script>
<button onclick={() => (value = { x: 1 })}>update</button>
<button onclick={() => (show = true)}>show</button>
<button onclick={() => deferred.resolve('')}>resolve</button>
{await wait(value)}
<p>{show ? value.x : ''}</p>
Loading…
Cancel
Save