diff --git a/.changeset/clever-months-clap.md b/.changeset/clever-months-clap.md new file mode 100644 index 0000000000..4a1b69e824 --- /dev/null +++ b/.changeset/clever-months-clap.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +perf: run blocks eagerly during flush instead of aborting diff --git a/.changeset/few-geese-itch.md b/.changeset/few-geese-itch.md new file mode 100644 index 0000000000..737aa01911 --- /dev/null +++ b/.changeset/few-geese-itch.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: destroy dynamic component instance before creating new one diff --git a/.changeset/itchy-games-guess.md b/.changeset/itchy-games-guess.md new file mode 100644 index 0000000000..44516cfc21 --- /dev/null +++ b/.changeset/itchy-games-guess.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't clone non-proxies in `$inspect` diff --git a/.changeset/six-shirts-scream.md b/.changeset/six-shirts-scream.md new file mode 100644 index 0000000000..ac4d183474 --- /dev/null +++ b/.changeset/six-shirts-scream.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: avoid recursion error when tagging circular references diff --git a/packages/svelte/src/internal/client/dev/inspect.js b/packages/svelte/src/internal/client/dev/inspect.js index c593f2622c..f79cf47299 100644 --- a/packages/svelte/src/internal/client/dev/inspect.js +++ b/packages/svelte/src/internal/client/dev/inspect.js @@ -26,7 +26,7 @@ export function inspect(get_value, inspector = console.log) { return; } - var snap = snapshot(value, true); + var snap = snapshot(value, true, true); untrack(() => { inspector(initial ? 'init' : 'update', ...snap); }); diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js index 2697722b39..be6611c96d 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js @@ -34,11 +34,6 @@ export function component(node, get_component, render_fn) { var pending_effect = null; function commit() { - if (effect) { - pause_effect(effect); - effect = null; - } - if (offscreen_fragment) { // remove the anchor /** @type {Text} */ (offscreen_fragment.lastChild).remove(); @@ -56,6 +51,11 @@ export function component(node, get_component, render_fn) { var defer = should_defer_append(); + if (effect) { + pause_effect(effect); + effect = null; + } + if (component) { var target = anchor; diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js index fb269e47e0..abc29a7670 100644 --- a/packages/svelte/src/internal/client/dom/operations.js +++ b/packages/svelte/src/internal/client/dom/operations.js @@ -6,6 +6,7 @@ import { get_descriptor, is_extensible } from '../../shared/utils.js'; import { active_effect } from '../runtime.js'; import { async_mode_flag } from '../../flags/index.js'; import { TEXT_NODE, EFFECT_RAN } from '#client/constants'; +import { eager_block_effects } from '../reactivity/batch.js'; // export these for reference in the compiled code, making global name deduplication unnecessary /** @type {Window} */ @@ -214,6 +215,7 @@ export function clear_text_content(node) { */ export function should_defer_append() { if (!async_mode_flag) return false; + if (eager_block_effects !== null) return false; var flags = /** @type {Effect} */ (active_effect).f; return (flags & EFFECT_RAN) !== 0; diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 3ae4b87ed5..dae3791eb0 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -93,9 +93,11 @@ export function proxy(value) { /** Used in dev for $inspect.trace() */ var path = ''; - + let updating = false; /** @param {string} new_path */ function update_path(new_path) { + if (updating) return; + updating = true; path = new_path; tag(version, `${path} version`); @@ -104,6 +106,7 @@ export function proxy(value) { for (const [prop, source] of sources) { tag(source, get_label(path, prop)); } + updating = false; } return new Proxy(/** @type {any} */ (value), { @@ -284,13 +287,13 @@ export function proxy(value) { if (s === undefined) { if (!has || get_descriptor(target, prop)?.writable) { s = with_parent(() => source(undefined, stack)); - set(s, proxy(value)); - - sources.set(prop, s); if (DEV) { tag(s, get_label(path, prop)); } + set(s, proxy(value)); + + sources.set(prop, s); } } else { has = s.v !== UNINITIALIZED; diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 123bc95d16..60fa03c56c 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -292,12 +292,12 @@ export class Batch { if (!skip && effect.fn !== null) { if (is_branch) { effect.f ^= CLEAN; + } else if ((flags & EFFECT) !== 0) { + this.#effects.push(effect); + } else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) { + this.#render_effects.push(effect); } else if ((flags & CLEAN) === 0) { - if ((flags & EFFECT) !== 0) { - this.#effects.push(effect); - } else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) { - this.#render_effects.push(effect); - } else if ((flags & ASYNC) !== 0) { + if ((flags & ASYNC) !== 0) { var effects = effect.b?.pending ? this.#boundary_async_effects : this.#async_effects; effects.push(effect); } else if (is_dirty(effect)) { @@ -584,6 +584,9 @@ function infinite_loop_guard() { } } +/** @type {Effect[] | null} */ +export let eager_block_effects = null; + /** * @param {Array} effects * @returns {void} @@ -598,7 +601,7 @@ function flush_queued_effects(effects) { var effect = effects[i++]; if ((effect.f & (DESTROYED | INERT)) === 0 && is_dirty(effect)) { - var n = current_batch ? current_batch.current.size : 0; + eager_block_effects = []; update_effect(effect); @@ -619,21 +622,20 @@ function flush_queued_effects(effects) { } } - // if state is written in a user effect, abort and re-schedule, lest we run - // effects that should be removed as a result of the state change - if ( - current_batch !== null && - current_batch.current.size > n && - (effect.f & USER_EFFECT) !== 0 - ) { - break; + if (eager_block_effects.length > 0) { + // TODO this feels incorrect! it gets the tests passing + old_values.clear(); + + for (const e of eager_block_effects) { + update_effect(e); + } + + eager_block_effects = []; } } } - while (i < length) { - schedule_effect(effects[i++]); - } + eager_block_effects = null; } /** diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 7fb3135708..cd0c28016d 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -33,7 +33,7 @@ import * as e from '../errors.js'; import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js'; import { get_stack, tag_proxy } from '../dev/tracing.js'; import { component_context, is_runes } from '../context.js'; -import { Batch, schedule_effect } from './batch.js'; +import { Batch, eager_block_effects, schedule_effect } from './batch.js'; import { proxy } from '../proxy.js'; import { execute_derived } from './deriveds.js'; @@ -334,6 +334,12 @@ function mark_reactions(signal, status) { if ((flags & DERIVED) !== 0) { mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY); } else if (not_dirty) { + if ((flags & BLOCK_EFFECT) !== 0) { + if (eager_block_effects !== null) { + eager_block_effects.push(/** @type {Effect} */ (reaction)); + } + } + schedule_effect(/** @type {Effect} */ (reaction)); } } diff --git a/packages/svelte/src/internal/shared/clone.js b/packages/svelte/src/internal/shared/clone.js index 4632fc3d68..b8f99ee198 100644 --- a/packages/svelte/src/internal/shared/clone.js +++ b/packages/svelte/src/internal/shared/clone.js @@ -15,14 +15,15 @@ const empty = []; * @template T * @param {T} value * @param {boolean} [skip_warning] + * @param {boolean} [no_tojson] * @returns {Snapshot} */ -export function snapshot(value, skip_warning = false) { +export function snapshot(value, skip_warning = false, no_tojson = false) { if (DEV && !skip_warning) { /** @type {string[]} */ const paths = []; - const copy = clone(value, new Map(), '', paths); + const copy = clone(value, new Map(), '', paths, null, no_tojson); if (paths.length === 1 && paths[0] === '') { // value could not be cloned w.state_snapshot_uncloneable(); @@ -40,7 +41,7 @@ export function snapshot(value, skip_warning = false) { return copy; } - return clone(value, new Map(), '', empty); + return clone(value, new Map(), '', empty, null, no_tojson); } /** @@ -49,10 +50,11 @@ export function snapshot(value, skip_warning = false) { * @param {Map>} cloned * @param {string} path * @param {string[]} paths - * @param {null | T} original The original value, if `value` was produced from a `toJSON` call + * @param {null | T} [original] The original value, if `value` was produced from a `toJSON` call + * @param {boolean} [no_tojson] * @returns {Snapshot} */ -function clone(value, cloned, path, paths, original = null) { +function clone(value, cloned, path, paths, original = null, no_tojson = false) { if (typeof value === 'object' && value !== null) { var unwrapped = cloned.get(value); if (unwrapped !== undefined) return unwrapped; @@ -71,7 +73,7 @@ function clone(value, cloned, path, paths, original = null) { for (var i = 0; i < value.length; i += 1) { var element = value[i]; if (i in value) { - copy[i] = clone(element, cloned, DEV ? `${path}[${i}]` : path, paths); + copy[i] = clone(element, cloned, DEV ? `${path}[${i}]` : path, paths, null, no_tojson); } } @@ -88,8 +90,15 @@ function clone(value, cloned, path, paths, original = null) { } for (var key in value) { - // @ts-expect-error - copy[key] = clone(value[key], cloned, DEV ? `${path}.${key}` : path, paths); + copy[key] = clone( + // @ts-expect-error + value[key], + cloned, + DEV ? `${path}.${key}` : path, + paths, + null, + no_tojson + ); } return copy; @@ -99,7 +108,7 @@ function clone(value, cloned, path, paths, original = null) { return /** @type {Snapshot} */ (structuredClone(value)); } - if (typeof (/** @type {T & { toJSON?: any } } */ (value).toJSON) === 'function') { + if (typeof (/** @type {T & { toJSON?: any } } */ (value).toJSON) === 'function' && !no_tojson) { return clone( /** @type {T & { toJSON(): any } } */ (value).toJSON(), cloned, diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-component-destroy-then-create/A.svelte b/packages/svelte/tests/runtime-runes/samples/dynamic-component-destroy-then-create/A.svelte new file mode 100644 index 0000000000..e73551cc28 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dynamic-component-destroy-then-create/A.svelte @@ -0,0 +1,8 @@ + + +

A

diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-component-destroy-then-create/B.svelte b/packages/svelte/tests/runtime-runes/samples/dynamic-component-destroy-then-create/B.svelte new file mode 100644 index 0000000000..459aa313c4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dynamic-component-destroy-then-create/B.svelte @@ -0,0 +1,8 @@ + + +

B

diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-component-destroy-then-create/_config.js b/packages/svelte/tests/runtime-runes/samples/dynamic-component-destroy-then-create/_config.js new file mode 100644 index 0000000000..e750c48a00 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dynamic-component-destroy-then-create/_config.js @@ -0,0 +1,13 @@ +import { test } from '../../test'; +import { flushSync } from 'svelte'; + +export default test({ + mode: ['client', 'hydrate'], + + async test({ assert, target, logs }) { + const [button] = target.querySelectorAll('button'); + + flushSync(() => button.click()); + assert.deepEqual(logs, ['create A', 'destroy A', 'create B']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-component-destroy-then-create/main.svelte b/packages/svelte/tests/runtime-runes/samples/dynamic-component-destroy-then-create/main.svelte new file mode 100644 index 0000000000..c9ef5430a4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dynamic-component-destroy-then-create/main.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/effect-loop-3/Child.svelte b/packages/svelte/tests/runtime-runes/samples/effect-loop-3/Child.svelte new file mode 100644 index 0000000000..9bf4db52d6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-loop-3/Child.svelte @@ -0,0 +1,13 @@ + + +{#if inited} + {@render children()} +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/effect-loop-3/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-loop-3/_config.js new file mode 100644 index 0000000000..046c190432 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-loop-3/_config.js @@ -0,0 +1,12 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [button] = target.querySelectorAll('button'); + + assert.doesNotThrow(() => { + flushSync(() => button.click()); + }); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/effect-loop-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/effect-loop-3/main.svelte new file mode 100644 index 0000000000..2b3a171798 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-loop-3/main.svelte @@ -0,0 +1,15 @@ + + + + +{#if show} + {#each { length: 1234 } as i} + {i} + {/each} +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-6/A.svelte b/packages/svelte/tests/runtime-runes/samples/effect-order-6/A.svelte index 2e789a0460..22b8ed0f20 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-order-6/A.svelte +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-6/A.svelte @@ -1,11 +1,12 @@ - + diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-6/B.svelte b/packages/svelte/tests/runtime-runes/samples/effect-order-6/B.svelte index 1fad19bc15..2233961177 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-order-6/B.svelte +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-6/B.svelte @@ -1,7 +1,5 @@ - - - -{#if object?.boolean} - - {@render children(object.boolean)} -{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-6/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-order-6/_config.js index 8f9077e954..91e1569e46 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-order-6/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-6/_config.js @@ -8,6 +8,6 @@ export default test({ flushSync(() => open.click()); flushSync(() => close.click()); - assert.deepEqual(logs, [true]); + assert.deepEqual(logs, [{ boolean: true, closed: false }]); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-6/main.svelte b/packages/svelte/tests/runtime-runes/samples/effect-order-6/main.svelte index eee487fa13..08f6fc48da 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-order-6/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-6/main.svelte @@ -1,6 +1,15 @@ @@ -15,9 +24,6 @@
- - {#snippet children(boolean)} - - {/snippet} - - +{#if object} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-7/A.svelte b/packages/svelte/tests/runtime-runes/samples/effect-order-7/A.svelte index 54f4869d62..b55c8f4115 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-order-7/A.svelte +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-7/A.svelte @@ -1,9 +1,9 @@ -{boolean} +{boolean} {closed} - + diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-7/B.svelte b/packages/svelte/tests/runtime-runes/samples/effect-order-7/B.svelte index 2a2e634db1..7b33c342f5 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-order-7/B.svelte +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-7/B.svelte @@ -1,7 +1,5 @@ - - - -{#if object?.nested} - - {@render children(object.nested)} -{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-7/main.svelte b/packages/svelte/tests/runtime-runes/samples/effect-order-7/main.svelte index c9c45c50cf..d9e9eb17ad 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-order-7/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-7/main.svelte @@ -1,6 +1,15 @@ @@ -15,8 +24,6 @@
- - {#snippet children(nested)} -
- {/snippet} - +{#if object} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-recursive-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/inspect-recursive-2/main.svelte index f7874d2192..23a1c237c3 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-recursive-2/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/inspect-recursive-2/main.svelte @@ -1,21 +1,15 @@ \ No newline at end of file