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/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/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/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/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/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/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'; 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/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} +
+ + 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

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, 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