diff --git a/.changeset/khaki-states-train.md b/.changeset/khaki-states-train.md new file mode 100644 index 0000000000..5d4054fb15 --- /dev/null +++ b/.changeset/khaki-states-train.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: properly unlink batches diff --git a/.changeset/neat-groups-grin.md b/.changeset/neat-groups-grin.md new file mode 100644 index 0000000000..1d615216f0 --- /dev/null +++ b/.changeset/neat-groups-grin.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: settle discarded batch diff --git a/.changeset/sad-shoes-help.md b/.changeset/sad-shoes-help.md new file mode 100644 index 0000000000..76db87ba95 --- /dev/null +++ b/.changeset/sad-shoes-help.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: resume outro-ed branches if they were kept around diff --git a/.changeset/swift-terms-exist.md b/.changeset/swift-terms-exist.md new file mode 100644 index 0000000000..cc69f1b3b1 --- /dev/null +++ b/.changeset/swift-terms-exist.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: avoid waterfall-warning when async resolves to same value diff --git a/documentation/docs/98-reference/.generated/compile-warnings.md b/documentation/docs/98-reference/.generated/compile-warnings.md index fb53c644cf..f372a010a5 100644 --- a/documentation/docs/98-reference/.generated/compile-warnings.md +++ b/documentation/docs/98-reference/.generated/compile-warnings.md @@ -842,7 +842,7 @@ Reassignments of module-level declarations will not cause reactive statements to ### script_unknown_attribute ``` -Unrecognized attribute — should be one of `generics`, `lang` or `module`. If this exists for a preprocessor, ensure that the preprocessor removes it +Unrecognised attribute — should be one of `generics`, `lang` or `module`. If this exists for a preprocessor, ensure that the preprocessor removes it ``` ### slot_element_deprecated diff --git a/packages/svelte/messages/compile-warnings/template.md b/packages/svelte/messages/compile-warnings/template.md index 3650e07b47..f1e32a6d18 100644 --- a/packages/svelte/messages/compile-warnings/template.md +++ b/packages/svelte/messages/compile-warnings/template.md @@ -107,7 +107,7 @@ This code will work when the component is rendered on the client (which is why t ## script_unknown_attribute -> Unrecognized attribute — should be one of `generics`, `lang` or `module`. If this exists for a preprocessor, ensure that the preprocessor removes it +> Unrecognised attribute — should be one of `generics`, `lang` or `module`. If this exists for a preprocessor, ensure that the preprocessor removes it ## slot_element_deprecated diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js index dffa79cd7a..8b4891da8e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js @@ -213,7 +213,7 @@ export function VariableDeclaration(node, context) { location ? b.literal(location) : undefined ); - call = should_save ? save(call) : b.await(call); + call = should_save ? save(call, true) : b.await(call); declarations.push(b.declarator(declarator.id, call)); } else { @@ -251,7 +251,7 @@ export function VariableDeclaration(node, context) { location ? b.literal(location) : undefined ); - call = should_save ? save(call) : b.await(call); + call = should_save ? save(call, true) : b.await(call); } declarations.push(b.declarator(id, call)); diff --git a/packages/svelte/src/compiler/utils/ast.js b/packages/svelte/src/compiler/utils/ast.js index 75aadd905b..8d6df28f31 100644 --- a/packages/svelte/src/compiler/utils/ast.js +++ b/packages/svelte/src/compiler/utils/ast.js @@ -633,7 +633,8 @@ export function has_await_expression(node) { /** * Turns `await ...` to `(await $.save(...))()` * @param {ESTree.Expression} expression + * @param {boolean} unset */ -export function save(expression) { - return b.call(b.await(b.call('$.save', expression))); +export function save(expression, unset = false) { + return b.call(b.await(b.call('$.save', expression, unset && b.true))); } diff --git a/packages/svelte/src/compiler/warnings.js b/packages/svelte/src/compiler/warnings.js index c7d660a617..98f407671d 100644 --- a/packages/svelte/src/compiler/warnings.js +++ b/packages/svelte/src/compiler/warnings.js @@ -804,11 +804,11 @@ export function script_context_deprecated(node) { } /** - * Unrecognized attribute — should be one of `generics`, `lang` or `module`. If this exists for a preprocessor, ensure that the preprocessor removes it + * Unrecognised attribute — should be one of `generics`, `lang` or `module`. If this exists for a preprocessor, ensure that the preprocessor removes it * @param {null | NodeLike} node */ export function script_unknown_attribute(node) { - w(node, 'script_unknown_attribute', `Unrecognized attribute — should be one of \`generics\`, \`lang\` or \`module\`. If this exists for a preprocessor, ensure that the preprocessor removes it\nhttps://svelte.dev/e/script_unknown_attribute`); + w(node, 'script_unknown_attribute', `Unrecognised attribute — should be one of \`generics\`, \`lang\` or \`module\`. If this exists for a preprocessor, ensure that the preprocessor removes it\nhttps://svelte.dev/e/script_unknown_attribute`); } /** diff --git a/packages/svelte/src/internal/client/dom/blocks/branches.js b/packages/svelte/src/internal/client/dom/blocks/branches.js index 44e55f90fc..bd5d4aac11 100644 --- a/packages/svelte/src/internal/client/dom/blocks/branches.js +++ b/packages/svelte/src/internal/client/dom/blocks/branches.js @@ -111,6 +111,8 @@ export class BranchManager { var offscreen = this.#offscreen.get(key); if (offscreen) { + // effect could have been outro'ed before through a prior batch — resume if necessary + resume_effect(offscreen.effect); this.#onscreen.set(key, offscreen.effect); this.#offscreen.delete(key); diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index fe97fd10d9..64cd032a33 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -26,6 +26,7 @@ import { set_reactivity_loss_tracker } from './deriveds.js'; import { aborted } from './effects.js'; +import { queue_micro_task } from '../dom/task.js'; /** * @param {Blocker[]} blockers @@ -152,13 +153,25 @@ export function capture() { * `await a + b` becomes `(await $.save(a))() + b` * @template T * @param {Promise} promise + * @param {boolean} unset * @returns {Promise<() => T>} */ -export async function save(promise) { +export async function save(promise, unset) { + var batch = current_batch; var restore = capture(); var value = await promise; return () => { + if (unset) { + // If this is happening outside the context of an async derived, + // context will not automatically be unset + queue_micro_task(() => { + if (batch === current_batch) { + unset_context(); + } + }); + } + restore(); return value; }; @@ -357,15 +370,15 @@ export function wait(blockers) { */ export function increment_pending() { var effect = /** @type {Effect} */ (active_effect); - var boundary = /** @type {Boundary} */ (effect.b); + var boundary = effect.b; // undefined if called outside the render tree, e.g. a standalone $effect.root var batch = /** @type {Batch} */ (current_batch); - var blocking = boundary.is_rendered(); + var blocking = !!boundary?.is_rendered(); - boundary.update_pending_count(1, batch); + boundary?.update_pending_count(1, batch); batch.increment(blocking, effect); return () => { - boundary.update_pending_count(-1, batch); + boundary?.update_pending_count(-1, batch); batch.decrement(blocking, effect); }; } diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 82c97cf95c..21e2cc6922 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -393,31 +393,30 @@ export class Batch { var next_batch = /** @type {Batch | null} */ (/** @type {unknown} */ (current_batch)); - if (this.linked && this.#pending === 0) { + if (this.#pending === 0 && (this.#roots.length === 0 || next_batch !== null)) { this.#unlink(); - } - // Order matters here - we need to commit and THEN continue flushing new batches, not the other way around, - // else we could start flushing a new batch and then, if it has pending work, rebase it right afterwards, which is wrong. - // In sync mode flushSync can cause #commit to wrongfully think that there needs to be a rebase, so we only do it in async mode - // TODO fix the underlying cause, otherwise this will likely regress when non-async mode is removed - if (async_mode_flag && !this.linked) { - this.#commit(); - // Rebases can activate other batches or null it out, therefore restore the new one here - current_batch = next_batch; + // Order matters here - we need to commit and THEN continue flushing new batches, not the other way around, + // else we could start flushing a new batch and then, if it has pending work, rebase it right afterwards, which is wrong. + // In sync mode flushSync can cause #commit to wrongfully think that there needs to be a rebase, so we only do it in async mode + // TODO fix the underlying cause, otherwise this will likely regress when non-async mode is removed + if (async_mode_flag) { + this.#commit(); + // Rebases can activate other batches or null it out, therefore restore the new one here + current_batch = next_batch; + } } // Edge case: During traversal new branches might create effects that run immediately and set state, // causing an effect and therefore a root to be scheduled again. We need to traverse the current batch // once more in that case - most of the time this will just clean up dirty branches. if (this.#roots.length > 0) { - if (next_batch === null) { + if (next_batch !== null) { + const batch = next_batch; + batch.#roots.push(...this.#roots.filter((r) => !batch.#roots.includes(r))); + } else { next_batch = this; - this.#link(); } - - const batch = next_batch; - batch.#roots.push(...this.#roots.filter((r) => !batch.#roots.includes(r))); } if (next_batch !== null) { @@ -633,6 +632,7 @@ export class Batch { this.#fork_commit_callbacks.clear(); this.#unlink(); + this.#deferred?.resolve(); } /** @@ -643,8 +643,6 @@ export class Batch { } #commit() { - this.#unlink(); - // If there are other pending batches, they now need to be 'rebased' — // in other words, we re-run block/async effects with the newly // committed state, unless the batch in question has a more diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 7ea6de6306..fd64f3b45d 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -230,9 +230,7 @@ export function async_derived(fn, label, location) { signal.f ^= ERROR_VALUE; } - internal_set(signal, value); - - if (DEV && location !== undefined) { + if (DEV && location !== undefined && !signal.equals(value)) { recent_async_deriveds.add(signal); setTimeout(() => { @@ -242,6 +240,8 @@ export function async_derived(fn, label, location) { } }); } + + internal_set(signal, value); } batch.deactivate(); diff --git a/packages/svelte/tests/runtime-runes/samples/async-async-disconnected-effect-root/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-async-disconnected-effect-root/main.svelte deleted file mode 100644 index 37c8919023..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/async-async-disconnected-effect-root/main.svelte +++ /dev/null @@ -1,11 +0,0 @@ - diff --git a/packages/svelte/tests/runtime-runes/samples/async-branch-reintro/_config.js b/packages/svelte/tests/runtime-runes/samples/async-branch-reintro/_config.js new file mode 100644 index 0000000000..928db008e6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-branch-reintro/_config.js @@ -0,0 +1,58 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + const [inc_count, inc_both, shift] = target.querySelectorAll('button'); + + inc_both.click(); + await tick(); + inc_count.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + 0 + 0 + + ` + ); + + shift.click(); + await tick(); + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + 1 + 2 + + ` + ); + + const button = /** @type {HTMLButtonElement} */ (target.querySelector('button:last-child')); + button.click(); + await tick(); + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + 2 + 2 + + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-branch-reintro/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-branch-reintro/main.svelte new file mode 100644 index 0000000000..92b7669fa9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-branch-reintro/main.svelte @@ -0,0 +1,23 @@ + + + + + + +{await push(other)} +{#if count % 2 === 0} + {await push(count)} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-same-value/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-same-value/_config.js new file mode 100644 index 0000000000..b4eb464e23 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-same-value/_config.js @@ -0,0 +1,19 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + compileOptions: { dev: true }, + async test({ assert, target, warnings }) { + await tick(); + const [button] = target.querySelectorAll('button'); + + button.click(); + await tick(); + assert.htmlEqual(target.innerHTML, ''); + + button.click(); + await tick(); + assert.htmlEqual(target.innerHTML, ''); + assert.deepEqual(warnings, []); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-same-value/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-same-value/main.svelte new file mode 100644 index 0000000000..78475047cd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-same-value/main.svelte @@ -0,0 +1,12 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-async-disconnected-effect-root/_config.js b/packages/svelte/tests/runtime-runes/samples/async-disconnected-effect-root/_config.js similarity index 57% rename from packages/svelte/tests/runtime-runes/samples/async-async-disconnected-effect-root/_config.js rename to packages/svelte/tests/runtime-runes/samples/async-disconnected-effect-root/_config.js index ecf048abc3..fdc773751b 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-async-disconnected-effect-root/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-disconnected-effect-root/_config.js @@ -1,9 +1,15 @@ +import { tick } from 'svelte'; import { test } from '../../test'; export default test({ // Test that an async derived inside an $effect.root not connected to the component tree still works async test({ assert, logs }) { await new Promise((resolve) => setTimeout(resolve, 10)); - assert.deepEqual(logs, [1]); + assert.deepEqual(logs, [1, 1]); + const [button] = document.querySelectorAll('button'); + + button.click(); + await tick(); + assert.deepEqual(logs, [1, 1, 2]); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/async-disconnected-effect-root/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-disconnected-effect-root/main.svelte new file mode 100644 index 0000000000..3b42acf171 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-disconnected-effect-root/main.svelte @@ -0,0 +1,19 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-settled-discard/_config.js b/packages/svelte/tests/runtime-runes/samples/async-settled-discard/_config.js new file mode 100644 index 0000000000..29ed72d90a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-settled-discard/_config.js @@ -0,0 +1,37 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + await tick(); + const [increment, pop] = target.querySelectorAll('button'); + + increment.click(); + await tick(); + increment.click(); + await tick(); + pop.click(); + await tick(); + assert.deepEqual(logs, ['settled 2', 'settled 2']); + assert.htmlEqual( + target.innerHTML, + ` + 2 + + + ` + ); + + pop.click(); + await tick(); + assert.deepEqual(logs, ['settled 2', 'settled 2']); + assert.htmlEqual( + target.innerHTML, + ` + 2 + + + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-settled-discard/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-settled-discard/main.svelte new file mode 100644 index 0000000000..c3675f8add --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-settled-discard/main.svelte @@ -0,0 +1,22 @@ + + +{await push(count)} + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-unset-context-after-restore/_config.js b/packages/svelte/tests/runtime-runes/samples/async-unset-context-after-restore/_config.js new file mode 100644 index 0000000000..ad543e91ae --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-unset-context-after-restore/_config.js @@ -0,0 +1,14 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, logs }) { + await new Promise((resolve) => setTimeout(resolve, 10)); + assert.deepEqual(logs, [1, 1]); + const [button] = document.querySelectorAll('button'); + + button.click(); + await tick(); + assert.deepEqual(logs, [1, 1, 2]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-unset-context-after-restore/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-unset-context-after-restore/main.svelte new file mode 100644 index 0000000000..a39c75fb94 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-unset-context-after-restore/main.svelte @@ -0,0 +1,15 @@ + + + diff --git a/packages/svelte/tests/validator/samples/script-invalid-spread-attribute/warnings.json b/packages/svelte/tests/validator/samples/script-invalid-spread-attribute/warnings.json index c6748f711d..35045313ba 100644 --- a/packages/svelte/tests/validator/samples/script-invalid-spread-attribute/warnings.json +++ b/packages/svelte/tests/validator/samples/script-invalid-spread-attribute/warnings.json @@ -1,7 +1,7 @@ [ { "code": "script_unknown_attribute", - "message": "Unrecognized attribute — should be one of `generics`, `lang` or `module`. If this exists for a preprocessor, ensure that the preprocessor removes it", + "message": "Unrecognised attribute — should be one of `generics`, `lang` or `module`. If this exists for a preprocessor, ensure that the preprocessor removes it", "start": { "column": 8, "line": 1 diff --git a/packages/svelte/tests/validator/samples/script-unknown-attribute/warnings.json b/packages/svelte/tests/validator/samples/script-unknown-attribute/warnings.json index fdcad269a6..0d41c2dc12 100644 --- a/packages/svelte/tests/validator/samples/script-unknown-attribute/warnings.json +++ b/packages/svelte/tests/validator/samples/script-unknown-attribute/warnings.json @@ -1,7 +1,7 @@ [ { "code": "script_unknown_attribute", - "message": "Unrecognized attribute — should be one of `generics`, `lang` or `module`. If this exists for a preprocessor, ensure that the preprocessor removes it", + "message": "Unrecognised attribute — should be one of `generics`, `lang` or `module`. If this exists for a preprocessor, ensure that the preprocessor removes it", "start": { "column": 8, "line": 1