resolve obsolete batch together with the latest batch that has overlap with it

batch-ordering-fixes
Simon Holthausen 23 hours ago
parent 3464c3d5b7
commit 2d97120abb

@ -672,19 +672,36 @@ export class Batch {
// If this is the last value that was resolved we still might want to flush the batch to bring it to completion,
// else it might become a zombie batch that is never removed from the `batches` Set
const current = new Map(this.current);
const current = new Map([...this.current].filter(([_, [, is_derived]]) => !is_derived));
let latest_overlap;
for (const batch of batches) {
if (batch.id <= this.id) continue;
for (const source of batch.current.keys()) {
if (current.has(source)) latest_overlap = batch;
current.delete(source);
}
}
// If the subsequent batches combined superseed this one, we can discard it.
// Otherwise we'll need to flush it to update the UI with the distinct changes.
if (current.size === 0) {
// If the subsequent batches combined superseed this one, we can discard it.
this.discard();
return;
} else if (latest_overlap) {
// Else we find the latest subsequent batch that overlaps with this one and make this one block on it,
// so that the changes resolve together with the rest of the changes in that batch. This guarantees
// glitch-free UI without tearing.
this.#blockers.add(latest_overlap); // TODO strictly speaking we should resolve this batch before the other one but in practise it hopefully doesn't matter
for (const batch of batches) {
batch.#blockers.delete(this);
if (batch.#blockers.size === 0 && !batch.#is_deferred()) {
batch.activate();
batch.#process();
}
}
return;
}
// If neither of the above is the case we need to flush now
}
this.#decrement_queued = true;

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

@ -0,0 +1,197 @@
<script>
import { getAbortSignal } from 'svelte';
const queue = [];
let n = $state(1);
let fizz = $state(true);
let buzz = $state(true);
function increment() {
n++;
fizz = n % 3 === 0;
buzz = n % 5 === 0;
}
function push(value) {
if (value === 1) return 1;
const d = Promise.withResolvers();
queue.push(() => d.resolve(value));
const signal = getAbortSignal();
signal.onabort = () => d.reject(signal.reason);
return d.promise;
}
</script>
<button onclick={increment}>
{$state.eager(n)}
</button>
<button onclick={() => queue.shift()?.()}>shift</button>
<p>{n} = {await push(n)}</p>
{#if true}
<p>fizz: {fizz}</p>
{/if}
{#if true}
<p>buzz: {buzz}</p>
{/if}
<!-- <script>
import { getAbortSignal } from 'svelte';
const queue = [];
function push(value) {
if (value === 1) return 1;
const d = Promise.withResolvers();
queue.push(() => d.resolve(value));
const signal = getAbortSignal();
signal.onabort = () => d.reject(signal.reason);
return d.promise;
}
function shift() {
queue.shift()?.();
}
function pop() {
queue.pop()?.();
}
let n = $state(1);
</script>
<button onclick={() => n++}>
{$state.eager(n)}
</button>
<button onclick={shift}>shift</button>
<button onclick={pop}>pop</button>
<p>{n} = {await push(n)}</p> -->
<!-- <script>
let a = $state(0);
const deferred = [];
function delay(value) {
if (!value) return value;
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
</script>
{a} {await delay(a)}
{#if a < 2}
{await delay(a)}
{/if}
<button onclick={() => {a++;}}>
a+1
</button>
<button onclick={() => {a+=2;}}>
a+2
</button>
<button onclick={() => deferred.shift()?.()}>shift</button>
<button onclick={() => deferred[2]()}>middle</button> -->
<!-- <script>
let a = $state(0);
let b = $derived(await delay(a * 2));
let c = $state(0);
let d = $derived(await delay(b + c));
// let e = $derived(d === (b + c));
const deferred = [];
function delay(value) {
if (!value) return value;
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
</script>
a {a} | b {b} | c {c} | d {d}
<button onclick={() => {a++;}}>
a++
</button>
<button onclick={() => {c++;}}>
c++
</button>
<button onclick={() => deferred.shift()?.()}>shift</button>
<button onclick={() => deferred.pop()?.()}>pop</button> -->
<!-- <script>
let count = $state(0);
let other = $state(0);
const queue = [];
function push(v) {
return new Promise((r,e) => queue.push(() => v === 1 ? e(v) : r(v)));
}
</script>
<button onclick={() => {
if (count === 0) {
other++;
count++;
} else {
count++
}
}}>increment</button>
<button onclick={() => queue.pop()?.()}>pop</button>
{#if count > 0}
<svelte:boundary>
{await push(count)} {count} {other}
{#snippet failed()}boom{/snippet}
</svelte:boundary>
{/if} -->
<!-- <script>
let count1 = $state(0);
let count2 = $state(0);
const queued = [];
async function delay(v) {
if (!v) return v;
return new Promise(r => queued.push(() => r(v)));
}
function show(get) {
console.log('running', get());
return $state.eager(get()) !== get();
}
</script>
<button onclick={() => count1++}>increment</button>
<button onclick={() => count2++}>increment</button>
<button onclick={() => queued.shift()?.()}>resolve</button>
{await delay(count1)}
{await delay(count2)}
{#if show(() => count1)}
<p>loading...</p>
{:else}
<p>{count1}</p>
{/if}
{#if show(() => count2)}
<p>loading...</p>
{:else}
<p>{count2}</p>
{/if} -->
Loading…
Cancel
Save