diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index dcc3222a9d..e468cdc648 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -759,17 +759,55 @@ export function get(signal) { // we saw turns out to differ from the committed one) var override_owner = override[1]; - if (override_owner !== null && active_reaction !== null && !untracking) { - override_owner = override_owner.resolved(); + if (override_owner !== null) { + if (active_reaction === null) { + // reads outside a reaction during a flush happen in one-shot init + // code (e.g. a component initialising inside a newly-created + // branch). They have no dependency history and no re-run + // mechanism, so they see the latest value + if ((signal.f & DERIVED) === 0) { + if ((signal.f & ERROR_VALUE) !== 0) { + throw signal.v; + } + + return signal.v; + } + } else if (!untracking) { + override_owner = override_owner.resolved(); - var readers = (override_owner.stale_readers ??= new Map()); - var seen = readers.get(active_reaction); + var readers = (override_owner.stale_readers ??= new Map()); + var seen = readers.get(active_reaction); - if (seen === undefined) { - readers.set(active_reaction, (seen = new Map())); - } + if (seen === undefined) { + readers.set(active_reaction, (seen = new Map())); + } + + // a reader keeps seeing the value it first observed while the owner is pending + if (seen.has(signal)) { + return seen.get(signal); + } - seen.set(signal, override[0]); + var override_value = override[0]; + + if ( + (active_reaction.f & REACTION_IS_UPDATING) !== 0 && + (signal.f & DERIVED) === 0 && + (active_reaction.deps === null || !includes.call(active_reaction.deps, signal)) + ) { + // the reaction never depended on this signal before the owner's write — + // the pre-write world never contained this combination of values, + // so read the latest value instead + if ((signal.f & ERROR_VALUE) !== 0) { + throw signal.v; + } + + override_value = signal.v; + } + + seen.set(signal, override_value); + + return override_value; + } } return override[0]; 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 index 0e741d3bbf..af066836b2 100644 --- 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 @@ -17,17 +17,17 @@ export default test({ await tick(); assert.deepEqual(logs, []); - // an independent batch runs the effect, which reads `x` through the - // pending batch's overlay (seeing the held-back value 0) + // an independent batch runs the effect, which newly depends on `x` — + // it reads the latest value (1) rather than the held-back one (0) y.click(); await tick(); - assert.deepEqual(logs, ['effect 0 1']); + assert.deepEqual(logs, ['effect 1 1']); - // the pending batch settles and commits x === 1 — the effect saw a - // stale value and must re-run with the real one + // the pending batch settles and commits x === 1 — exactly the value + // the effect already saw, so it should not re-run shift.click(); await tick(); - assert.deepEqual(logs, ['effect 0 1', 'effect 1 1']); + assert.deepEqual(logs, ['effect 1 1']); assert.htmlEqual( target.innerHTML, '

1

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

1

' diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-1/_config.js b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-1/_config.js index dee8af2446..081bb88f8f 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-1/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-1/_config.js @@ -10,26 +10,23 @@ export default test({ y.click(); await tick(); + // the new branch's reactive reads of `x` are new dependencies on a value the + // pending batch has written, so they see the latest value ('universe') assert.htmlEqual( target.innerHTML, ` - world - ` // if this does not show world - that would also be ok + universe + ` ); resolve.click(); await tick(); - assert.deepEqual(logs, [ - 'universe', - 'world', - '$effect: world', - '$effect: universe', - '$effect: universe' - ]); - // assert.deepEqual(logs, ['universe', 'universe', '$effect: universe', '$effect: universe']); // this would also be ok + // the init-time console.log runs outside a reaction and sees the latest value; + // the second child's $effect already saw the committed value, so it does not re-run + assert.deepEqual(logs, ['universe', 'universe', '$effect: universe', '$effect: universe']); assert.htmlEqual( target.innerHTML, ` diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-2/_config.js b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-2/_config.js index d99f0df731..e21eb02417 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-2/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-2/_config.js @@ -10,6 +10,8 @@ export default test({ y.click(); await tick(); + // the new branch's reads of `x` are new dependencies on a value the + // pending batch has written, so they see the latest value ('universe') assert.htmlEqual( target.innerHTML, ` @@ -17,13 +19,13 @@ export default test({
- world - "world" - world - world - world - "world" - ` // if this does not show world "world" world world world "world" - then this would also be ok + universe + "universe" + universe + universe + universe + "universe" + ` ); resolve.click(); diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-3/_config.js b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-3/_config.js index eb4485e8a6..5ad32b24c5 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-3/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-3/_config.js @@ -22,6 +22,8 @@ export default test({ resolve.click(); await tick(); + // the new branch's async expression read `x` as a new dependency on a value + // the pending batch had written, so it saw the latest value ('universe') assert.htmlEqual( target.innerHTML, ` @@ -29,13 +31,13 @@ export default test({
- world - "world" - world - world - world - "world" - ` // if this does not show world "world" world world world "world" - then this would also be ok + universe + "universe" + universe + universe + universe + "universe" + ` ); resolve.click(); 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..5b0d3e996c --- /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's reads of `x` are new dependencies on a value the + // pending batch has written, so they see the latest value ('universe') — + // including the init-time console.log, which runs outside a reaction + assert.deepEqual(logs, ['universe', 'universe', '$effect: universe']); + assert.htmlEqual( + target.innerHTML, + ` + + + + universe + ` + ); + + resolve.click(); + await tick(); + // the second child's $effect already saw the committed value, so it does not re-run + assert.deepEqual(logs, ['universe', 'universe', '$effect: universe', '$effect: universe']); + assert.htmlEqual( + target.innerHTML, + ` + + + + 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/_config.js b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch/_config.js index f4b6cc777b..5b0d3e996c 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch/_config.js @@ -11,26 +11,24 @@ export default test({ y.click(); await tick(); - assert.deepEqual(logs, ['universe', 'world', '$effect: world']); + // the new branch's reads of `x` are new dependencies on a value the + // pending batch has written, so they see the latest value ('universe') — + // including the init-time console.log, which runs outside a reaction + assert.deepEqual(logs, ['universe', 'universe', '$effect: universe']); assert.htmlEqual( target.innerHTML, ` - world + universe ` ); resolve.click(); await tick(); - assert.deepEqual(logs, [ - 'universe', - 'world', - '$effect: world', - '$effect: universe', - '$effect: universe' - ]); + // the second child's $effect already saw the committed value, so it does not re-run + assert.deepEqual(logs, ['universe', 'universe', '$effect: universe', '$effect: universe']); assert.htmlEqual( target.innerHTML, ` 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