From e4ae9e9f1dd23d47eb056f4be3a5301de5bce5dd Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 10 Oct 2025 17:20:13 +0200 Subject: [PATCH 01/15] runtime-first approach --- .../phases/3-transform/client/types.d.ts | 2 - .../client/visitors/CallExpression.js | 18 +++------ .../3-transform/client/visitors/Fragment.js | 5 --- .../client/visitors/RegularElement.js | 7 +--- .../svelte/src/compiler/types/template.d.ts | 1 - .../internal/client/dom/blocks/boundary.js | 39 +++++++++---------- .../src/internal/client/reactivity/batch.js | 14 +++++++ .../svelte/src/internal/client/runtime.js | 30 ++++++++++++-- 8 files changed, 65 insertions(+), 51 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts index 248158992278..932d35367162 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts @@ -24,8 +24,6 @@ export interface ClientTransformState extends TransformState { /** `true` if we're transforming the contents of ` + + + +{count} | {x} From e8330ee7bfff1eef61d5990e99c75dc6e225b5bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20R=C3=BCger?= Date: Tue, 14 Oct 2025 15:56:41 +0200 Subject: [PATCH 10/15] fix: svg `radialGradient` `fr` attribute missing in types (#16943) * fix(svg radialGradient): fr attribute missing in types * chore: add changeset --- .changeset/grumpy-towns-stop.md | 5 +++++ packages/svelte/elements.d.ts | 1 + 2 files changed, 6 insertions(+) create mode 100644 .changeset/grumpy-towns-stop.md diff --git a/.changeset/grumpy-towns-stop.md b/.changeset/grumpy-towns-stop.md new file mode 100644 index 000000000000..2b146818f5ed --- /dev/null +++ b/.changeset/grumpy-towns-stop.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +add missing type for `fr` attribute for `radialGradient` tags in svg diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts index b0c2fae2de69..17ff10072998 100644 --- a/packages/svelte/elements.d.ts +++ b/packages/svelte/elements.d.ts @@ -1658,6 +1658,7 @@ export interface SVGAttributes extends AriaAttributes, DO 'font-variant'?: number | string | undefined | null; 'font-weight'?: number | string | undefined | null; format?: number | string | undefined | null; + fr?: number | string | undefined | null; from?: number | string | undefined | null; fx?: number | string | undefined | null; fy?: number | string | undefined | null; From 2a951391dc31326fd5df14bf1bfcf425bd13d5ad Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 09:59:09 -0400 Subject: [PATCH 11/15] Version Packages (#16940) * Version Packages * Update packages/svelte/CHANGELOG.md --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Rich Harris --- .changeset/grumpy-towns-stop.md | 5 ----- .changeset/major-beans-fry.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/grumpy-towns-stop.md delete mode 100644 .changeset/major-beans-fry.md diff --git a/.changeset/grumpy-towns-stop.md b/.changeset/grumpy-towns-stop.md deleted file mode 100644 index 2b146818f5ed..000000000000 --- a/.changeset/grumpy-towns-stop.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -add missing type for `fr` attribute for `radialGradient` tags in svg diff --git a/.changeset/major-beans-fry.md b/.changeset/major-beans-fry.md deleted file mode 100644 index 8f35683cd623..000000000000 --- a/.changeset/major-beans-fry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: unset context on stale promises diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 70f549ce2964..b3af39eb4cb6 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.39.13 + +### Patch Changes + +- fix: add missing type for `fr` attribute for `radialGradient` tags in svg ([#16943](https://github.com/sveltejs/svelte/pull/16943)) + +- fix: unset context on stale promises ([#16935](https://github.com/sveltejs/svelte/pull/16935)) + ## 5.39.12 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index a2d5a6e40148..55b44fb2b65c 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.39.12", + "version": "5.39.13", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index e520d1248a49..536a2260c9e2 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.39.12'; +export const VERSION = '5.39.13'; export const PUBLIC_VERSION = '5'; From a7c958a2a5a89e1d22fa530e2bacd2b0e3ba7ba6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 14 Oct 2025 11:12:07 -0400 Subject: [PATCH 12/15] chore: simplify `batch.apply()` (#16945) * chore: simplify `batch.apply()` * belt and braces * note to self --- .changeset/pretty-llamas-explode.md | 5 ++ .../src/internal/client/reactivity/batch.js | 67 +++++++------------ .../internal/client/reactivity/deriveds.js | 8 ++- .../svelte/src/internal/client/runtime.js | 10 ++- 4 files changed, 41 insertions(+), 49 deletions(-) create mode 100644 .changeset/pretty-llamas-explode.md diff --git a/.changeset/pretty-llamas-explode.md b/.changeset/pretty-llamas-explode.md new file mode 100644 index 000000000000..00109112de60 --- /dev/null +++ b/.changeset/pretty-llamas-explode.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: simplify `batch.apply()` diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 102d0670b664..0dc149260a82 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -44,12 +44,12 @@ export let current_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 - * the results in this map rather than on the deriveds themselves - * @type {Map | null} + * When time travelling (i.e. working in one batch, while other batches + * still have ongoing work), we ignore the real values of affected + * signals in favour of their values within the batch + * @type {Map | null} */ -export let batch_deriveds = null; +export let batch_values = null; /** @type {Set<() => void>} */ export let effect_pending_updates = new Set(); @@ -152,7 +152,7 @@ export class Batch { previous_batch = null; - var revert = Batch.apply(this); + this.apply(); for (const root of root_effects) { this.#traverse_effect_tree(root); @@ -161,6 +161,10 @@ export class Batch { // if we didn't start any new async work, and no async work // is outstanding from a previous flush, commit if (this.#pending === 0) { + // TODO we need this because we commit _then_ flush effects... + // maybe there's a way we can reverse the order? + var previous_batch_sources = batch_values; + this.#commit(); var render_effects = this.#render_effects; @@ -175,6 +179,7 @@ export class Batch { previous_batch = this; current_batch = null; + batch_values = previous_batch_sources; flush_queued_effects(render_effects); flush_queued_effects(effects); @@ -187,7 +192,7 @@ export class Batch { this.#defer_effects(this.#block_effects); } - revert(); + batch_values = null; for (const effect of this.#boundary_async_effects) { update_effect(effect); @@ -274,6 +279,7 @@ export class Batch { } this.current.set(source, source.v); + batch_values?.set(source, source.v); } activate() { @@ -282,6 +288,7 @@ export class Batch { deactivate() { current_batch = null; + batch_values = null; } flush() { @@ -352,14 +359,14 @@ export class Batch { if (queued_root_effects.length > 0) { current_batch = batch; - const revert = Batch.apply(batch); + batch.apply(); for (const root of queued_root_effects) { batch.#traverse_effect_tree(root); } queued_root_effects = []; - revert(); + batch.deactivate(); } } @@ -423,49 +430,23 @@ export class Batch { queue_micro_task(task); } - /** - * @param {Batch} current_batch - */ - static apply(current_batch) { - if (!async_mode_flag || batches.size === 1) { - return noop; - } + apply() { + if (!async_mode_flag || batches.size === 1) return; // if there are multiple batches, we are 'time travelling' — - // we need to undo the changes belonging to any batch - // other than the current one - - /** @type {Map} */ - var current_values = new Map(); - batch_deriveds = new Map(); - - for (const [source, current] of current_batch.current) { - current_values.set(source, { v: source.v, wv: source.wv }); - source.v = current; - } + // we need to override values with the ones in this batch... + batch_values = new Map(this.current); + // ...and undo changes belonging to other batches for (const batch of batches) { - if (batch === current_batch) continue; + if (batch === this) continue; for (const [source, previous] of batch.#previous) { - if (!current_values.has(source)) { - current_values.set(source, { v: source.v, wv: source.wv }); - source.v = previous; + if (!batch_values.has(source)) { + batch_values.set(source, previous); } } } - - return () => { - for (const [source, { v, wv }] of current_values) { - // reset the source to the current value (unless - // it got a newer value as a result of effects running) - if (source.wv <= wv) { - source.v = v; - } - } - - batch_deriveds = null; - }; } } diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 076a91923680..bf8733cfe508 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -33,7 +33,7 @@ import { async_mode_flag, tracing_mode_flag } from '../../flags/index.js'; import { Boundary } from '../dom/blocks/boundary.js'; import { component_context } from '../context.js'; import { UNINITIALIZED } from '../../../constants.js'; -import { batch_deriveds, current_batch } from './batch.js'; +import { batch_values, current_batch } from './batch.js'; import { unset_context } from './async.js'; import { deferred } from '../../shared/utils.js'; @@ -336,6 +336,8 @@ export function update_derived(derived) { var value = execute_derived(derived); if (!derived.equals(value)) { + // TODO can we avoid setting `derived.v` when `batch_values !== null`, + // without causing the value to be stale later? derived.v = value; derived.wv = increment_write_version(); } @@ -346,8 +348,8 @@ export function update_derived(derived) { return; } - if (batch_deriveds !== null) { - batch_deriveds.set(derived, derived.v); + if (batch_values !== null) { + batch_values.set(derived, derived.v); } else { var status = (skip_reaction || (derived.f & UNOWNED) !== 0) && derived.deps !== null ? MAYBE_DIRTY : CLEAN; diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index b8f5f5ffc999..a146659bf688 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -42,7 +42,7 @@ import { set_dev_stack } from './context.js'; import * as w from './warnings.js'; -import { Batch, batch_deriveds, flushSync, schedule_effect } from './reactivity/batch.js'; +import { Batch, batch_values, flushSync, schedule_effect } from './reactivity/batch.js'; import { handle_error } from './error-handling.js'; import { UNINITIALIZED } from '../../constants.js'; import { captured_signals } from './legacy.js'; @@ -671,8 +671,8 @@ export function get(signal) { } else if (is_derived) { derived = /** @type {Derived} */ (signal); - if (batch_deriveds?.has(derived)) { - return batch_deriveds.get(derived); + if (batch_values?.has(derived)) { + return batch_values.get(derived); } if (is_dirty(derived)) { @@ -680,6 +680,10 @@ export function get(signal) { } } + if (batch_values?.has(signal)) { + return batch_values.get(signal); + } + if ((signal.f & ERROR_VALUE) !== 0) { throw signal.v; } From d50701d277e5c57204f1e51314926b0d73b0cbf9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 14 Oct 2025 11:36:55 -0400 Subject: [PATCH 13/15] unused --- packages/svelte/src/internal/client/reactivity/batch.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 0dc149260a82..2edfc1343a50 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -14,7 +14,7 @@ import { DERIVED } from '#client/constants'; import { async_mode_flag } from '../../flags/index.js'; -import { deferred, define_property, noop } from '../../shared/utils.js'; +import { deferred, define_property } from '../../shared/utils.js'; import { active_effect, is_dirty, From 28765f846e956c0da0e41e634de8c1066da9e6d5 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Tue, 14 Oct 2025 17:59:02 +0200 Subject: [PATCH 14/15] fix: don't rerun async effects unnecessarily (#16944) Since #16866, when an async effect runs multiple times, we rebase older batches and rerun those effects. This can have unintended consequences: In a case where an async effect only depends on a single source, and that single source was updated in a later batch, we know that we don't need to / should not rerun the older batch. This PR makes it so: We collect all the sources of older batches that are not part of the current batch that just committed, and then only mark those async effects as dirty which depend on one of those other sources. Fixes the bug I noticed while working on #16935 --- .changeset/wild-mirrors-take.md | 5 ++ .../src/internal/client/reactivity/batch.js | 67 ++++++++++++++----- .../internal/client/reactivity/deriveds.js | 7 ++ .../samples/async-resolve-stale/_config.js | 3 +- 4 files changed, 65 insertions(+), 17 deletions(-) create mode 100644 .changeset/wild-mirrors-take.md diff --git a/.changeset/wild-mirrors-take.md b/.changeset/wild-mirrors-take.md new file mode 100644 index 000000000000..faf28e7695c5 --- /dev/null +++ b/.changeset/wild-mirrors-take.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't rerun async effects unnecessarily diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 2edfc1343a50..2956e7ed6afe 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -1,4 +1,4 @@ -/** @import { Derived, Effect, Source, Value } from '#client' */ +/** @import { Derived, Effect, Reaction, Source, Value } from '#client' */ import { BLOCK_EFFECT, BRANCH_EFFECT, @@ -342,31 +342,46 @@ export class Batch { continue; } + /** @type {Source[]} */ + const sources = []; + for (const [source, value] of this.current) { if (batch.current.has(source)) { - if (is_earlier) { + if (is_earlier && value !== batch.current.get(source)) { // bring the value up to date batch.current.set(source, value); } else { - // later batch has more recent value, + // same value or later batch has more recent value, // no need to re-run these effects continue; } } - mark_effects(source); + sources.push(source); } - if (queued_root_effects.length > 0) { - current_batch = batch; - batch.apply(); + if (sources.length === 0) { + continue; + } - for (const root of queued_root_effects) { - batch.#traverse_effect_tree(root); + // Re-run async/block effects that depend on distinct values changed in both batches + const others = [...batch.current.keys()].filter((s) => !this.current.has(s)); + if (others.length > 0) { + for (const source of sources) { + mark_effects(source, others); } - queued_root_effects = []; - batch.deactivate(); + if (queued_root_effects.length > 0) { + current_batch = batch; + batch.apply(); + + for (const root of queued_root_effects) { + batch.#traverse_effect_tree(root); + } + + queued_root_effects = []; + batch.deactivate(); + } } } @@ -621,17 +636,19 @@ function flush_queued_effects(effects) { /** * This is similar to `mark_reactions`, but it only marks async/block effects - * so that these can re-run after another batch has been committed + * depending on `value` and at least one of the other `sources`, so that + * these effects can re-run after another batch has been committed * @param {Value} value + * @param {Source[]} sources */ -function mark_effects(value) { +function mark_effects(value, sources) { if (value.reactions !== null) { for (const reaction of value.reactions) { const flags = reaction.f; if ((flags & DERIVED) !== 0) { - mark_effects(/** @type {Derived} */ (reaction)); - } else if ((flags & (ASYNC | BLOCK_EFFECT)) !== 0) { + mark_effects(/** @type {Derived} */ (reaction), sources); + } else if ((flags & (ASYNC | BLOCK_EFFECT)) !== 0 && depends_on(reaction, sources)) { set_signal_status(reaction, DIRTY); schedule_effect(/** @type {Effect} */ (reaction)); } @@ -639,6 +656,26 @@ function mark_effects(value) { } } +/** + * @param {Reaction} reaction + * @param {Source[]} sources + */ +function depends_on(reaction, sources) { + if (reaction.deps !== null) { + for (const dep of reaction.deps) { + if (sources.includes(dep)) { + return true; + } + + if ((dep.f & DERIVED) !== 0 && depends_on(/** @type {Derived} */ (dep), sources)) { + return true; + } + } + } + + return false; +} + /** * @param {Effect} signal * @returns {void} diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index bf8733cfe508..fa780013e15b 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -171,6 +171,13 @@ export function async_derived(fn, location) { internal_set(signal, value); + // All prior async derived runs are now stale + for (const [b, d] of deferreds) { + deferreds.delete(b); + if (b === batch) break; + d.reject(STALE_REACTION); + } + if (DEV && location !== undefined) { recent_async_deriveds.add(signal); diff --git a/packages/svelte/tests/runtime-runes/samples/async-resolve-stale/_config.js b/packages/svelte/tests/runtime-runes/samples/async-resolve-stale/_config.js index bccf12562ad3..50bb414afc8b 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-resolve-stale/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-resolve-stale/_config.js @@ -20,7 +20,6 @@ export default test({ input.value = '12'; input.dispatchEvent(new Event('input', { bubbles: true })); await macrotask(6); - // TODO this is wrong (separate bug), this should be 3 | 12 - assert.htmlEqual(target.innerHTML, ' 5 | 12'); + assert.htmlEqual(target.innerHTML, ' 3 | 12'); } }); From 99711d582263ce3dc0103baecf579e995363db86 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Tue, 14 Oct 2025 18:25:52 +0200 Subject: [PATCH 15/15] fix: ensure map iteration order is correct (#16947) quick follow-up to #16944 Resetting a map entry does not change its position in the map when iterating. We need to make sure that reset makes that batch jump "to the front" for the "reject all stale batches" logic below. Edge case for which I can't come up with a test case but it _is_ a possibility. --- packages/svelte/src/internal/client/reactivity/deriveds.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index fa780013e15b..6aa9a1d9d920 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -144,6 +144,7 @@ export function async_derived(fn, location) { batch.increment(); deferreds.get(batch)?.reject(STALE_REACTION); + deferreds.delete(batch); // delete to ensure correct order in Map iteration below deferreds.set(batch, d); } }