diff --git a/.changeset/rude-ghosts-tickle.md b/.changeset/rude-ghosts-tickle.md new file mode 100644 index 0000000000..abd8b59020 --- /dev/null +++ b/.changeset/rude-ghosts-tickle.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: correctly handle proxied signal writes before reads diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 7106264e74..4f26c6bd42 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -6,7 +6,8 @@ import { updating_derived, batch_inspect, current_component_context, - set_ignore_mutation_validation + set_ignore_mutation_validation, + untrack } from './runtime.js'; import { effect_active } from './reactivity/computations.js'; import { @@ -261,10 +262,21 @@ const state_proxy_handler = { return has; }, - set(target, prop, value) { + set(target, prop, value, receiver) { const metadata = target[STATE_SYMBOL]; - const s = metadata.s.get(prop); - if (s !== undefined) set(s, proxy(value, metadata.i, metadata.o)); + let s = metadata.s.get(prop); + // If we haven't yet created a source for this property, we need to ensure + // we do so otherwise if we read it later, then the write won't be tracked and + // the heuristics of effects will be different vs if we had read the proxied + // object property before writing to that property. + if (s === undefined && effect_active()) { + // the read creates a signal + untrack(() => receiver[prop]); + s = metadata.s.get(prop); + } + if (s !== undefined) { + set(s, proxy(value, metadata.i, metadata.o)); + } const is_array = metadata.a; const not_has = !(prop in target); diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index b53ebda54c..6ecb818210 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -744,6 +744,7 @@ export function get(signal) { current_untracked_writes !== null && current_effect !== null && (current_effect.f & CLEAN) !== 0 && + (current_effect.f & MANAGED) === 0 && current_untracked_writes.includes(signal) ) { set_signal_status(current_effect, DIRTY); @@ -975,7 +976,8 @@ export function set_signal_value(signal, value) { !ignore_mutation_validation && current_effect !== null && current_effect.c === null && - (current_effect.f & CLEAN) !== 0 + (current_effect.f & CLEAN) !== 0 && + (current_effect.f & MANAGED) === 0 ) { if (current_dependencies !== null && current_dependencies.includes(signal)) { set_signal_status(current_effect, DIRTY); diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index 8b91601a3b..0df1218f57 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -8,6 +8,7 @@ import { } from '../../src/internal/client/reactivity/computations'; import { source } from '../../src/internal/client/reactivity/sources'; import type { ComputationSignal } from '../../src/internal/client/types'; +import { proxy } from '../../src/internal/client/proxy'; /** * @param runes runes mode @@ -333,4 +334,25 @@ describe('signals', () => { assert.equal(errored, true); }; }); + + test('schedules rerun when writing to signal before reading it', (runes) => { + if (!runes) return () => {}; + + const value = proxy({ arr: [] }); + user_effect(() => { + value.arr = []; + value.arr; + }); + + return () => { + let errored = false; + try { + $.flushSync(); + } catch (e: any) { + assert.include(e.message, 'ERR_SVELTE_TOO_MANY_UPDATES'); + errored = true; + } + assert.equal(errored, true); + }; + }); });