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} + + {#snippet pending()}loading{/snippet} +

{await load(searchParams.value)}

+
+{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-branch-update/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-branch-update/_config.js new file mode 100644 index 0000000000..8b50ff2e19 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-branch-update/_config.js @@ -0,0 +1,45 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +const buttons = ` + + + + + + + + +`; + +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}

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} +
+{/if} + +{#if show2} + +

async a: {await push('a', a)}

+ + {#snippet pending()} +

loading 2...

+ {/snippet} +
+{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-source-only-overlap/_config.js b/packages/svelte/tests/runtime-runes/samples/async-source-only-overlap/_config.js new file mode 100644 index 0000000000..a65c8a6fba --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-source-only-overlap/_config.js @@ -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, + ' 0' + ); + + b.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ' 0' + ); + + resolve.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ' 1' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-source-only-overlap/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-source-only-overlap/main.svelte new file mode 100644 index 0000000000..1e107c0374 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-source-only-overlap/main.svelte @@ -0,0 +1,18 @@ + + + + + +{await push(a)} diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-4/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-4/Child.svelte new file mode 100644 index 0000000000..05409ef225 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-4/Child.svelte @@ -0,0 +1,7 @@ + + +{x.x} diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-4/_config.js b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-4/_config.js new file mode 100644 index 0000000000..9e3e4369a4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-4/_config.js @@ -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, + ` + + + + ` + ); + + resolve.click(); + await tick(); + // both branches commit together, fully consistent + assert.deepEqual(logs, ['universe', 'universe', '$effect: universe', '$effect: universe']); + assert.htmlEqual( + target.innerHTML, + ` + + + + universe + universe + universe + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-4/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-4/main.svelte new file mode 100644 index 0000000000..3b858909c2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-4/main.svelte @@ -0,0 +1,28 @@ + + + + + + + + +{#if x?.x === 'universe'} + {await delay(x.x)} + +{/if} + +{#if y > 0} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-5/_config.js b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-5/_config.js new file mode 100644 index 0000000000..28b8229839 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-5/_config.js @@ -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, + ` + + + +

WORLD

+ ` + ); + + 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, + ` + + + +

WORLD

+ ` + ); + + resolve.click(); + await tick(); + // both branches commit together, fully consistent + assert.htmlEqual( + target.innerHTML, + ` + + + +

UNIVERSE

+ universe +

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 x.x === 'universe'} + {await delay(x.x)} +{/if} + +{#if y > 0} +

{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} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-read-new-dependency/_config.js b/packages/svelte/tests/runtime-runes/samples/async-state-read-new-dependency/_config.js new file mode 100644 index 0000000000..f1bf5a7349 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-read-new-dependency/_config.js @@ -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, + ` + + + +

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)}

+ + +