bring over some tests from entangle batch PR

async-another-try
Simon Holthausen 1 week ago
parent 4bdf9e3db4
commit 762e5cb6d8
No known key found for this signature in database

@ -560,8 +560,6 @@ export function get(signal) {
// we don't add the dependency, because that would create a memory leak
var destroyed = active_effect !== null && (active_effect.f & DESTROYED) !== 0;
first_time = (active_reaction.f & REACTION_RAN) === 0;
if (!destroyed && (current_sources === null || !current_sources.has(signal))) {
var deps = active_reaction.deps;
@ -577,8 +575,10 @@ export function get(signal) {
skipped_deps++;
} else if (new_deps === null) {
new_deps = [signal];
first_time = true;
} else {
new_deps.push(signal);
first_time = true;
}
}
} else {
@ -590,6 +590,7 @@ export function get(signal) {
active_reaction.deps ??= [];
if (!includes.call(active_reaction.deps, signal)) {
active_reaction.deps.push(signal);
first_time = true;
}
var reactions = signal.reactions;

@ -0,0 +1,36 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
await tick();
const [x, y, shift] = target.querySelectorAll('button');
assert.htmlEqual(
target.innerHTML,
'<p>0</p><button>x</button><button>y</button><button>shift</button>'
);
assert.deepEqual(logs, []);
// write x — the batch is pending on its async expression
x.click();
await tick();
assert.deepEqual(logs, []);
// 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 1 1']);
// 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 1 1']);
assert.htmlEqual(
target.innerHTML,
'<p>1</p><button>x</button><button>y</button><button>shift</button>'
);
}
});

@ -0,0 +1,23 @@
<script>
let x = $state(0);
let y = $state(0);
let pend = false;
const deferred = [];
function delay(value) {
if (!pend) return value;
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
$effect(() => {
if (y > 0) {
console.log(`effect ${x} ${y}`);
}
});
</script>
<p>{await delay(x)}</p>
<button onclick={() => { pend = true; x += 1; }}>x</button>
<button onclick={() => y++}>y</button>
<button onclick={() => deferred.shift()?.()}>shift</button>

@ -0,0 +1,33 @@
import { tick } from 'svelte';
import { test } from '../../test';
const buttons = '<button>x</button><button>show</button><button>shift</button>';
export default test({
async test({ assert, target, logs }) {
await tick();
const [x, show, shift] = target.querySelectorAll('button');
assert.htmlEqual(target.innerHTML, `<p>true</p><p>1</p>${buttons}`);
assert.deepEqual(logs, []);
// write x — the batch is pending on its async expression, and claims
// the `positive` derived (marked through x)
x.click();
await tick();
assert.deepEqual(logs, []);
// an independent batch runs the effect, which reads the claimed derived
// through the pending batch's overlay (x = 1, so `positive` is true)
show.click();
await tick();
assert.deepEqual(logs, ['positive true']);
// the pending batch settles and commits x = 2 — `positive` recomputes
// to the same value (true), so the effect should not re-run
shift.click();
await tick();
assert.deepEqual(logs, ['positive true']);
assert.htmlEqual(target.innerHTML, `<p>true</p><p>2</p>${buttons}`);
}
});

@ -0,0 +1,26 @@
<script>
let x = $state(1);
let show = $state(false);
let pend = false;
const deferred = [];
let positive = $derived(x > 0);
function delay(value) {
if (!pend) return value;
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
$effect(() => {
if (show) {
console.log(`positive ${positive}`);
}
});
</script>
<p>{positive}</p>
<p>{await delay(x)}</p>
<button onclick={() => { pend = true; x += 1; }}>x</button>
<button onclick={() => (show = true)}>show</button>
<button onclick={() => deferred.shift()?.()}>shift</button>

@ -0,0 +1,37 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
await tick();
const [x, y, shift] = target.querySelectorAll('button');
assert.deepEqual(logs, ['effect _ 0']);
// write x — the batch is pending on its async expression
x.click();
await tick();
assert.deepEqual(logs, ['effect _ 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 1 1']);
// the effect re-runs and no longer depends on `x` at all
y.click();
await tick();
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 1 1', 'effect _ 2']);
assert.htmlEqual(
target.innerHTML,
'<p>1</p><button>x</button><button>y</button><button>shift</button>'
);
}
});

@ -0,0 +1,24 @@
<script>
let x = $state(0);
let y = $state(0);
const deferred = [];
function delay(value) {
if (!value) return value;
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
$effect(() => {
if (y === 1) {
console.log(`effect ${x} ${y}`);
} else {
console.log(`effect _ ${y}`);
}
});
</script>
<p>{await delay(x)}</p>
<button onclick={() => x++}>x</button>
<button onclick={() => y++}>y</button>
<button onclick={() => deferred.shift()?.()}>shift</button>

@ -0,0 +1,33 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
await tick();
const [revert, y, shift] = target.querySelectorAll('button');
assert.htmlEqual(
target.innerHTML,
'<p>0</p><button>revert</button><button>y</button><button>shift</button>'
);
assert.deepEqual(logs, []);
// write x and revert it within the same batch — the batch is pending
// (its async expression re-runs), with previous === current for `x`
revert.click();
await tick();
assert.deepEqual(logs, []);
// an independent batch runs the effect, which now reads `x` through
// the pending batch's overlay (seeing the held-back value 0)
y.click();
await tick();
assert.deepEqual(logs, ['effect 0 1']);
// the pending batch settles and commits x === 0, i.e. exactly the value
// the effect already saw — it should not re-run
shift.click();
await tick();
assert.deepEqual(logs, ['effect 0 1']);
}
});

@ -0,0 +1,23 @@
<script>
let x = $state(0);
let y = $state(0);
let pend = false;
const deferred = [];
function delay(value) {
if (!pend) return value;
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
$effect(() => {
if (y > 0) {
console.log(`effect ${x} ${y}`);
}
});
</script>
<p>{await delay(x)}</p>
<button onclick={() => { pend = true; x += 1; x -= 1; }}>revert</button>
<button onclick={() => y++}>y</button>
<button onclick={() => deferred.shift()?.()}>shift</button>

@ -0,0 +1,16 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [increment] = target.querySelectorAll('button');
assert.htmlEqual(target.innerHTML, '<button>increment</button><p>0</p>');
increment.click();
await tick();
assert.htmlEqual(target.innerHTML, '<button>increment</button><p>2</p>');
}
});

@ -0,0 +1,14 @@
<script>
let count = $state(0);
let runs = 0;
function delay(value, eager_value) {
if (runs++ > 10) throw new Error('infinite loop detected')
if (value === 0) return 0;
return Promise.resolve(value + eager_value);
}
</script>
<button onclick={() => count += 1}>increment</button>
<p>{await delay(count, $state.eager(count))}</p>

@ -0,0 +1,36 @@
import { tick } from 'svelte';
import { test } from '../../test';
const buttons = '<button>count</button><button>eager</button><button>shift</button>';
export default test({
// running more than once per bump
async test({ assert, target, logs }) {
await tick();
const [count, eager, shift] = target.querySelectorAll('button');
assert.htmlEqual(target.innerHTML, `<p>-1</p>${buttons}`);
assert.deepEqual(logs, []);
// count++ creates a pending batch; the inner block (containing the
// $state.eager expression) is created inside it and belongs to it.
// it does not depend on `count`
count.click();
await tick();
assert.deepEqual(logs, ['inner 0']);
// an eager version bump re-runs the inner block in the eager batch's
// world — since the block doesn't depend on the pending batch's
// changes, it sees exactly the same values the owner's world would
eager.click();
await tick();
assert.deepEqual(logs, ['inner 0', 'inner 1']);
// the pending batch settles — nothing the inner block sees has
// changed since its eager run, so it should not be re-run
shift.click();
await tick();
assert.deepEqual(logs, ['inner 0', 'inner 1']);
assert.htmlEqual(target.innerHTML, `<p>0</p><span>1</span>${buttons}`);
}
});

@ -0,0 +1,26 @@
<script>
let count = $state(-1);
let eag = $state(0);
const deferred = [];
function delay(value) {
if (value < 0) return value;
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
function log(value) {
console.log(`inner ${value}`);
return value >= 0;
}
</script>
<p>{await delay(count)}</p>
{#if count >= 0}
{#if log($state.eager(eag))}
<span>{eag}</span>
{/if}
{/if}
<button onclick={() => count++}>count</button>
<button onclick={() => eag++}>eager</button>
<button onclick={() => deferred.shift()?.()}>shift</button>

@ -0,0 +1,41 @@
import { tick } from 'svelte';
import { test } from '../../test';
const buttons = `
<button>fork</button>
<button>update</button>
<button>resolve</button>
<button>discard</button>
`;
export default test({
async test({ assert, target, instance }) {
const [fork_button, update, resolve, discard] = target.querySelectorAll('button');
fork_button.click();
await tick();
assert.equal(instance.get_calls(), 1);
assert.htmlEqual(target.innerHTML, buttons);
// Transfer an invalidation into the fork while its async work is pending.
update.click();
await tick();
assert.equal(instance.get_calls(), 2); // can also be 1 at this point already, would also be ok
try {
resolve.click();
await tick();
assert.equal(instance.get_calls(), 2);
// Completing the replacement must not replay the same invalidation.
resolve.click();
await tick();
assert.equal(instance.get_calls(), 2);
} finally {
discard.click();
await tick();
}
assert.htmlEqual(target.innerHTML, buttons);
}
});

@ -0,0 +1,42 @@
<script>
import { fork } from 'svelte';
let sharedState = $state(0);
let show = $state(false);
let f;
let calls = 0;
const resolvers = [];
// Only evaluated in the fork, with a fresh object on each evaluation.
const searchParams = $derived({ value: sharedState });
export function get_calls() {
return calls;
}
function load(value) {
calls += 1;
return new Promise((resolve) => resolvers.push(() => resolve(value)));
}
</script>
<button
onclick={() => {
f = fork(() => {
sharedState = 1;
show = true;
});
}}
>fork</button
>
<button onclick={() => sharedState++}>update</button>
<button onclick={() => resolvers.shift()?.()}>resolve</button>
<button onclick={() => f?.discard()}>discard</button>
{#if show}
<svelte:boundary>
{#snippet pending()}loading{/snippet}
<p>{await load(searchParams.value)}</p>
</svelte:boundary>
{/if}

@ -0,0 +1,45 @@
import { flushSync, tick } from 'svelte';
import { test } from '../../test';
const buttons = `
<button>preload</button>
<button>increment</button>
<button>commit</button>
<button>reveal</button>
<button>discard</button>
<button>preload second</button>
<button>commit second</button>
<button>reset</button>
`;
export default test({
async test({ assert, target }) {
const [preload, increment, commit, reveal, discard, preload_second, commit_second, reset] =
target.querySelectorAll('button');
for (const mode of ['commit', 'reveal', 'second-fork']) {
preload.click();
flushSync(() => increment.click());
assert.htmlEqual(target.innerHTML, buttons);
if (mode === 'commit') {
commit.click();
await tick();
} else if (mode === 'reveal') {
flushSync(() => reveal.click());
discard.click();
} else {
preload_second.click();
discard.click();
commit_second.click();
await tick();
}
assert.htmlEqual(target.innerHTML, `${buttons}<p>1 2</p>`);
flushSync(() => increment.click());
assert.htmlEqual(target.innerHTML, `${buttons}<p>2 4</p>`);
flushSync(() => reset.click());
assert.htmlEqual(target.innerHTML, buttons);
}
}
});

@ -0,0 +1,22 @@
<script>
import { fork } from 'svelte';
let show = $state(false);
let count = $state(0);
let doubled = $derived(count * 2);
let f;
let other;
</script>
<button onclick={() => (f = fork(() => (show = true)))}>preload</button>
<button onclick={() => count++}>increment</button>
<button onclick={() => f.commit()}>commit</button>
<button onclick={() => (show = true)}>reveal</button>
<button onclick={() => f.discard()}>discard</button>
<button onclick={() => (other = fork(() => (show = true)))}>preload second</button>
<button onclick={() => other.commit()}>commit second</button>
<button onclick={() => { show = false; count = 0; }}>reset</button>
{#if show}
<p>{count} {doubled}</p>
{/if}

@ -0,0 +1,43 @@
import { tick } from 'svelte';
import { test } from '../../test';
const buttons =
'<button>fork</button><button>y</button><button>shift</button><button>commit</button>';
export default test({
async test({ assert, target }) {
await tick();
const [forkButton, y, shift, commit] = target.querySelectorAll('button');
assert.htmlEqual(target.innerHTML, `<p>0</p>${buttons}`);
// speculative world: x becomes 1, async expression runs with x + y = 1
forkButton.click();
await tick();
assert.htmlEqual(target.innerHTML, `<p>0</p>${buttons}`);
// real world: y becomes 1, async expression runs with x + y = 1
// (computed with the pre-fork x = 0) — this run supersedes the
// fork's validation of the effect
y.click();
await tick();
assert.htmlEqual(target.innerHTML, `<p>0</p>${buttons}`);
// commit the fork while the real batch is still pending — x = 1 is
// written, and the async expression must eventually re-run with the
// committed value, because its in-flight run used x = 0
commit.click();
await tick();
assert.htmlEqual(target.innerHTML, `<p>0</p>${buttons}`);
// resolve all in-flight runs (superseded ones are no-ops)
for (let i = 0; i < 4; i += 1) {
shift.click();
await tick();
}
// x = 1, y = 1 — anything else means the effect resolved with a value
// computed from stale inputs and was never re-run
assert.htmlEqual(target.innerHTML, `<p>2</p>${buttons}`);
}
});

@ -0,0 +1,20 @@
<script>
import { fork } from 'svelte';
let x = $state(0);
let y = $state(0);
let f;
const deferred = [];
function delay(value) {
if (!value) return value;
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
</script>
<p>{await delay(x + y)}</p>
<button onclick={() => { f = fork(() => x++); }}>fork</button>
<button onclick={() => y++}>y</button>
<button onclick={() => deferred.shift()?.()}>shift</button>
<button onclick={() => f.commit()}>commit</button>

@ -0,0 +1,34 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
await tick();
const [x, y, shift, pop, commit] = target.querySelectorAll('button');
const [p] = target.querySelectorAll('p');
logs.length = 0;
y.click();
await tick();
assert.deepEqual(logs, ['called with 0,1']);
logs.length = 0;
x.click();
await tick();
assert.deepEqual(logs, ['called with 1,1']);
assert.htmlEqual(p.innerHTML, '0');
logs.length = 0;
shift.click();
await tick();
assert.deepEqual(logs, []);
assert.htmlEqual(p.innerHTML, '1');
commit.click();
await tick();
pop.click();
await tick();
assert.deepEqual(logs, []);
assert.htmlEqual(p.innerHTML, '2');
}
});

@ -0,0 +1,23 @@
<script>
import { fork } from 'svelte';
let x = $state(0);
let y = $state(0);
let f;
const deferred = [];
function delay(_, value) {
if (!value) return value;
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
</script>
<button onclick={() => {f = fork(() => x++)}}>x</button>
<button onclick={() => y++}>y</button>
<button onclick={() => deferred.shift()?.()}>shift</button>
<button onclick={() => deferred.pop()?.()}>pop</button>
<button onclick={() => f.commit()}>commit</button>
<p>{await delay(console.log('called with ' + x + ',' + y), x + y)}</p>

@ -0,0 +1,42 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
await tick();
const [forkButton, real, shift, discard] = target.querySelectorAll('button');
assert.deepEqual(logs, ['b 0']);
logs.length = 0;
// speculative world: fork writes b and c, runs the async expression
forkButton.click();
await tick();
assert.deepEqual(logs, ['b 1']);
// real world: b++ re-runs the async expression for real
real.click();
await tick();
assert.deepEqual(logs, ['b 1', 'b 1']);
// resolve the fork's in-flight run — the fork is still speculative,
// nothing should be committed or re-run
shift.click();
await tick();
assert.deepEqual(logs, ['b 1', 'b 1']);
// resolve the real run — the real batch commits b = 1. The fork's world
// value of `b` is also 1 (its own write, now also committed), so the
// fork's async expression sees unchanged inputs and should not re-run
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<p>1 0</p><button>fork</button><button>real</button><button>shift</button><button>discard</button>'
);
assert.deepEqual(logs, ['b 1', 'b 1']);
discard.click();
await tick();
}
});

@ -0,0 +1,20 @@
<script>
import { fork } from 'svelte';
let b = $state(0);
let c = $state(0);
let f;
const deferred = [];
function delay(_, value) {
if (!value) return value;
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
</script>
<p>{await delay(console.log(`b ${b}`), b)} {c}</p>
<button onclick={() => { f = fork(() => { b += 1; c += 1; }); }}>fork</button>
<button onclick={() => b++}>real</button>
<button onclick={() => deferred.shift()?.()}>shift</button>
<button onclick={() => f.discard()}>discard</button>

@ -0,0 +1,61 @@
import { tick } from 'svelte';
import { test } from '../../test';
const buttons = `
<button>up</button>
<button>down</button>
<button>show1</button>
<button>show2</button>
<button>shift a</button>
<button>shift t</button>
`;
export default test({
async test({ assert, target }) {
await tick();
const [up, down, show1, show2, shift_a, shift_t] = target.querySelectorAll('button');
// batch B: reveals boundary 1 -> commits with pending snippet, stays live
show1.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons} <p>loading 1...</p>`);
// batch C: reveals boundary 2 -> commits with pending snippet, stays live
show2.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons} <p>loading 1...</p> <p>loading 2...</p>`);
// batch A: writes a=1; the async-a effect is owned by C, so A merges with
// C and stays pending. B remains separate
up.click();
await tick();
// B's continuation first-reads `a`, which is overlaid by the pending
// merged batch. B has already committed its UI, so it cannot entangle —
// it reads the latest value (1) instead
shift_t.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons} <p>late read: 1</p> <p>loading 2...</p>`);
// revert `a` to 0 inside the pending batch — its eventual commit leaves
// `a` unchanged. The write re-runs the late reader (it acquired `a` as a
// dependency), so it re-awaits a fresh deferred
down.click();
await tick();
// resolve the pending batch's async-a runs -> it commits
shift_a.click();
await tick();
shift_a.click();
await tick();
shift_a.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons} <p>late read: 1</p> <p>async a: 0</p>`);
// resolve the late reader's re-run -> it converges on the committed value
shift_t.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons} <p>late read: 0</p> <p>async a: 0</p>`);
}
});

@ -0,0 +1,49 @@
<script>
let a = $state(0);
let show1 = $state(false);
let show2 = $state(false);
let deferreds = [];
function push(key, v) {
const d = Promise.withResolvers();
deferreds.push({ key, v, d });
return d.promise;
}
function shift(key, override) {
const i = deferreds.findIndex((d) => d.key === key);
if (i === -1) return;
const [{ v, d }] = deferreds.splice(i, 1);
d.resolve(override ?? v);
}
</script>
<button onclick={() => a++}>up</button>
<button onclick={() => a--}>down</button>
<button onclick={() => (show1 = true)}>show1</button>
<button onclick={() => (show2 = true)}>show2</button>
<button onclick={() => shift('a')}>shift a</button>
<button onclick={() => shift('t', 1)}>shift t</button>
{#if show1}
<svelte:boundary>
<!-- no state deps initially; reads `a` for the first time only when
the awaited value (controlled by the test) says so -->
<p>late read: {(await push('t', 0)) > 0 ? a : -1}</p>
{#snippet pending()}
<p>loading 1...</p>
{/snippet}
</svelte:boundary>
{/if}
{#if show2}
<svelte:boundary>
<p>async a: {await push('a', a)}</p>
{#snippet pending()}
<p>loading 2...</p>
{/snippet}
</svelte:boundary>
{/if}

@ -0,0 +1,30 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [a_b, b, resolve] = target.querySelectorAll('button');
a_b.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>a_b 0_0</button> <button>b 0</button> <button>resolve</button> 0'
);
b.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>a_b 0_0</button> <button>b 0</button> <button>resolve</button> 0'
);
resolve.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>a_b 1_2</button> <button>b 2</button> <button>resolve</button> 1'
);
}
});

@ -0,0 +1,18 @@
<script>
let a = $state(0);
let b = $state(0);
let deferreds = [];
const value = () => (`${a}_${b}`);
function push(value) {
if (!value) return value;
return new Promise((resolve) => {
deferreds.push(() => resolve(value));
});
}
</script>
<button onclick={() => {a++;b++}}>a_b {value()}</button>
<button onclick={() => (b++)}>b {b}</button>
<button onclick={() => (deferreds.shift()?.())}>resolve</button>
{await push(a)}

@ -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 reads `x`, which the pending batch has written, as a new
// dependency — the two batches entangle, so the new branch is held back
// until the async work completes. Its $effect is deferred, but the
// init-time console.log necessarily runs eagerly (with the latest value)
assert.deepEqual(logs, ['universe', 'universe']);
assert.htmlEqual(
target.innerHTML,
`
<button>x</button>
<button>y++</button>
<button>resolve</button>
`
);
resolve.click();
await tick();
// both branches commit together, fully consistent
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}

@ -0,0 +1,51 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const [x, y, resolve] = target.querySelectorAll('button');
x.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>x</button>
<button>y++</button>
<button>resolve</button>
<h1>WORLD</h1>
`
);
y.click();
await tick();
// the new branch reads `upper` — a derived owned by the pending batch — as
// a new dependency. The two batches entangle, so the new branch is held
// back until the async work completes (rather than rendering with the
// derived's pre-write value, 'WORLD')
assert.htmlEqual(
target.innerHTML,
`
<button>x</button>
<button>y++</button>
<button>resolve</button>
<h1>WORLD</h1>
`
);
resolve.click();
await tick();
// both branches commit together, fully consistent
assert.htmlEqual(
target.innerHTML,
`
<button>x</button>
<button>y++</button>
<button>resolve</button>
<h1>UNIVERSE</h1>
universe
<p>UNIVERSE</p>
`
);
}
});

@ -0,0 +1,29 @@
<script>
let x = $state({ x: 'world' });
let y = $state(0);
let deferred = [];
const upper = $derived(x.x.toUpperCase());
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>
<h1>{upper}</h1>
{#if x.x === 'universe'}
{await delay(x.x)}
{/if}
{#if y > 0}
<p>{upper}</p>
{/if}

@ -0,0 +1,63 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const [a, t, shift_a, shift_t] = target.querySelectorAll('button');
shift_a.click();
shift_t.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>a</button>
<button>t</button>
<button>shift a</button>
<button>shift t</button>
<p>async a: 0</p>
<p>late read: -1</p>
`
);
// batch A: writes `a`, stays pending (its promise is unresolved)
a.click();
await tick();
// batch B: writes `t`; resolve its promise so the continuation
// reads `a` for the first time while A is still pending. This
// entangles B with A — both worlds are held back and commit together
t.click();
await tick();
shift_t.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>a</button>
<button>t</button>
<button>shift a</button>
<button>shift t</button>
<p>async a: 0</p>
<p>late read: -1</p>
`
);
// commit the merged batch
shift_a.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>a</button>
<button>t</button>
<button>shift a</button>
<button>shift t</button>
<p>async a: 1</p>
<p>late read: 1</p>
`
);
}
});

@ -0,0 +1,35 @@
<script>
let a = $state(0);
let t = $state(0);
let deferreds = [];
function push(key, v) {
const d = Promise.withResolvers();
deferreds.push({ key, v, d });
return d.promise;
}
function shift(key) {
const i = deferreds.findIndex((d) => d.key === key);
if (i === -1) return;
const [{ v, d }] = deferreds.splice(i, 1);
d.resolve(v);
}
</script>
<button onclick={() => a++}>a</button>
<button onclick={() => t++}>t</button>
<button onclick={() => shift('a')}>shift a</button>
<button onclick={() => shift('t')}>shift t</button>
<svelte:boundary>
<p>async a: {await push('a', a)}</p>
<!-- reads `a` after an await, but only once t > 0 -->
<p>late read: {(await push('t', t), t > 0 ? a : -1)}</p>
{#snippet pending()}
<p>loading...</p>
{/snippet}
</svelte:boundary>

@ -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>

@ -0,0 +1,31 @@
import { tick } from 'svelte';
import { test } from '../../test';
const buttons = '<button>a</button><button>b</button><button>shift</button>';
export default test({
async test({ assert, target }) {
await tick();
const [a, b, shift] = target.querySelectorAll('button');
assert.htmlEqual(target.innerHTML, `<p>0</p><p>0</p>${buttons}`);
// batch A writes `a` and pends on its async expression
a.click();
await tick();
assert.htmlEqual(target.innerHTML, `<p>0</p><p>0</p>${buttons}`);
// batch B writes `b` — it only shares the `{add(a, b)}` template expression
// (a leaf) with batch A, so the two are independent: B commits right away,
// rendering its own view of the world (in which `a` is still 0)
b.click();
await tick();
assert.htmlEqual(target.innerHTML, `<p>0</p><p>1</p>${buttons}`);
// the async expression settles — A commits and brings the shared
// template expression up to date
shift.click();
await tick();
assert.htmlEqual(target.innerHTML, `<p>1</p><p>2</p>${buttons}`);
}
});

@ -0,0 +1,23 @@
<script>
let a = $state(0);
let b = $state(0);
const c = $derived(add(a, b))
let pend = false;
const deferred = [];
function delay(value) {
if (!pend) return value;
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
function add(x, y) {
return x + y;
}
</script>
<p>{await delay(a)}</p>
<p>{c}</p>
<button onclick={() => { pend = true; a++; }}>a</button>
<button onclick={() => b++}>b</button>
<button onclick={() => deferred.shift()?.()}>shift</button>

@ -0,0 +1,38 @@
import { tick } from 'svelte';
import { test } from '../../test';
const buttons = '<button>z</button><button>w</button><button>shift</button>';
export default test({
async test({ assert, target, logs }) {
await tick();
const [z, w, shift] = target.querySelectorAll('button');
assert.htmlEqual(target.innerHTML, `<p>true</p><p>0</p>${buttons}`);
assert.deepEqual(logs, ['eval true']);
// with no other batch pending, the equality cut-off works: `c`
// recomputes to the same value, the template expression is not
// re-evaluated
z.click();
await tick();
assert.deepEqual(logs, ['eval true']);
// write w — the batch is pending on its async expression
w.click();
await tick();
assert.deepEqual(logs, ['eval true']);
// z++ recomputes `c` to the same value again — the template expression
// should still not be re-evaluated, even though another batch is pending
z.click();
await tick();
assert.deepEqual(logs, ['eval true']);
// the pending batch settles — nothing `c` depends on changed
shift.click();
await tick();
assert.deepEqual(logs, ['eval true']);
assert.htmlEqual(target.innerHTML, `<p>true</p><p>1</p>${buttons}`);
}
});

@ -0,0 +1,25 @@
<script>
let z = $state(1);
let w = $state(0);
let pend = false;
const deferred = [];
let c = $derived(z > 0);
function delay(value) {
if (!pend) return value;
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
function log(value) {
console.log(`eval ${value}`);
return value;
}
</script>
<p>{log(c)}</p>
<p>{await delay(w)}</p>
<button onclick={() => z++}>z</button>
<button onclick={() => { pend = true; w += 1; }}>w</button>
<button onclick={() => deferred.shift()?.()}>shift</button>

@ -0,0 +1,36 @@
import { tick } from 'svelte';
import { test } from '../../test';
const buttons = '<button>b</button><button>z</button><button>shift</button>';
export default test({
async test({ assert, target, logs }) {
await tick();
const [b, z, shift] = target.querySelectorAll('button');
assert.htmlEqual(target.innerHTML, `<p>0</p><p>a</p>${buttons}`);
assert.deepEqual(logs, ['fetch a']);
// write x = 'b' — the batch is pending on its async expression
b.click();
await tick();
assert.deepEqual(logs, ['fetch a', 'fetch b']);
// an independent batch runs the effect, which writes x = 'b' — the
// real value is already 'b', so this is a no-op: the async expression
// must not re-run (no needless refetch), and the batch must not be
// entangled with the pending one (z commits immediately)
z.click();
await tick();
assert.deepEqual(logs, ['fetch a', 'fetch b']);
assert.htmlEqual(target.innerHTML, `<p>1</p><p>a</p>${buttons}`);
// the pending batch settles
shift.click();
await tick();
shift.click();
await tick();
assert.deepEqual(logs, ['fetch a', 'fetch b']);
assert.htmlEqual(target.innerHTML, `<p>1</p><p>b</p>${buttons}`);
}
});

@ -0,0 +1,25 @@
<script>
let x = $state('a');
let z = $state(0);
let pend = false;
const deferred = [];
function delay(value) {
console.log(`fetch ${value}`);
if (!pend) return value;
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
$effect(() => {
if (z > 0) {
x = 'b';
}
});
</script>
<p>{z}</p>
<p>{await delay(x)}</p>
<button onclick={() => { pend = true; x = 'b'; }}>b</button>
<button onclick={() => z++}>z</button>
<button onclick={() => deferred.shift()?.()}>shift</button>

@ -0,0 +1,33 @@
import { tick } from 'svelte';
import { test } from '../../test';
const buttons = '<button>b</button><button>reset</button><button>shift</button>';
export default test({
async test({ assert, target, logs }) {
await tick();
const [b, reset, shift] = target.querySelectorAll('button');
assert.htmlEqual(target.innerHTML, `<p>0</p><p>a</p>${buttons}`);
assert.deepEqual(logs, ['fetch a']);
// write x = 'b' — the batch is pending on its async expression
b.click();
await tick();
assert.deepEqual(logs, ['fetch a', 'fetch b']);
// an independent batch runs the effect, which resets x = 'a'. The real
// (pending) value is 'b', so this is a genuine change and must not be
// swallowed just because the effect's world still shows 'a'
reset.click();
await tick();
assert.deepEqual(logs, ['fetch a', 'fetch b', 'fetch a']);
// resolve all in-flight runs — the reset must win
shift.click();
await tick();
shift.click();
await tick();
assert.htmlEqual(target.innerHTML, `<p>1</p><p>a</p>${buttons}`);
}
});

@ -0,0 +1,25 @@
<script>
let x = $state('a');
let z = $state(0);
let pend = false;
const deferred = [];
function delay(value) {
console.log(`fetch ${value}`);
if (!pend) return value;
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
$effect(() => {
if (z > 0) {
x = 'a';
}
});
</script>
<p>{z}</p>
<p>{await delay(x)}</p>
<button onclick={() => { pend = true; x = 'b'; }}>b</button>
<button onclick={() => z++}>reset</button>
<button onclick={() => deferred.shift()?.()}>shift</button>
Loading…
Cancel
Save