Skip to content

Commit 53417ea

Browse files
authored
fix: preserve dirty status of deferred effects (#16487)
* don't mark_reactions inside decrement, it can cause infinite loops * revert mark_reactions changes * preserve DIRTY/MAYBE_DIRTY status of deferred effects * changeset * tweak
1 parent f882095 commit 53417ea

File tree

5 files changed

+101
-21
lines changed

5 files changed

+101
-21
lines changed

.changeset/cool-insects-argue.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: preserve dirty status of deferred effects

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

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import {
1010
INERT,
1111
RENDER_EFFECT,
1212
ROOT_EFFECT,
13-
USER_EFFECT
13+
USER_EFFECT,
14+
MAYBE_DIRTY
1415
} from '#client/constants';
1516
import { async_mode_flag } from '../../flags/index.js';
1617
import { deferred, define_property } from '../../shared/utils.js';
@@ -27,7 +28,7 @@ import * as e from '../errors.js';
2728
import { flush_tasks } from '../dom/task.js';
2829
import { DEV } from 'esm-env';
2930
import { invoke_error_boundary } from '../error-handling.js';
30-
import { mark_reactions, old_values } from './sources.js';
31+
import { old_values } from './sources.js';
3132
import { unlink_effect } from './effects.js';
3233
import { unset_context } from './async.js';
3334

@@ -146,6 +147,18 @@ export class Batch {
146147
*/
147148
#block_effects = [];
148149

150+
/**
151+
* Deferred effects (which run after async work has completed) that are DIRTY
152+
* @type {Effect[]}
153+
*/
154+
#dirty_effects = [];
155+
156+
/**
157+
* Deferred effects that are MAYBE_DIRTY
158+
* @type {Effect[]}
159+
*/
160+
#maybe_dirty_effects = [];
161+
149162
/**
150163
* A set of branches that still exist, but will be destroyed when this batch
151164
* is committed — we skip over these during `process`
@@ -221,10 +234,9 @@ export class Batch {
221234

222235
this.#deferred?.resolve();
223236
} else {
224-
// otherwise mark effects clean so they get scheduled on the next run
225-
for (const e of this.#render_effects) set_signal_status(e, CLEAN);
226-
for (const e of this.#effects) set_signal_status(e, CLEAN);
227-
for (const e of this.#block_effects) set_signal_status(e, CLEAN);
237+
this.#defer_effects(this.#render_effects);
238+
this.#defer_effects(this.#effects);
239+
this.#defer_effects(this.#block_effects);
228240
}
229241

230242
if (current_values) {
@@ -271,15 +283,15 @@ export class Batch {
271283
if (!skip && effect.fn !== null) {
272284
if (is_branch) {
273285
effect.f ^= CLEAN;
274-
} else if ((flags & EFFECT) !== 0) {
275-
this.#effects.push(effect);
276-
} else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) {
277-
this.#render_effects.push(effect);
278-
} else if (is_dirty(effect)) {
279-
if ((flags & ASYNC) !== 0) {
286+
} else if ((flags & CLEAN) === 0) {
287+
if ((flags & EFFECT) !== 0) {
288+
this.#effects.push(effect);
289+
} else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) {
290+
this.#render_effects.push(effect);
291+
} else if ((flags & ASYNC) !== 0) {
280292
var effects = effect.b?.pending ? this.#boundary_async_effects : this.#async_effects;
281293
effects.push(effect);
282-
} else {
294+
} else if (is_dirty(effect)) {
283295
if ((effect.f & BLOCK_EFFECT) !== 0) this.#block_effects.push(effect);
284296
update_effect(effect);
285297
}
@@ -303,6 +315,21 @@ export class Batch {
303315
}
304316
}
305317

318+
/**
319+
* @param {Effect[]} effects
320+
*/
321+
#defer_effects(effects) {
322+
for (const e of effects) {
323+
const target = (e.f & DIRTY) !== 0 ? this.#dirty_effects : this.#maybe_dirty_effects;
324+
target.push(e);
325+
326+
// mark as clean so they get scheduled if they depend on pending async state
327+
set_signal_status(e, CLEAN);
328+
}
329+
330+
effects.length = 0;
331+
}
332+
306333
/**
307334
* Associate a change to a given source with the current
308335
* batch, noting its previous and current values
@@ -380,8 +407,14 @@ export class Batch {
380407
this.#pending -= 1;
381408

382409
if (this.#pending === 0) {
383-
for (const source of this.current.keys()) {
384-
mark_reactions(source, DIRTY, false);
410+
for (const e of this.#dirty_effects) {
411+
set_signal_status(e, DIRTY);
412+
schedule_effect(e);
413+
}
414+
415+
for (const e of this.#maybe_dirty_effects) {
416+
set_signal_status(e, MAYBE_DIRTY);
417+
schedule_effect(e);
385418
}
386419

387420
this.#render_effects = [];

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

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -301,10 +301,9 @@ export function increment(source) {
301301
/**
302302
* @param {Value} signal
303303
* @param {number} status should be DIRTY or MAYBE_DIRTY
304-
* @param {boolean} schedule_async
305304
* @returns {void}
306305
*/
307-
export function mark_reactions(signal, status, schedule_async = true) {
306+
function mark_reactions(signal, status) {
308307
var reactions = signal.reactions;
309308
if (reactions === null) return;
310309

@@ -324,16 +323,14 @@ export function mark_reactions(signal, status, schedule_async = true) {
324323
continue;
325324
}
326325

327-
var should_schedule = (flags & DIRTY) === 0 && (schedule_async || (flags & ASYNC) === 0);
328-
329326
// don't set a DIRTY reaction to MAYBE_DIRTY
330-
if (should_schedule) {
327+
if ((flags & DIRTY) === 0) {
331328
set_signal_status(reaction, status);
332329
}
333330

334331
if ((flags & DERIVED) !== 0) {
335332
mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY);
336-
} else if (should_schedule) {
333+
} else {
337334
schedule_effect(/** @type {Effect} */ (reaction));
338335
}
339336
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { tick } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
async test({ assert, target, logs }) {
6+
await tick();
7+
8+
const [increment] = target.querySelectorAll('button');
9+
10+
assert.deepEqual(logs, [false]);
11+
assert.htmlEqual(target.innerHTML, '<button>increment</button><p>0</p>');
12+
13+
increment.click();
14+
await tick();
15+
assert.deepEqual(logs, [false]);
16+
assert.htmlEqual(target.innerHTML, '<button>increment</button><p>1</p>');
17+
18+
increment.click();
19+
await tick();
20+
assert.deepEqual(logs, [false, true]);
21+
assert.htmlEqual(target.innerHTML, '<button>increment</button><p>2</p>');
22+
23+
increment.click();
24+
await tick();
25+
assert.deepEqual(logs, [false, true]);
26+
assert.htmlEqual(target.innerHTML, '<button>increment</button><p>3</p>');
27+
}
28+
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<script lang="ts">
2+
let count = $state(0);
3+
let two_or_larger = $derived(count >= 2);
4+
5+
$effect(() => {
6+
console.log(two_or_larger);
7+
});
8+
</script>
9+
10+
<svelte:boundary>
11+
<button onclick={() => count += 1}>increment</button>
12+
<p>{await count}</p>
13+
14+
{#snippet pending()}
15+
<p>loading...</p>
16+
{/snippet}
17+
</svelte:boundary>

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy