fix: wait with resolving prior batches until current one commits

Tweaks the timing when we resolve earlier/outdated batches. Instead of doing that right away we only do it once the current batch is committed. That way
- the old batch will not render right away in case this was the last pending value, while the current batch is still pending, causing tearing (because we partly render values of the current batch in the old batch already)
- the old batch still has a chance to resolve itself in the meantime if the current batch is still pending in other areas
pull/18181/head
Simon Holthausen 3 months ago
parent cc505f0ec5
commit 036a8b6221

@ -107,6 +107,13 @@ export class Batch {
*/
previous = new Map();
/**
* Async effects which this batch doesn't take into account anymore when calculating blockers,
* as it has a value for it already.
* @type {Set<Effect>}
*/
unblocked = new Set();
/**
* When the batch is committed (and the DOM is updated), we need to remove old branches
* and append new ones by calling the functions added inside (if/each/key/etc) blocks
@ -198,6 +205,8 @@ export class Batch {
#is_blocked() {
for (const batch of this.#blockers) {
for (const effect of batch.#blocking_pending.keys()) {
if (this.unblocked.has(effect)) continue;
var skipped = false;
var e = effect;

@ -120,7 +120,7 @@ export function async_derived(fn, label, location) {
var promise = /** @type {Promise<V>} */ (/** @type {unknown} */ (undefined));
var signal = source(/** @type {V} */ (UNINITIALIZED));
if (DEV) signal.label = label;
if (DEV) signal.label = label ?? fn.toString();
// only suspend in async deriveds created on initialisation
var should_suspend = !active_reaction;
@ -213,6 +213,10 @@ export function async_derived(fn, label, location) {
decrement_pending?.();
if (error === OBSOLETE || (effect.f & DESTROYED) !== 0) {
return;
}
batch.activate();
if (error) {
@ -229,8 +233,18 @@ export function async_derived(fn, label, location) {
// All prior async derived runs are now stale
for (const [b, d] of deferreds) {
if (b.id <= batch.id) deferreds.delete(b);
if (b.id < batch.id) d.resolve(value);
if (b.id === batch.id) deferreds.delete(b);
if (b.id < batch.id) {
// Don't delete + resolve directly, instead only do that once
// the current batch commits. This way we avoid tearing when
// `b` is rendering through the early resolve while `batch` is
// still pending.
batch.unblocked.add(effect);
batch.oncommit(() => {
deferreds.delete(b);
d.resolve(value);
});
}
}
if (DEV && location !== undefined) {

@ -0,0 +1,34 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [button1, button2, pop, shift] = target.querySelectorAll('button');
const [p] = target.querySelectorAll('p');
button1.click();
await tick();
button2.click();
await tick();
assert.htmlEqual(p.innerHTML, `0 + 0 = 0 | 0 0`);
shift.click();
await tick();
assert.htmlEqual(p.innerHTML, `0 + 0 = 0 | 0 0`);
pop.click();
await tick();
assert.htmlEqual(p.innerHTML, `0 + 0 = 0 | 0 0`);
pop.click();
await tick();
assert.htmlEqual(p.innerHTML, `1 + 0 = 1 | 1 0`);
shift.click();
await tick();
pop.click();
await tick();
assert.htmlEqual(p.innerHTML, `1 + 2 = 3 | 1 1`);
}
});

@ -0,0 +1,21 @@
<script>
const queue1 = [];
const queue2 = [];
let a = $state(0);
let b = $state(0);
let c = $state(0);
let d = $state(0)
function push(value, where = 1) {
if (!value) return value;
return new Promise(r => (where === 1 ? queue1 : queue2).push(() => r(value)));
}
</script>
<button onclick={() => {a++;c++}}>a / c</button>
<button onclick={() => {b+=2;d++}}>b / d</button>
<button onclick={() => queue1.pop()?.()}>pop 1</button>
<button onclick={() => queue2.shift()?.()}>shift 2</button>
<p>{a} + {b} = {await push(a + b)} | {await push(c, 2)} {await push(d, 2)}</p>
Loading…
Cancel
Save