preserve DIRTY/MAYBE_DIRTY status of deferred effects

pull/16487/head
Rich Harris 1 month ago
parent a826136d74
commit 0134d04c16

@ -10,7 +10,8 @@ import {
INERT, INERT,
RENDER_EFFECT, RENDER_EFFECT,
ROOT_EFFECT, ROOT_EFFECT,
USER_EFFECT USER_EFFECT,
MAYBE_DIRTY
} from '#client/constants'; } from '#client/constants';
import { async_mode_flag } from '../../flags/index.js'; import { async_mode_flag } from '../../flags/index.js';
import { deferred, define_property } from '../../shared/utils.js'; import { deferred, define_property } from '../../shared/utils.js';
@ -146,6 +147,18 @@ export class Batch {
*/ */
#block_effects = []; #block_effects = [];
/**
* Deferred effects (which run after async work has completed) that are DIRTY
* @type {Effect[]}
*/
#dirty_effects = [];
/**
* Deferred effects that are MAYBE_DIRTY
* @type {Effect[]}
*/
#maybe_dirty_effects = [];
/** /**
* A set of branches that still exist, but will be destroyed when this batch * A set of branches that still exist, but will be destroyed when this batch
* is committed we skip over these during `process` * is committed we skip over these during `process`
@ -221,10 +234,9 @@ export class Batch {
this.#deferred?.resolve(); this.#deferred?.resolve();
} else { } else {
// otherwise mark effects clean so they get scheduled on the next run this.#defer_effects(this.#render_effects);
for (const e of this.#render_effects) set_signal_status(e, CLEAN); this.#defer_effects(this.#effects);
for (const e of this.#effects) set_signal_status(e, CLEAN); this.#defer_effects(this.#block_effects);
for (const e of this.#block_effects) set_signal_status(e, CLEAN);
} }
if (current_values) { if (current_values) {
@ -307,6 +319,21 @@ export class Batch {
} }
} }
/**
* @param {Effect[]} effects
*/
#defer_effects(effects) {
for (const e of effects) {
const target = (e.f & DIRTY) !== 0 ? this.#dirty_effects : this.#maybe_dirty_effects;
target.push(e);
// mark as clean so they get scheduled if they depend on pending async state
set_signal_status(e, CLEAN);
}
effects.length = 0;
}
/** /**
* Associate a change to a given source with the current * Associate a change to a given source with the current
* batch, noting its previous and current values * batch, noting its previous and current values
@ -384,18 +411,13 @@ export class Batch {
this.#pending -= 1; this.#pending -= 1;
if (this.#pending === 0) { if (this.#pending === 0) {
for (const e of this.#render_effects) { for (const e of this.#dirty_effects) {
set_signal_status(e, DIRTY); set_signal_status(e, DIRTY);
schedule_effect(e); schedule_effect(e);
} }
for (const e of this.#effects) { for (const e of this.#maybe_dirty_effects) {
set_signal_status(e, DIRTY); set_signal_status(e, MAYBE_DIRTY);
schedule_effect(e);
}
for (const e of this.#block_effects) {
set_signal_status(e, DIRTY);
schedule_effect(e); schedule_effect(e);
} }

@ -0,0 +1,28 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
await tick();
const [increment] = target.querySelectorAll('button');
assert.deepEqual(logs, [false]);
assert.htmlEqual(target.innerHTML, '<button>increment</button><p>0</p>');
increment.click();
await tick();
assert.deepEqual(logs, [false]);
assert.htmlEqual(target.innerHTML, '<button>increment</button><p>1</p>');
increment.click();
await tick();
assert.deepEqual(logs, [false, true]);
assert.htmlEqual(target.innerHTML, '<button>increment</button><p>2</p>');
increment.click();
await tick();
assert.deepEqual(logs, [false, true]);
assert.htmlEqual(target.innerHTML, '<button>increment</button><p>3</p>');
}
});

@ -0,0 +1,17 @@
<script lang="ts">
let count = $state(0);
let two_or_larger = $derived(count >= 2);
$effect(() => {
console.log(two_or_larger);
});
</script>
<svelte:boundary>
<button onclick={() => count += 1}>increment</button>
<p>{await count}</p>
{#snippet pending()}
<p>loading...</p>
{/snippet}
</svelte:boundary>
Loading…
Cancel
Save