diff --git a/.changeset/async-hydration-orphan-nodes.md b/.changeset/async-hydration-orphan-nodes.md new file mode 100644 index 0000000000..4c35d6b74e --- /dev/null +++ b/.changeset/async-hydration-orphan-nodes.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: properly track effect end node for async sibling component \ No newline at end of file diff --git a/.changeset/rare-donkeys-repair.md b/.changeset/rare-donkeys-repair.md new file mode 100644 index 0000000000..bd22c564db --- /dev/null +++ b/.changeset/rare-donkeys-repair.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: prevent false-positive reactivity loss warning diff --git a/.changeset/soft-moments-exist.md b/.changeset/soft-moments-exist.md new file mode 100644 index 0000000000..71a3148afa --- /dev/null +++ b/.changeset/soft-moments-exist.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: bump esrap dependency diff --git a/packages/svelte/package.json b/packages/svelte/package.json index b3f08354b5..d6cb24ab37 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -188,7 +188,7 @@ "clsx": "^2.1.1", "devalue": "^5.8.1", "esm-env": "^1.2.1", - "esrap": "^2.2.9", + "esrap": "^2.2.11", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", diff --git a/packages/svelte/src/internal/client/dom/blocks/async.js b/packages/svelte/src/internal/client/dom/blocks/async.js index 170529a6b9..78e2ed44be 100644 --- a/packages/svelte/src/internal/client/dom/blocks/async.js +++ b/packages/svelte/src/internal/client/dom/blocks/async.js @@ -9,6 +9,7 @@ import { set_hydrating, skip_nodes } from '../hydration.js'; +import { assign_nodes } from '../template.js'; /** * @param {TemplateNode} node @@ -23,6 +24,7 @@ export function async(node, blockers = [], expressions = [], fn) { if (was_hydrating) { hydrate_next(); end = skip_nodes(false); + assign_nodes(node, end); // Necessary if this wraps the sole child of a block, else end marker can be wrong } if (expressions.length === 0 && blockers.every((b) => b.settled)) { diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index 2f2d741447..8cb76a9226 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -185,10 +185,10 @@ export async function save(promise) { * @returns {Promise<() => T>} */ export async function track_reactivity_loss(promise) { - var previous_async_effect = reactivity_loss_tracker; + var previous_reactivity_loss_tracker = reactivity_loss_tracker; // Ensure that unrelated reads after an async operation is kicked off don't cause false positives queueMicrotask(() => { - if (reactivity_loss_tracker === previous_async_effect) { + if (reactivity_loss_tracker === previous_reactivity_loss_tracker) { set_reactivity_loss_tracker(null); } }); @@ -196,12 +196,12 @@ export async function track_reactivity_loss(promise) { var value = await promise; return () => { - set_reactivity_loss_tracker(previous_async_effect); + set_reactivity_loss_tracker(previous_reactivity_loss_tracker); // While this can result in false negatives it also guards against the more important // false positives that would occur if this is the last in a chain of async operations, // and the reactivity_loss_tracker would then stay around until the next async operation happens. queueMicrotask(() => { - if (reactivity_loss_tracker === previous_async_effect) { + if (reactivity_loss_tracker === previous_reactivity_loss_tracker) { set_reactivity_loss_tracker(null); } }); diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 01220d52e3..45171ca4a7 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -51,6 +51,7 @@ import { batch_values, current_batch, flushSync, + previous_batch, schedule_effect } from './reactivity/batch.js'; import { handle_error } from './error-handling.js'; @@ -586,6 +587,11 @@ export function get(signal) { if ( !untracking && reactivity_loss_tracker && + // By checking that current/previous batch are null we filter out false positives. + // reactivity_loss_tracker is only reset after a microtask, so if a flush happens + // before that, we get warnings for things we shouldn't warn on. + current_batch === null && + previous_batch === null && !reactivity_loss_tracker.warned && (reactivity_loss_tracker.effect.f & REACTION_IS_UPDATING) === 0 && !reactivity_loss_tracker.effect_deps.has(signal) diff --git a/packages/svelte/tests/hydration/test.ts b/packages/svelte/tests/hydration/test.ts index d2bf6fde34..0050130817 100644 --- a/packages/svelte/tests/hydration/test.ts +++ b/packages/svelte/tests/hydration/test.ts @@ -72,7 +72,7 @@ const { test, run } = suite(async (config, cwd) => { const target = window.document.body; const head = window.document.head; - const rendered = render((await import(`${cwd}/_output/server/main.svelte.js`)).default, { + const rendered = await render((await import(`${cwd}/_output/server/main.svelte.js`)).default, { props: config.server_props ?? config.props ?? {}, idPrefix: config?.id_prefix }); @@ -80,8 +80,8 @@ const { test, run } = suite(async (config, cwd) => { const override = read(`${cwd}/_override.html`); const override_head = read(`${cwd}/_override_head.html`); - fs.writeFileSync(`${cwd}/_output/body.html`, rendered.html + '\n'); - target.innerHTML = override ?? rendered.html; + fs.writeFileSync(`${cwd}/_output/body.html`, rendered.body + '\n'); + target.innerHTML = override ?? rendered.body; if (rendered.head) { fs.writeFileSync(`${cwd}/_output/head.html`, rendered.head + '\n'); @@ -145,7 +145,7 @@ const { test, run } = suite(async (config, cwd) => { flushSync(); - const expected = read(`${cwd}/_expected.html`) ?? rendered.html; + const expected = read(`${cwd}/_expected.html`) ?? rendered.body; assert_html_equal(target.innerHTML, expected); if (rendered.head) { diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-no-false-positive-4/_config.js b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-no-false-positive-4/_config.js new file mode 100644 index 0000000000..4bd8adc9c5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-no-false-positive-4/_config.js @@ -0,0 +1,15 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + compileOptions: { dev: true }, + async test({ assert, target, warnings }) { + await tick(); + const [increment] = target.querySelectorAll('button'); + + increment.click(); + await new Promise((resolve) => setTimeout(resolve, 10)); + assert.htmlEqual(target.innerHTML, ' 1 1'); + assert.deepEqual(warnings, []); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-no-false-positive-4/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-no-false-positive-4/main.svelte new file mode 100644 index 0000000000..d47d99e652 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-no-false-positive-4/main.svelte @@ -0,0 +1,16 @@ + + + + +{await x} +{y} diff --git a/packages/svelte/tests/runtime-runes/samples/async-sole-if-child/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-sole-if-child/Child.svelte new file mode 100644 index 0000000000..304b7bd8a9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-sole-if-child/Child.svelte @@ -0,0 +1,7 @@ + + +
+