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({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