Skip to content

Commit 0ee1d56

Browse files
committed
fix memory leak, fix branch commit bug
1 parent 0f4d0b1 commit 0ee1d56

File tree

9 files changed

+52
-37
lines changed

9 files changed

+52
-37
lines changed

.changeset/tame-ears-invite.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: ensure obsolete batches are removed and its necessary dom changes committed

packages/svelte/src/internal/client/dom/blocks/each.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
187187
}
188188
}
189189

190-
block(() => {
190+
var b = block(() => {
191191
// store a reference to the effect so that we can update the start/end nodes in reconciliation
192192
each_effect ??= /** @type {Effect} */ (active_effect);
193193

@@ -310,7 +310,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
310310
}
311311
}
312312

313-
batch.add_callback(commit);
313+
batch.add_callback(() => b, commit);
314314
} else {
315315
commit();
316316
}

packages/svelte/src/internal/client/dom/blocks/if.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export function if_block(node, fn, elseif = false) {
124124
if (active) batch.skipped_effects.delete(active);
125125
if (inactive) batch.skipped_effects.add(inactive);
126126

127-
batch.add_callback(commit);
127+
batch.add_callback(() => b, commit);
128128
} else {
129129
commit();
130130
}
@@ -135,7 +135,7 @@ export function if_block(node, fn, elseif = false) {
135135
}
136136
};
137137

138-
block(() => {
138+
var b = block(() => {
139139
has_branch = false;
140140
fn(set_branch);
141141
if (!has_branch) {

packages/svelte/src/internal/client/dom/blocks/key.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export function key(node, get_key, render_fn) {
5252
effect = pending_effect;
5353
}
5454

55-
block(() => {
55+
var b = block(() => {
5656
if (changed(key, (key = get_key()))) {
5757
var target = anchor;
5858

@@ -66,7 +66,7 @@ export function key(node, get_key, render_fn) {
6666
pending_effect = branch(() => render_fn(target));
6767

6868
if (defer) {
69-
/** @type {Batch} */ (current_batch).add_callback(commit);
69+
/** @type {Batch} */ (current_batch).add_callback(() => b, commit);
7070
} else {
7171
commit();
7272
}

packages/svelte/src/internal/client/dom/blocks/svelte-component.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export function component(node, get_component, render_fn) {
5151
pending_effect = null;
5252
}
5353

54-
block(() => {
54+
var b = block(() => {
5555
if (component === (component = get_component())) return;
5656

5757
var defer = should_defer_append();
@@ -70,7 +70,7 @@ export function component(node, get_component, render_fn) {
7070
}
7171

7272
if (defer) {
73-
/** @type {Batch} */ (current_batch).add_callback(commit);
73+
/** @type {Batch} */ (current_batch).add_callback(() => b, commit);
7474
} else {
7575
commit();
7676
}

packages/svelte/src/internal/client/reactivity/batch.js

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,12 @@ export class Batch {
9595

9696
/**
9797
* When the batch is committed (and the DOM is updated), we need to remove old branches
98-
* and append new ones by calling the functions added inside (if/each/key/etc) blocks
99-
* @type {Set<() => void>}
98+
* and append new ones by calling the functions added inside (if/each/key/etc) blocks.
99+
* Key is a function that returns the block effect because #callbacks will be called before
100+
* the block effect reference exists, so we need to capture it in a closure.
101+
* @type {Map<() => Effect, () => void>}
100102
*/
101-
#callbacks = new Set();
103+
#callbacks = new Map();
102104

103105
/**
104106
* The number of async effects that are currently in flight
@@ -112,12 +114,6 @@ export class Batch {
112114
*/
113115
#deferred = null;
114116

115-
/**
116-
* True if an async effect inside this batch resolved and
117-
* its parent branch was already deleted
118-
*/
119-
#neutered = false;
120-
121117
/**
122118
* Async effects (created inside `async_derived`) encountered during processing.
123119
* These run after the rest of the batch has updated, since they should
@@ -233,8 +229,20 @@ export class Batch {
233229
// if we didn't start any new async work, and no async work
234230
// is outstanding from a previous flush, commit
235231
if (this.#async_effects.length === 0 && this.#pending === 0) {
236-
for (const batch of superseeded_batches) {
237-
batch.remove();
232+
if (superseeded_batches.length > 0) {
233+
const own = [...this.#callbacks.keys()].map((c) => c());
234+
for (const batch of superseeded_batches) {
235+
// A superseeded batch could have callbacks for e.g. destroying if blocks
236+
// that are not part of the current batch because it already happened in the prior one,
237+
// and the corresponding block effect therefore returning early because nothing was changed from its
238+
// point of view, therefore not adding a callback to the current batch, so we gotta call them here.
239+
for (const [effect, cb] of batch.#callbacks) {
240+
if (!own.includes(effect())) {
241+
cb();
242+
}
243+
}
244+
batch.remove();
245+
}
238246
}
239247

240248
this.#commit();
@@ -394,12 +402,13 @@ export class Batch {
394402
}
395403
}
396404

397-
neuter() {
398-
this.#neutered = true;
399-
}
400-
401405
remove() {
402-
this.neuter();
406+
this.#callbacks.clear();
407+
this.#maybe_dirty_effects =
408+
this.#dirty_effects =
409+
this.#boundary_async_effects =
410+
this.#async_effects =
411+
[];
403412
batches.delete(this);
404413
}
405414

@@ -427,10 +436,8 @@ export class Batch {
427436
* Append and remove branches to/from the DOM
428437
*/
429438
#commit() {
430-
if (!this.#neutered) {
431-
for (const fn of this.#callbacks) {
432-
fn();
433-
}
439+
for (const fn of this.#callbacks.values()) {
440+
fn();
434441
}
435442

436443
this.#callbacks.clear();
@@ -463,9 +470,12 @@ export class Batch {
463470
}
464471
}
465472

466-
/** @param {() => void} fn */
467-
add_callback(fn) {
468-
this.#callbacks.add(fn);
473+
/**
474+
* @param {() => Effect} effect
475+
* @param {() => void} fn
476+
*/
477+
add_callback(effect, fn) {
478+
this.#callbacks.set(effect, fn);
469479
}
470480

471481
settled() {

packages/svelte/src/internal/client/reactivity/deriveds.js

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -184,12 +184,6 @@ export function async_derived(fn, location) {
184184
};
185185

186186
promise.then(handler, (e) => handler(null, e || 'unknown'));
187-
188-
if (batch) {
189-
return () => {
190-
queueMicrotask(() => batch.neuter());
191-
};
192-
}
193187
});
194188

195189
if (DEV) {

packages/svelte/tests/runtime-runes/samples/async-redirect/_config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export default test({
2929
<button>c</button>
3030
<button>ok</button>
3131
<p>c</p>
32+
<p>b or c</p>
3233
`
3334
);
3435

@@ -46,6 +47,7 @@ export default test({
4647
<button>c</button>
4748
<button>ok</button>
4849
<p>b</p>
50+
<p>b or c</p>
4951
`
5052
);
5153
}

packages/svelte/tests/runtime-runes/samples/async-redirect/main.svelte

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@
3333
<p>c</p>
3434
{/if}
3535

36+
{#if route === 'b' || route === 'c'}
37+
<p>b or c</p>
38+
{/if}
39+
3640
{#snippet pending()}
3741
<p>pending...</p>
3842
{/snippet}

0 commit comments

Comments
 (0)