From e01bd97befd8181b0c1050b156062abc5b98eeaa Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 15 Jul 2025 19:58:39 -0400 Subject: [PATCH 1/4] fix: update `$effect.pending()` immediately after a batch is removed (#16382) * WIP sync effect pending updates * fix * changeset * fix * add test * inline * unused --- .changeset/popular-tips-lie.md | 5 ++ .../internal/client/dom/blocks/boundary.js | 14 ++-- .../src/internal/client/reactivity/batch.js | 17 +++- .../samples/async-effect-pending/_config.js | 81 +++++++++++++++++++ .../samples/async-effect-pending/main.svelte | 32 ++++++++ 5 files changed, 142 insertions(+), 7 deletions(-) create mode 100644 .changeset/popular-tips-lie.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-effect-pending/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-effect-pending/main.svelte diff --git a/.changeset/popular-tips-lie.md b/.changeset/popular-tips-lie.md new file mode 100644 index 000000000000..45bd3e23f358 --- /dev/null +++ b/.changeset/popular-tips-lie.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: update `$effect.pending()` immediately after a batch is removed diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 5ed0515210ec..5e678ab113ce 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -22,7 +22,7 @@ import { get_next_sibling } from '../operations.js'; import { queue_micro_task } from '../task.js'; import * as e from '../../errors.js'; import { DEV } from 'esm-env'; -import { Batch } from '../../reactivity/batch.js'; +import { Batch, effect_pending_updates } from '../../reactivity/batch.js'; import { internal_set, source } from '../../reactivity/sources.js'; import { tag } from '../../dev/tracing.js'; import { createSubscriber } from '../../../../reactivity/create-subscriber.js'; @@ -92,6 +92,12 @@ export class Boundary { */ #effect_pending = null; + #effect_pending_update = () => { + if (this.#effect_pending) { + internal_set(this.#effect_pending, this.#pending_count); + } + }; + #effect_pending_subscriber = createSubscriber(() => { this.#effect_pending = source(this.#pending_count); @@ -238,11 +244,7 @@ export class Boundary { this.parent.#update_pending_count(d); } - queueMicrotask(() => { - if (this.#effect_pending) { - internal_set(this.#effect_pending, this.#pending_count); - } - }); + effect_pending_updates.add(this.#effect_pending_update); } get_effect_pending() { diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 1126946ce9b1..f881330e90df 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -49,6 +49,9 @@ export let batch_deriveds = null; /** @type {Effect[]} Stack of effects, dev only */ export let dev_effect_stack = []; +/** @type {Set<() => void>} */ +export let effect_pending_updates = new Set(); + /** @type {Effect[]} */ let queued_root_effects = []; @@ -296,6 +299,16 @@ export class Batch { deactivate() { current_batch = null; + + for (const update of effect_pending_updates) { + effect_pending_updates.delete(update); + update(); + + if (current_batch !== null) { + // only do one at a time + break; + } + } } neuter() { @@ -319,7 +332,7 @@ export class Batch { batches.delete(this); } - current_batch = null; + this.deactivate(); } flush_effects() { @@ -389,6 +402,8 @@ export class Batch { this.#effects = []; this.flush(); + } else { + this.deactivate(); } } diff --git a/packages/svelte/tests/runtime-runes/samples/async-effect-pending/_config.js b/packages/svelte/tests/runtime-runes/samples/async-effect-pending/_config.js new file mode 100644 index 000000000000..9df362079827 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-effect-pending/_config.js @@ -0,0 +1,81 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [increment, shift] = target.querySelectorAll('button'); + + shift.click(); + shift.click(); + shift.click(); + + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

0

+

0

+

0

+

pending: 0

+ ` + ); + + increment.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

0

+

0

+

0

+

pending: 3

+ ` + ); + + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

0

+

0

+

0

+

pending: 2

+ ` + ); + + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

0

+

0

+

0

+

pending: 1

+ ` + ); + + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

1

+

1

+

1

+

pending: 0

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-effect-pending/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-effect-pending/main.svelte new file mode 100644 index 000000000000..89cead2cc6b1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-effect-pending/main.svelte @@ -0,0 +1,32 @@ + + + + + + +

{await push(value)}

+

{await push(value)}

+

{await push(value)}

+ +

pending: {$effect.pending()}

+ + {#snippet pending()} +

loading...

+ {/snippet} +
+ + From 8e73fd4b0343cae992429b7e75812acf0ec6cb6c Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Wed, 16 Jul 2025 05:14:19 -0700 Subject: [PATCH 2/4] fix: don't log `await_reactivity_loss` warning when signal is read in `untrack` (#16385) * fix: don't log `await_reactivity_loss` warning when signal is read in `untrack` * add test * fix --- .changeset/big-readers-lie.md | 5 +++++ packages/svelte/src/internal/client/runtime.js | 2 +- .../samples/async-reactivity-loss/_config.js | 7 +++++-- .../samples/async-reactivity-loss/main.svelte | 11 +++++++---- 4 files changed, 18 insertions(+), 7 deletions(-) create mode 100644 .changeset/big-readers-lie.md diff --git a/.changeset/big-readers-lie.md b/.changeset/big-readers-lie.md new file mode 100644 index 000000000000..9f5dd166c1e1 --- /dev/null +++ b/.changeset/big-readers-lie.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't log `await_reactivity_loss` warning when signal is read in `untrack` diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 9fdb87239b45..6c4d92bbadf5 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -609,7 +609,7 @@ export function get(signal) { var tracking = (current_async_effect.f & REACTION_IS_UPDATING) !== 0; var was_read = current_async_effect.deps?.includes(signal); - if (!tracking && !was_read) { + if (!tracking && !untracking && !was_read) { w.await_reactivity_loss(/** @type {string} */ (signal.label)); var trace = get_stack('TracedAt'); diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js index f8a7cfd479af..16318a3b44c2 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js @@ -6,11 +6,14 @@ export default test({ dev: true }, - html: `

pending

`, + html: `

pending

`, async test({ assert, target, warnings }) { await tick(); - assert.htmlEqual(target.innerHTML, '

3

3

'); + assert.htmlEqual( + target.innerHTML, + '

6

6

' + ); assert.equal( warnings[0], diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/main.svelte index bdb1b095c9bc..03596ce05130 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/main.svelte @@ -1,18 +1,21 @@ + -

{await a_plus_b()}

-

{await a + await b}

+

{await a_plus_b_plus_c()}

+

{await a + await b + await c}

{#snippet pending()}

pending

From 6cf3a193428f08a1c15b403dad681627425c0f1d Mon Sep 17 00:00:00 2001 From: 7nik Date: Wed, 16 Jul 2025 16:55:15 +0300 Subject: [PATCH 3/4] fix: better handle $inspect on array mutations (#16389) * fix: better handle $inspect on array mutations * increase stack trace limit, revert test change * second changeset --------- Co-authored-by: Rich Harris --- .changeset/curvy-houses-jog.md | 5 ++ .changeset/metal-coats-thank.md | 5 ++ packages/svelte/src/internal/client/proxy.js | 60 +++++++++++++++---- .../src/internal/client/reactivity/sources.js | 40 ++++++++----- .../samples/array-delete-item/_config.js | 13 ++++ .../samples/array-delete-item/main.svelte | 8 +++ .../samples/inspect-deep-array/_config.js | 13 +--- packages/svelte/tests/suite.ts | 3 + 8 files changed, 110 insertions(+), 37 deletions(-) create mode 100644 .changeset/curvy-houses-jog.md create mode 100644 .changeset/metal-coats-thank.md create mode 100644 packages/svelte/tests/runtime-runes/samples/array-delete-item/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/array-delete-item/main.svelte diff --git a/.changeset/curvy-houses-jog.md b/.changeset/curvy-houses-jog.md new file mode 100644 index 000000000000..0a2323d8e4f0 --- /dev/null +++ b/.changeset/curvy-houses-jog.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: better handle $inspect on array mutations diff --git a/.changeset/metal-coats-thank.md b/.changeset/metal-coats-thank.md new file mode 100644 index 000000000000..b307fd3d45c0 --- /dev/null +++ b/.changeset/metal-coats-thank.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: leave proxied array `length` untouched when deleting properties diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 5da1b7e188fd..3ae4b87ed5d6 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -15,7 +15,13 @@ import { is_array, object_prototype } from '../shared/utils.js'; -import { state as source, set, increment } from './reactivity/sources.js'; +import { + state as source, + set, + increment, + flush_inspect_effects, + set_inspect_effects_deferred +} from './reactivity/sources.js'; import { PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants'; import { UNINITIALIZED } from '../../constants.js'; import * as e from './errors.js'; @@ -80,6 +86,9 @@ export function proxy(value) { // We need to create the length source eagerly to ensure that // mutations to the array are properly synced with our proxy sources.set('length', source(/** @type {any[]} */ (value).length, stack)); + if (DEV) { + value = /** @type {any} */ (inspectable_array(/** @type {any[]} */ (value))); + } } /** Used in dev for $inspect.trace() */ @@ -142,16 +151,6 @@ export function proxy(value) { } } } else { - // When working with arrays, we need to also ensure we update the length when removing - // an indexed property - if (is_proxied_array && typeof prop === 'string') { - var ls = /** @type {Source} */ (sources.get('length')); - var n = Number(prop); - - if (Number.isInteger(n) && n < ls.v) { - set(ls, n); - } - } set(s, UNINITIALIZED); increment(version); } @@ -388,3 +387,42 @@ export function get_proxied_value(value) { export function is(a, b) { return Object.is(get_proxied_value(a), get_proxied_value(b)); } + +const ARRAY_MUTATING_METHODS = new Set([ + 'copyWithin', + 'fill', + 'pop', + 'push', + 'reverse', + 'shift', + 'sort', + 'splice', + 'unshift' +]); + +/** + * Wrap array mutating methods so $inspect is triggered only once and + * to prevent logging an array in intermediate state (e.g. with an empty slot) + * @param {any[]} array + */ +function inspectable_array(array) { + return new Proxy(array, { + get(target, prop, receiver) { + var value = Reflect.get(target, prop, receiver); + if (!ARRAY_MUTATING_METHODS.has(/** @type {string} */ (prop))) { + return value; + } + + /** + * @this {any[]} + * @param {any[]} args + */ + return function (...args) { + set_inspect_effects_deferred(); + var result = value.apply(this, args); + flush_inspect_effects(); + return result; + }; + } + }); +} diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 29c657bdd038..bd55b9d935f4 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -49,6 +49,12 @@ export function set_inspect_effects(v) { inspect_effects = v; } +let inspect_effects_deferred = false; + +export function set_inspect_effects_deferred() { + inspect_effects_deferred = true; +} + /** * @template V * @param {V} v @@ -213,26 +219,32 @@ export function internal_set(source, value) { } } - if (DEV && inspect_effects.size > 0) { - const inspects = Array.from(inspect_effects); + if (DEV && inspect_effects.size > 0 && !inspect_effects_deferred) { + flush_inspect_effects(); + } + } + + return value; +} - for (const effect of inspects) { - // Mark clean inspect-effects as maybe dirty and then check their dirtiness - // instead of just updating the effects - this way we avoid overfiring. - if ((effect.f & CLEAN) !== 0) { - set_signal_status(effect, MAYBE_DIRTY); - } +export function flush_inspect_effects() { + inspect_effects_deferred = false; - if (is_dirty(effect)) { - update_effect(effect); - } - } + const inspects = Array.from(inspect_effects); - inspect_effects.clear(); + for (const effect of inspects) { + // Mark clean inspect-effects as maybe dirty and then check their dirtiness + // instead of just updating the effects - this way we avoid overfiring. + if ((effect.f & CLEAN) !== 0) { + set_signal_status(effect, MAYBE_DIRTY); + } + + if (is_dirty(effect)) { + update_effect(effect); } } - return value; + inspect_effects.clear(); } /** diff --git a/packages/svelte/tests/runtime-runes/samples/array-delete-item/_config.js b/packages/svelte/tests/runtime-runes/samples/array-delete-item/_config.js new file mode 100644 index 000000000000..71341170672b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/array-delete-item/_config.js @@ -0,0 +1,13 @@ +import { ok, test } from '../../test'; +import { flushSync } from 'svelte'; + +export default test({ + mode: ['client'], + async test({ target, assert, logs }) { + const btn = target.querySelector('button'); + + flushSync(() => btn?.click()); + + assert.deepEqual(logs[0], [0, , 2]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/array-delete-item/main.svelte b/packages/svelte/tests/runtime-runes/samples/array-delete-item/main.svelte new file mode 100644 index 000000000000..ca00a85491ec --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/array-delete-item/main.svelte @@ -0,0 +1,8 @@ + + + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-deep-array/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-deep-array/_config.js index 0e6b12508b0f..49f1b5de4170 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-deep-array/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/inspect-deep-array/_config.js @@ -13,17 +13,6 @@ export default test({ button?.click(); }); - assert.deepEqual(logs, [ - 'init', - [1, 2, 3, 7], - 'update', - [2, 2, 3, 7], - 'update', - [2, 3, 3, 7], - 'update', - [2, 3, 7, 7], - 'update', - [2, 3, 7] - ]); + assert.deepEqual(logs, ['init', [1, 2, 3, 7], 'update', [2, 3, 7]]); } }); diff --git a/packages/svelte/tests/suite.ts b/packages/svelte/tests/suite.ts index 6954b8b683f6..bbd252b8e188 100644 --- a/packages/svelte/tests/suite.ts +++ b/packages/svelte/tests/suite.ts @@ -20,6 +20,9 @@ const filter = process.env.FILTER ) : /./; +// this defaults to 10, which is too low for some of our tests +Error.stackTraceLimit = 100; + export function suite(fn: (config: Test, test_dir: string) => void) { return { test: (config: Test) => config, From be44f8b23ecf14bb5945c06dd775000bcc483eaf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 16 Jul 2025 10:45:47 -0400 Subject: [PATCH 4/4] Version Packages (#16384) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/big-readers-lie.md | 5 ----- .changeset/curvy-houses-jog.md | 5 ----- .changeset/metal-coats-thank.md | 5 ----- .changeset/popular-tips-lie.md | 5 ----- packages/svelte/CHANGELOG.md | 12 ++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 7 files changed, 14 insertions(+), 22 deletions(-) delete mode 100644 .changeset/big-readers-lie.md delete mode 100644 .changeset/curvy-houses-jog.md delete mode 100644 .changeset/metal-coats-thank.md delete mode 100644 .changeset/popular-tips-lie.md diff --git a/.changeset/big-readers-lie.md b/.changeset/big-readers-lie.md deleted file mode 100644 index 9f5dd166c1e1..000000000000 --- a/.changeset/big-readers-lie.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: don't log `await_reactivity_loss` warning when signal is read in `untrack` diff --git a/.changeset/curvy-houses-jog.md b/.changeset/curvy-houses-jog.md deleted file mode 100644 index 0a2323d8e4f0..000000000000 --- a/.changeset/curvy-houses-jog.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: better handle $inspect on array mutations diff --git a/.changeset/metal-coats-thank.md b/.changeset/metal-coats-thank.md deleted file mode 100644 index b307fd3d45c0..000000000000 --- a/.changeset/metal-coats-thank.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"svelte": patch ---- - -fix: leave proxied array `length` untouched when deleting properties diff --git a/.changeset/popular-tips-lie.md b/.changeset/popular-tips-lie.md deleted file mode 100644 index 45bd3e23f358..000000000000 --- a/.changeset/popular-tips-lie.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: update `$effect.pending()` immediately after a batch is removed diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 8c0b21364d46..2ea2f58de85b 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,17 @@ # svelte +## 5.36.3 + +### Patch Changes + +- fix: don't log `await_reactivity_loss` warning when signal is read in `untrack` ([#16385](https://github.com/sveltejs/svelte/pull/16385)) + +- fix: better handle $inspect on array mutations ([#16389](https://github.com/sveltejs/svelte/pull/16389)) + +- fix: leave proxied array `length` untouched when deleting properties ([#16389](https://github.com/sveltejs/svelte/pull/16389)) + +- fix: update `$effect.pending()` immediately after a batch is removed ([#16382](https://github.com/sveltejs/svelte/pull/16382)) + ## 5.36.2 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 6e000e2bbf05..119c4142cfda 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.2", + "version": "5.36.3", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 9ccd2ff0ce6d..e8b0e6f1ee71 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.2'; +export const VERSION = '5.36.3'; export const PUBLIC_VERSION = '5'; pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy