diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 65180e00e5..c33f94d168 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -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; diff --git a/packages/svelte/tests/runtime-runes/samples/async-commit-stale-reader-changed/_config.js b/packages/svelte/tests/runtime-runes/samples/async-commit-stale-reader-changed/_config.js new file mode 100644 index 0000000000..af066836b2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-commit-stale-reader-changed/_config.js @@ -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, + '
0
' + ); + 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, + '1
' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-commit-stale-reader-changed/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-commit-stale-reader-changed/main.svelte new file mode 100644 index 0000000000..3a9323d8ce --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-commit-stale-reader-changed/main.svelte @@ -0,0 +1,23 @@ + + +{await delay(x)}
+ + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-commit-stale-reader-derived-equality/_config.js b/packages/svelte/tests/runtime-runes/samples/async-commit-stale-reader-derived-equality/_config.js new file mode 100644 index 0000000000..58eab0f674 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-commit-stale-reader-derived-equality/_config.js @@ -0,0 +1,33 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +const buttons = ''; + +export default test({ + async test({ assert, target, logs }) { + await tick(); + const [x, show, shift] = target.querySelectorAll('button'); + + assert.htmlEqual(target.innerHTML, `true
1
${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, `true
2
${buttons}`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-commit-stale-reader-derived-equality/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-commit-stale-reader-derived-equality/main.svelte new file mode 100644 index 0000000000..91214d6874 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-commit-stale-reader-derived-equality/main.svelte @@ -0,0 +1,26 @@ + + +{positive}
+{await delay(x)}
+ + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-commit-stale-reader-dropped-dep/_config.js b/packages/svelte/tests/runtime-runes/samples/async-commit-stale-reader-dropped-dep/_config.js new file mode 100644 index 0000000000..4a7e6282fb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-commit-stale-reader-dropped-dep/_config.js @@ -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, + '1
' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-commit-stale-reader-dropped-dep/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-commit-stale-reader-dropped-dep/main.svelte new file mode 100644 index 0000000000..4d9d616293 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-commit-stale-reader-dropped-dep/main.svelte @@ -0,0 +1,24 @@ + + +{await delay(x)}
+ + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-commit-stale-reader-unchanged/_config.js b/packages/svelte/tests/runtime-runes/samples/async-commit-stale-reader-unchanged/_config.js new file mode 100644 index 0000000000..4a6e2587d4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-commit-stale-reader-unchanged/_config.js @@ -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, + '0
' + ); + 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']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-commit-stale-reader-unchanged/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-commit-stale-reader-unchanged/main.svelte new file mode 100644 index 0000000000..2c95b4a2f8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-commit-stale-reader-unchanged/main.svelte @@ -0,0 +1,23 @@ + + +{await delay(x)}
+ + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-eager-effect-loop/_config.js b/packages/svelte/tests/runtime-runes/samples/async-eager-effect-loop/_config.js new file mode 100644 index 0000000000..4dee4cc511 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-eager-effect-loop/_config.js @@ -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, '0
'); + + increment.click(); + await tick(); + + assert.htmlEqual(target.innerHTML, '2
'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-eager-effect-loop/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-eager-effect-loop/main.svelte new file mode 100644 index 0000000000..8ba1897206 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-eager-effect-loop/main.svelte @@ -0,0 +1,14 @@ + + + + +{await delay(count, $state.eager(count))}
diff --git a/packages/svelte/tests/runtime-runes/samples/async-eager-reestablish-no-refetch/_config.js b/packages/svelte/tests/runtime-runes/samples/async-eager-reestablish-no-refetch/_config.js new file mode 100644 index 0000000000..b7177c7135 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-eager-reestablish-no-refetch/_config.js @@ -0,0 +1,36 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +const buttons = ''; + +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, `-1
${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, `0
1${buttons}`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-eager-reestablish-no-refetch/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-eager-reestablish-no-refetch/main.svelte new file mode 100644 index 0000000000..3e4bd83e93 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-eager-reestablish-no-refetch/main.svelte @@ -0,0 +1,26 @@ + + +{await delay(count)}
+{#if count >= 0} + {#if log($state.eager(eag))} + {eag} + {/if} +{/if} + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-async-effect-replay/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-async-effect-replay/_config.js new file mode 100644 index 0000000000..1f9a10c5a1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-async-effect-replay/_config.js @@ -0,0 +1,41 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +const buttons = ` + + + + +`; + +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); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-async-effect-replay/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-async-effect-replay/main.svelte new file mode 100644 index 0000000000..814980d99b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-async-effect-replay/main.svelte @@ -0,0 +1,42 @@ + + + + + + + +{#if show} +{await load(searchParams.value)}
+1 2
`); + flushSync(() => increment.click()); + assert.htmlEqual(target.innerHTML, `${buttons}2 4
`); + flushSync(() => reset.click()); + assert.htmlEqual(target.innerHTML, buttons); + } + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-branch-update/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-branch-update/main.svelte new file mode 100644 index 0000000000..a0e98c1c4b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-branch-update/main.svelte @@ -0,0 +1,22 @@ + + + + + + + + + + + +{#if show} +{count} {doubled}
+{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-commit-superseded-effect/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-commit-superseded-effect/_config.js new file mode 100644 index 0000000000..b7ace65f23 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-commit-superseded-effect/_config.js @@ -0,0 +1,43 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +const buttons = + ''; + +export default test({ + async test({ assert, target }) { + await tick(); + const [forkButton, y, shift, commit] = target.querySelectorAll('button'); + + assert.htmlEqual(target.innerHTML, `0
${buttons}`); + + // speculative world: x becomes 1, async expression runs with x + y = 1 + forkButton.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `0
${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, `0
${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, `0
${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, `2
${buttons}`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-commit-superseded-effect/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-commit-superseded-effect/main.svelte new file mode 100644 index 0000000000..3dba3d1004 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-commit-superseded-effect/main.svelte @@ -0,0 +1,20 @@ + + +{await delay(x + y)}
+ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-effect-overlap-2/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-effect-overlap-2/_config.js new file mode 100644 index 0000000000..4aeeff78b2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-effect-overlap-2/_config.js @@ -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'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-effect-overlap-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-effect-overlap-2/main.svelte new file mode 100644 index 0000000000..2e461cd884 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-effect-overlap-2/main.svelte @@ -0,0 +1,23 @@ + + + + + + + + +{await delay(console.log('called with ' + x + ',' + y), x + y)}
diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-revalidation-no-refetch/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-revalidation-no-refetch/_config.js new file mode 100644 index 0000000000..31474640ef --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-revalidation-no-refetch/_config.js @@ -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, + '1 0
' + ); + assert.deepEqual(logs, ['b 1', 'b 1']); + + discard.click(); + await tick(); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-revalidation-no-refetch/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-revalidation-no-refetch/main.svelte new file mode 100644 index 0000000000..f2a6df36d7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-revalidation-no-refetch/main.svelte @@ -0,0 +1,20 @@ + + +{await delay(console.log(`b ${b}`), b)} {c}
+ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-read-pending-value-in-committed-batch/_config.js b/packages/svelte/tests/runtime-runes/samples/async-read-pending-value-in-committed-batch/_config.js new file mode 100644 index 0000000000..6a65bb3c1a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-read-pending-value-in-committed-batch/_config.js @@ -0,0 +1,61 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +const buttons = ` + + + + + + +`; + +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}loading 1...
`); + + // batch C: reveals boundary 2 -> commits with pending snippet, stays live + show2.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}loading 1...
loading 2...
`); + + // 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}late read: 1
loading 2...
`); + + // 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}late read: 1
async a: 0
`); + + // resolve the late reader's re-run -> it converges on the committed value + shift_t.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}late read: 0
async a: 0
`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-read-pending-value-in-committed-batch/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-read-pending-value-in-committed-batch/main.svelte new file mode 100644 index 0000000000..78ca377225 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-read-pending-value-in-committed-batch/main.svelte @@ -0,0 +1,49 @@ + + + + + + + + + +{#if show1} +late read: {(await push('t', 0)) > 0 ? a : -1}
+ + {#snippet pending()} +loading 1...
+ {/snippet} +async a: {await push('a', a)}
+ + {#snippet pending()} +loading 2...
+ {/snippet} +UNIVERSE
+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-5/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-5/main.svelte new file mode 100644 index 0000000000..aa95730425 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-5/main.svelte @@ -0,0 +1,29 @@ + + + + + + + + +{upper}
+{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-read-new-dependency-continuation/_config.js b/packages/svelte/tests/runtime-runes/samples/async-state-read-new-dependency-continuation/_config.js new file mode 100644 index 0000000000..f0d530cf10 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-read-new-dependency-continuation/_config.js @@ -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, + ` + + + + +async a: 0
+late read: -1
+ ` + ); + + // 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, + ` + + + + +async a: 0
+late read: -1
+ ` + ); + + // commit the merged batch + shift_a.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + + +async a: 1
+late read: 1
+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-read-new-dependency-continuation/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-read-new-dependency-continuation/main.svelte new file mode 100644 index 0000000000..428dd4c774 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-read-new-dependency-continuation/main.svelte @@ -0,0 +1,35 @@ + + + + + + + +async a: {await push('a', a)}
+ + +late read: {(await push('t', t), t > 0 ? a : -1)}
+ + {#snippet pending()} +loading...
+ {/snippet} +1
+ ` + ); + + resolve.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +1
+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-read-new-dependency/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-read-new-dependency/main.svelte new file mode 100644 index 0000000000..80e54f9d53 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-read-new-dependency/main.svelte @@ -0,0 +1,17 @@ + + + + + + +{await wait(value)} + +{show ? value.x : ''}
\ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-template-expression-entangle/_config.js b/packages/svelte/tests/runtime-runes/samples/async-template-expression-entangle/_config.js new file mode 100644 index 0000000000..d7f844881b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-template-expression-entangle/_config.js @@ -0,0 +1,31 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +const buttons = ''; + +export default test({ + async test({ assert, target }) { + await tick(); + const [a, b, shift] = target.querySelectorAll('button'); + + assert.htmlEqual(target.innerHTML, `0
0
${buttons}`); + + // batch A writes `a` and pends on its async expression + a.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `0
0
${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, `0
1
${buttons}`); + + // the async expression settles — A commits and brings the shared + // template expression up to date + shift.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `1
2
${buttons}`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-template-expression-entangle/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-template-expression-entangle/main.svelte new file mode 100644 index 0000000000..a56fe9b0d8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-template-expression-entangle/main.svelte @@ -0,0 +1,23 @@ + + +{await delay(a)}
+{c}
+ + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-template-expression-equality-cut/_config.js b/packages/svelte/tests/runtime-runes/samples/async-template-expression-equality-cut/_config.js new file mode 100644 index 0000000000..7206812832 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-template-expression-equality-cut/_config.js @@ -0,0 +1,38 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +const buttons = ''; + +export default test({ + async test({ assert, target, logs }) { + await tick(); + const [z, w, shift] = target.querySelectorAll('button'); + + assert.htmlEqual(target.innerHTML, `true
0
${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, `true
1
${buttons}`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-template-expression-equality-cut/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-template-expression-equality-cut/main.svelte new file mode 100644 index 0000000000..9c54dd79d6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-template-expression-equality-cut/main.svelte @@ -0,0 +1,25 @@ + + +{log(c)}
+{await delay(w)}
+ + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-write-equals-real-value/_config.js b/packages/svelte/tests/runtime-runes/samples/async-write-equals-real-value/_config.js new file mode 100644 index 0000000000..be2a3bcfd3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-write-equals-real-value/_config.js @@ -0,0 +1,36 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +const buttons = ''; + +export default test({ + async test({ assert, target, logs }) { + await tick(); + const [b, z, shift] = target.querySelectorAll('button'); + + assert.htmlEqual(target.innerHTML, `0
a
${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, `1
a
${buttons}`); + + // the pending batch settles + shift.click(); + await tick(); + shift.click(); + await tick(); + assert.deepEqual(logs, ['fetch a', 'fetch b']); + assert.htmlEqual(target.innerHTML, `1
b
${buttons}`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-write-equals-real-value/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-write-equals-real-value/main.svelte new file mode 100644 index 0000000000..22276aba5a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-write-equals-real-value/main.svelte @@ -0,0 +1,25 @@ + + +{z}
+{await delay(x)}
+ + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-write-equals-visible-value/_config.js b/packages/svelte/tests/runtime-runes/samples/async-write-equals-visible-value/_config.js new file mode 100644 index 0000000000..b1b6c717af --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-write-equals-visible-value/_config.js @@ -0,0 +1,33 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +const buttons = ''; + +export default test({ + async test({ assert, target, logs }) { + await tick(); + const [b, reset, shift] = target.querySelectorAll('button'); + + assert.htmlEqual(target.innerHTML, `0
a
${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, `1
a
${buttons}`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-write-equals-visible-value/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-write-equals-visible-value/main.svelte new file mode 100644 index 0000000000..fe70fd762b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-write-equals-visible-value/main.svelte @@ -0,0 +1,25 @@ + + +{z}
+{await delay(x)}
+ + +