From 7eb11e0e247d9da4edb88dc5652101ceb55fc530 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 24 Jul 2025 16:00:43 +0200 Subject: [PATCH 1/3] fix: don't destroy effect roots created inside of deriveds (#16492) We were wrongfully adding effect roots to `derived.effects`, too, which meant those were destroyed when the derived reran. --- .changeset/lemon-weeks-call.md | 5 ++ .../src/internal/client/reactivity/effects.js | 6 ++- .../src/internal/client/reactivity/types.d.ts | 2 +- packages/svelte/tests/signals/test.ts | 51 +++++++++++++++++++ 4 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 .changeset/lemon-weeks-call.md diff --git a/.changeset/lemon-weeks-call.md b/.changeset/lemon-weeks-call.md new file mode 100644 index 000000000000..ae62305630b5 --- /dev/null +++ b/.changeset/lemon-weeks-call.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't destroy effect roots created inside of deriveds diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index c4edd2bf8d95..f44efa32f1ca 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -148,7 +148,11 @@ function create_effect(type, fn, sync, push = true) { } // if we're in a derived, add the effect there too - if (active_reaction !== null && (active_reaction.f & DERIVED) !== 0) { + if ( + active_reaction !== null && + (active_reaction.f & DERIVED) !== 0 && + (type & ROOT_EFFECT) === 0 + ) { var derived = /** @type {Derived} */ (active_reaction); (derived.effects ??= []).push(effect); } diff --git a/packages/svelte/src/internal/client/reactivity/types.d.ts b/packages/svelte/src/internal/client/reactivity/types.d.ts index 72187e84a720..81f7197b806d 100644 --- a/packages/svelte/src/internal/client/reactivity/types.d.ts +++ b/packages/svelte/src/internal/client/reactivity/types.d.ts @@ -54,7 +54,7 @@ export interface Reaction extends Signal { export interface Derived extends Value, Reaction { /** The derived function */ fn: () => V; - /** Effects created inside this signal */ + /** Effects created inside this signal. Used to destroy those effects when the derived reruns or is cleaned up */ effects: null | Effect[]; /** Parent effect or derived */ parent: Effect | Derived | null; diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index 937324727b16..eff6d6166a5e 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -1390,4 +1390,55 @@ describe('signals', () => { destroy(); }; }); + + test('$effect.root inside deriveds stay alive independently', () => { + const log: any[] = []; + const c = state(0); + const cleanup: any[] = []; + const inner_states: any[] = []; + + const d = derived(() => { + const destroy = effect_root(() => { + const x = state(0); + inner_states.push(x); + + effect(() => { + log.push('inner ' + $.get(x)); + return () => { + log.push('inner destroyed'); + }; + }); + }); + + cleanup.push(destroy); + + return $.get(c); + }); + + return () => { + log.push($.get(d)); + flushSync(); + + assert.deepEqual(log, [0, 'inner 0']); + log.length = 0; + + set(inner_states[0], 1); + flushSync(); + + assert.deepEqual(log, ['inner destroyed', 'inner 1']); + log.length = 0; + + set(c, 1); + log.push($.get(d)); + flushSync(); + + assert.deepEqual(log, [1, 'inner 0']); + log.length = 0; + + cleanup.forEach((fn) => fn()); + flushSync(); + + assert.deepEqual(log, ['inner destroyed', 'inner destroyed']); + }; + }); }); From dc043fb2d3c87619e1c26315f600acd4fe19dfa4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 24 Jul 2025 10:16:15 -0400 Subject: [PATCH 2/3] fix: don't update a focused input with values from its own past (#16491) * fix: don't update a focused input with values from its own past * remove * fix --- .changeset/fast-mails-fail.md | 5 +++ .../client/dom/elements/bindings/input.js | 11 ++++-- .../src/internal/client/reactivity/batch.js | 12 +++++- .../_config.js | 37 +++++++++++++++++++ .../main.svelte | 25 +++++++++++++ 5 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 .changeset/fast-mails-fail.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/main.svelte diff --git a/.changeset/fast-mails-fail.md b/.changeset/fast-mails-fail.md new file mode 100644 index 000000000000..027cb01548c2 --- /dev/null +++ b/.changeset/fast-mails-fail.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't update a focused input with values from its own past diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/input.js b/packages/svelte/src/internal/client/dom/elements/bindings/input.js index 7c1fccea0fbc..7c73280dd664 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/input.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/input.js @@ -8,7 +8,7 @@ import { queue_micro_task } from '../../task.js'; import { hydrating } from '../../hydration.js'; import { untrack } from '../../../runtime.js'; import { is_runes } from '../../../context.js'; -import { current_batch } from '../../../reactivity/batch.js'; +import { current_batch, previous_batch } from '../../../reactivity/batch.js'; /** * @param {HTMLInputElement} input @@ -76,13 +76,18 @@ export function bind_value(input, get, set = get) { var value = get(); - if (input === document.activeElement && batches.has(/** @type {Batch} */ (current_batch))) { + if (input === document.activeElement) { + // we need both, because in non-async mode, render effects run before previous_batch is set + var batch = /** @type {Batch} */ (previous_batch ?? current_batch); + // Never rewrite the contents of a focused input. We can get here if, for example, // an update is deferred because of async work depending on the input: // // //

{await find(query)}

- return; + if (batches.has(batch)) { + return; + } } if (is_numberlike_input(input) && value === to_number(input.value)) { diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 89bad947c7fa..123bc95d163a 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -38,6 +38,13 @@ const batches = new Set(); /** @type {Batch | null} */ export let current_batch = null; +/** + * This is needed to avoid overwriting inputs in non-async mode + * TODO 6.0 remove this, as non-async mode will go away + * @type {Batch | null} + */ +export let previous_batch = null; + /** * When time travelling, we re-evaluate deriveds based on the temporary * values of their dependencies rather than their actual values, and cache @@ -71,7 +78,6 @@ let last_scheduled_effect = null; let is_flushing = false; let is_flushing_sync = false; - export class Batch { /** * The current values of any sources that are updated in this batch @@ -173,6 +179,8 @@ export class Batch { process(root_effects) { queued_root_effects = []; + previous_batch = null; + /** @type {Map | null} */ var current_values = null; @@ -218,6 +226,7 @@ export class Batch { // If sources are written to, then work needs to happen in a separate batch, else prior sources would be mixed with // newly updated sources, which could lead to infinite loops when effects run over and over again. + previous_batch = current_batch; current_batch = null; flush_queued_effects(render_effects); @@ -350,6 +359,7 @@ export class Batch { deactivate() { current_batch = null; + previous_batch = null; for (const update of effect_pending_updates) { effect_pending_updates.delete(update); diff --git a/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/_config.js b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/_config.js new file mode 100644 index 000000000000..b0772ad3c071 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/_config.js @@ -0,0 +1,37 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, instance }) { + instance.shift(); + await tick(); + + const [input] = target.querySelectorAll('input'); + + input.focus(); + input.value = '1'; + input.dispatchEvent(new InputEvent('input', { bubbles: true })); + await tick(); + + assert.htmlEqual(target.innerHTML, `

0

`); + assert.equal(input.value, '1'); + + input.focus(); + input.value = '2'; + input.dispatchEvent(new InputEvent('input', { bubbles: true })); + await tick(); + + assert.htmlEqual(target.innerHTML, `

0

`); + assert.equal(input.value, '2'); + + instance.shift(); + await tick(); + assert.htmlEqual(target.innerHTML, `

1

`); + assert.equal(input.value, '2'); + + instance.shift(); + await tick(); + assert.htmlEqual(target.innerHTML, `

2

`); + assert.equal(input.value, '2'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/main.svelte new file mode 100644 index 000000000000..2fc898e6540d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/main.svelte @@ -0,0 +1,25 @@ + + + + +

{await push(count)}

+ + {#snippet pending()} +

loading...

+ {/snippet} +
From 8ad02e4a8c920823034051e5e2a13df58f25ebd5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 24 Jul 2025 16:22:30 +0200 Subject: [PATCH 3/3] Version Packages (#16493) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/fast-mails-fail.md | 5 ----- .changeset/lemon-weeks-call.md | 5 ----- packages/svelte/CHANGELOG.md | 8 ++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) delete mode 100644 .changeset/fast-mails-fail.md delete mode 100644 .changeset/lemon-weeks-call.md diff --git a/.changeset/fast-mails-fail.md b/.changeset/fast-mails-fail.md deleted file mode 100644 index 027cb01548c2..000000000000 --- a/.changeset/fast-mails-fail.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: don't update a focused input with values from its own past diff --git a/.changeset/lemon-weeks-call.md b/.changeset/lemon-weeks-call.md deleted file mode 100644 index ae62305630b5..000000000000 --- a/.changeset/lemon-weeks-call.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: don't destroy effect roots created inside of deriveds diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 5bffa5f70ef3..450ecde53b1e 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.36.16 + +### Patch Changes + +- fix: don't update a focused input with values from its own past ([#16491](https://github.com/sveltejs/svelte/pull/16491)) + +- fix: don't destroy effect roots created inside of deriveds ([#16492](https://github.com/sveltejs/svelte/pull/16492)) + ## 5.36.15 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 051f82ec3aad..07954026b550 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.36.15", + "version": "5.36.16", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 1d469f29b0f6..5d76fc3f2963 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.36.15'; +export const VERSION = '5.36.16'; export const PUBLIC_VERSION = '5';