diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js
index 35b6430b60..877eb3ff84 100644
--- a/packages/svelte/src/internal/client/dom/blocks/each.js
+++ b/packages/svelte/src/internal/client/dom/blocks/each.js
@@ -166,6 +166,13 @@ function destroy_effects(state, to_destroy, remove_dom = true) {
const fragment = document.createDocumentFragment();
move_effect(e, fragment);
+
+ // `reconcile` expects offscreen items to come after all onscreen items
+ if (e !== state.effect.last) {
+ link(state, e.prev, e.next);
+ link(state, state.effect.last, e);
+ link(state, e, null);
+ }
} else {
destroy_effect(to_destroy[i], remove_dom);
}
diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js
index 281e6fa0ff..eca4a35c48 100644
--- a/packages/svelte/src/internal/client/reactivity/batch.js
+++ b/packages/svelte/src/internal/client/reactivity/batch.js
@@ -404,18 +404,24 @@ export class Batch {
* @param {Effect} effect
*/
unskip_effect(effect) {
- var tracked = this.#skipped_branches.get(effect);
- if (tracked) {
- this.#skipped_branches.delete(effect);
-
- for (var e of tracked.d) {
- this.schedule(e, DIRTY);
- }
+ for (var batch = /** @type {Batch | null} */ (this); batch !== null; batch = batch.prev) {
+ // Earlier (non-fork) batches that skip this branch have reset the effects inside it that are
+ // dirty because of their changes. This batch sees those changes as well, so it runs them, too
+ var tracked =
+ batch === this || !batch.is_fork ? batch.#skipped_branches.get(effect) : undefined;
+
+ if (tracked) {
+ for (var e of tracked.d) {
+ this.schedule(e, DIRTY);
+ }
- for (e of tracked.m) {
- this.schedule(e, MAYBE_DIRTY);
+ for (e of tracked.m) {
+ this.schedule(e, MAYBE_DIRTY);
+ }
}
}
+
+ this.#skipped_branches.delete(effect);
this.unskipped_branches.add(effect);
}
diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-item-skipped-stale/_config.js b/packages/svelte/tests/runtime-runes/samples/async-each-item-skipped-stale/_config.js
new file mode 100644
index 0000000000..6adae749b5
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-each-item-skipped-stale/_config.js
@@ -0,0 +1,34 @@
+import { tick } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ mode: ['client'],
+ async test({ assert, target }) {
+ await tick();
+
+ const [skip, readd, resolve_earlier, resolve_all] = target.querySelectorAll('button');
+ const buttons =
+ '';
+
+ assert.htmlEqual(target.innerHTML, `${buttons}0|01|1`);
+
+ // the second item is skipped by this batch, which also changes what it shows
+ skip.click();
+ await tick();
+ // the second item is needed again by a later batch
+ readd.click();
+ await tick();
+
+ // resolve the earlier batch first, so that it commits first
+ resolve_earlier.click();
+ await tick();
+ assert.htmlEqual(target.innerHTML, `${buttons}3|6`);
+
+ for (let i = 0; i < 5; i++) {
+ await tick();
+ resolve_all.click();
+ }
+ await tick();
+ assert.htmlEqual(target.innerHTML, `${buttons}5|101|2`);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-item-skipped-stale/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-each-item-skipped-stale/main.svelte
new file mode 100644
index 0000000000..b5347b9832
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-each-item-skipped-stale/main.svelte
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+{#each list as x}{x}|{await delay(x * c)}{/each}
diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-keyed-order-overlapping/_config.js b/packages/svelte/tests/runtime-runes/samples/async-each-keyed-order-overlapping/_config.js
new file mode 100644
index 0000000000..09f57c9166
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-each-keyed-order-overlapping/_config.js
@@ -0,0 +1,28 @@
+import { tick } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ mode: ['client'],
+ async test({ assert, target }) {
+ await tick();
+
+ const [set_b, list_1, list_2, resolve_all] = target.querySelectorAll('button');
+ const buttons =
+ '';
+
+ assert.htmlEqual(target.innerHTML, `${buttons}01`);
+
+ set_b.click();
+ await tick();
+ list_1.click();
+ await tick();
+ list_2.click();
+
+ for (let i = 0; i < 5; i++) {
+ await tick();
+ resolve_all.click();
+ }
+ await tick();
+ assert.htmlEqual(target.innerHTML, `${buttons}425`);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-keyed-order-overlapping/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-each-keyed-order-overlapping/main.svelte
new file mode 100644
index 0000000000..ec5153cd84
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-each-keyed-order-overlapping/main.svelte
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+{#each list as x (x)}{await delay(x + b)}{/each}
diff --git a/packages/svelte/tests/runtime-runes/samples/async-if-skipped-branch-stale/_config.js b/packages/svelte/tests/runtime-runes/samples/async-if-skipped-branch-stale/_config.js
new file mode 100644
index 0000000000..dad1f1b244
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-if-skipped-branch-stale/_config.js
@@ -0,0 +1,34 @@
+import { tick } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ mode: ['client'],
+ async test({ assert, target }) {
+ await tick();
+
+ const [skip, show, resolve_earlier, resolve_all] = target.querySelectorAll('button');
+ const buttons =
+ '';
+
+ assert.htmlEqual(target.innerHTML, `${buttons}
p0
c0i0`); + + // the branch is skipped by this batch, which also changes what the branch shows + skip.click(); + await tick(); + // the branch is needed again by a later batch + show.click(); + await tick(); + + // resolve the earlier batch first, so that it commits first + resolve_earlier.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}p1
c0`); + + for (let i = 0; i < 5; i++) { + await tick(); + resolve_all.click(); + } + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}p1
c1i1`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-if-skipped-branch-stale/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-if-skipped-branch-stale/main.svelte new file mode 100644 index 0000000000..c903b007f1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-if-skipped-branch-stale/main.svelte @@ -0,0 +1,43 @@ + + + + + + + +{await delay('p' + b)}
+{await delay('c' + c)} +{#if show}{await delay('i' + b)}{/if}