diff --git a/.changeset/chatty-snails-train.md b/.changeset/chatty-snails-train.md new file mode 100644 index 0000000000..817621178b --- /dev/null +++ b/.changeset/chatty-snails-train.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure signal graph is consistent before triggering $inspect signals diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 5bdd199908..e3af722ea5 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -13,6 +13,7 @@ import { import { equals, safe_equals } from './equality.js'; import * as e from '../errors.js'; import { destroy_effect } from './effects.js'; +import { inspect_effects, set_inspect_effects } from './sources.js'; /** * @template V @@ -92,6 +93,8 @@ export function update_derived(derived) { var value; if (DEV) { + let prev_inspect_effects = inspect_effects; + set_inspect_effects(new Set()); try { if (stack.includes(derived)) { e.derived_references_self(); @@ -102,6 +105,7 @@ export function update_derived(derived) { destroy_derived_children(derived); value = update_reaction(derived); } finally { + set_inspect_effects(prev_inspect_effects); stack.pop(); } } else { diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 1d72faea43..c32cc450e1 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -15,7 +15,8 @@ import { increment_version, update_effect, derived_sources, - set_derived_sources + set_derived_sources, + flush_sync } from '../runtime.js'; import { equals, safe_equals } from './equality.js'; import { @@ -29,7 +30,14 @@ import { } from '../constants.js'; import * as e from '../errors.js'; -let inspect_effects = new Set(); +export let inspect_effects = new Set(); + +/** + * @param {Set} v + */ +export function set_inspect_effects(v) { + inspect_effects = v; +} /** * @template V @@ -159,11 +167,19 @@ export function set(source, value) { } } - if (DEV) { - for (const effect of inspect_effects) { + if (DEV && inspect_effects.size > 0) { + const inspects = Array.from(inspect_effects); + if (current_effect === null) { + // Triggering an effect sync can tear the signal graph, so to avoid this we need + // to ensure the graph has been flushed before triggering any inspect effects. + // Only needed when there's currently no effect, and flushing with one present + // could have other unintended consequences, like effects running out of order. + // This is expensive, but given this is a DEV mode only feature, it should be fine + flush_sync(); + } + for (const effect of inspects) { update_effect(effect); } - inspect_effects.clear(); } } diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-recursive/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-recursive/_config.js new file mode 100644 index 0000000000..e35917b1f3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/inspect-recursive/_config.js @@ -0,0 +1,16 @@ +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + }, + + async test({ assert, logs, target }) { + const [btn] = target.querySelectorAll('button'); + btn.click(); + btn.click(); + await Promise.resolve(); + + assert.deepEqual(logs, ['init', [], 'update', [{}], 'update', [{}, {}]]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-recursive/main.svelte b/packages/svelte/tests/runtime-runes/samples/inspect-recursive/main.svelte new file mode 100644 index 0000000000..7a2030fa31 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/inspect-recursive/main.svelte @@ -0,0 +1,35 @@ + + + +