fix more edge cases

async-another-try
Simon Holthausen 4 days ago
parent 7005aae212
commit b08aeee3cf
No known key found for this signature in database

@ -190,6 +190,17 @@ export class BranchManager {
var defer = should_defer_append();
var first = false;
// Re-evaluating in the surviving batch supersedes selections made before a merge,
// even though those batches originally had newer IDs and still have commit callbacks.
for (const previous of this.#batches.keys()) {
for (let merged = previous.merged_into; merged !== null; merged = merged.merged_into) {
if (merged === batch) {
this.#batches.delete(previous);
break;
}
}
}
if (fn && !this.#onscreen.has(key) && !this.#offscreen.has(key)) {
first = true;

@ -720,6 +720,11 @@ export class Batch {
}
}
this.#pending += batch.#pending;
for (const [effect, count] of batch.#blocking_pending) {
this.#blocking_pending.set(effect, (this.#blocking_pending.get(effect) ?? 0) + count);
}
for (const c of batch.#commit_callbacks) {
this.oncommit(() => c(batch));
}
@ -848,7 +853,7 @@ export class Batch {
batch.current.clear();
batch.discard();
} else {
if (current) batch.current.set(source, current);
if (current && current[0] !== value) batch.current.set(source, current);
if (
!is_derived &&
(!current || current[0] !== value) &&
@ -862,7 +867,7 @@ export class Batch {
batch.current.delete(source);
const b = batch;
queue_micro_task(() => {
if (b.mark(source, DIRTY)) {
if (b.linked && b.mark(source, DIRTY)) {
b.flush();
}
});
@ -979,8 +984,13 @@ export class Batch {
* @param {Set<Effect>} dirty_effects
* @param {Set<Effect>} maybe_dirty_effects
* @param {Set<Derived>} dirty_deriveds
* @returns {void}
*/
transfer_effects(dirty_effects, maybe_dirty_effects, dirty_deriveds) {
if (this.merged_into) {
return this.merged_into.transfer_effects(dirty_effects, maybe_dirty_effects, dirty_deriveds);
}
for (const e of dirty_effects) {
this.#dirty_effects.add(e);
}
@ -1594,7 +1604,7 @@ export function fork(fn) {
} else if (!is_derived) {
const b = next_batch;
queue_micro_task(() => {
if (b.mark(source, DIRTY)) {
if (b.linked && b.mark(source, DIRTY)) {
b.flush();
}
});

@ -0,0 +1,38 @@
import { tick } from 'svelte';
import { test } from '../../test';
const buttons = `
<button>fork x = 1</button>
<button>update y and discard</button>
<button>commit y fork and discard</button>
<button>resolve requests</button>
<button>reset</button>
`;
export default test({
mode: ['client'],
async test({ assert, target, logs }) {
await tick();
const [speculate, update, commit, resolve, reset] = target.querySelectorAll('button');
for (const action of [update, commit]) {
speculate.click();
await tick();
logs.length = 0;
// Discard before the queued fork revalidation runs. It must not restart
// the async effect and abort the real world's request, whether the write
// came from a normal update or another fork's commit.
action.click();
await tick();
resolve.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<p>0/1</p>`);
assert.deepEqual(logs, ['0/1']);
reset.click();
await tick();
}
}
});

@ -0,0 +1,48 @@
<script>
import { fork, getAbortSignal } from 'svelte';
let x = $state(0);
let y = $state(0);
let f;
const resolvers = [];
function load(x, y) {
console.log(`${x}/${y}`);
const signal = getAbortSignal();
if (y === 0) return `${x}/${y}`;
return new Promise((resolve, reject) => {
resolvers.push(() => resolve(`${x}/${y}`));
signal.addEventListener('abort', () => reject(signal.reason));
});
}
function speculate() {
f = fork(() => { x = 1; });
}
function update_and_discard(use_fork) {
if (use_fork) {
fork(() => { y = 1; }).commit();
} else {
y = 1;
}
f.discard();
}
function finish() {
for (const resolve of resolvers.splice(0)) resolve();
}
function reset() {
y = 0;
}
</script>
<button onclick={speculate}>fork x = 1</button>
<button onclick={() => update_and_discard(false)}>update y and discard</button>
<button onclick={() => update_and_discard(true)}>commit y fork and discard</button>
<button onclick={finish}>resolve requests</button>
<button onclick={reset}>reset</button>
<p>{await load(x, y)}</p>

@ -0,0 +1,42 @@
import { tick } from 'svelte';
import { test } from '../../test';
const buttons = `
<button>fork x = y = 1</button>
<button>set x = y = 1</button>
<button>set z = 1</button>
<button>commit fork</button>
<button>discard fork</button>
`;
export default test({
mode: ['client'],
async test({ assert, target, logs }) {
await tick();
const [speculate, catch_up, update, commit, discard] = target.querySelectorAll('button');
speculate.click();
await tick();
catch_up.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<p>2</p>`);
logs.length = 0;
// All speculative writes are obsolete. Only the real world should react.
update.click();
await tick();
try {
assert.htmlEqual(target.innerHTML, `${buttons}<p>3</p>`);
assert.deepEqual(logs, ['1/1/1']);
// Committing an automatically discarded fork should be a no-op and not throw,
// as the user cannot really know that something got automatically discarded.
commit.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<p>3</p>`);
assert.deepEqual(logs, ['1/1/1', 'committed']);
} finally {
discard.click();
}
}
});

@ -0,0 +1,43 @@
<script>
import { fork } from 'svelte';
let x = $state(0);
let y = $state(0);
let z = $state(0);
let f;
function load(x, y, z) {
console.log(`${x}/${y}/${z}`);
return x + y + z;
}
function speculate() {
f = fork(() => { x = 1; y = 1; });
}
function catch_up() {
x = 1;
y = 1;
}
function update() {
z = 1;
}
function discard() {
f.discard();
}
async function commit() {
await f.commit();
console.log('committed');
}
</script>
<button onclick={speculate}>fork x = y = 1</button>
<button onclick={catch_up}>set x = y = 1</button>
<button onclick={update}>set z = 1</button>
<button onclick={commit}>commit fork</button>
<button onclick={discard}>discard fork</button>
<p>{await load(x, y, z)}</p>

@ -0,0 +1,31 @@
import { tick } from 'svelte';
import { test } from '../../test';
const buttons = `
<button>start A: x = 1</button>
<button>start B: show branch</button>
<button>resolve A</button>
`;
export default test({
mode: ['client'],
async test({ assert, target }) {
await tick();
const [start_a, start_b, resolve] = target.querySelectorAll('button');
start_a.click();
await tick();
start_b.click();
await tick();
// B selected the empty branch before merging into A. A's async result
// now selects the content, which B's old commit callback must not remove.
// The two blocks register A's and B's callbacks in opposite orders.
resolve.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`${buttons}<p>1/1</p><strong>ready</strong><em>also ready</em>`
);
}
});

@ -0,0 +1,24 @@
<script>
let x = $state(0);
let show = $state(false);
let done;
function delay(value) {
if (value === 0) return 0;
return new Promise(resolve => { done = () => resolve(value); });
}
let result = $derived(await delay(x));
</script>
<button onclick={() => x = 1}>start A: x = 1</button>
<button onclick={() => show = true}>start B: show branch</button>
<button onclick={() => done()}>resolve A</button>
<p>{x}/{result}</p>
{#if show && x > 0 && result > 0}
<strong>ready</strong>
{/if}
{#if x > 0 && show && result > 0}
<em>also ready</em>
{/if}

@ -0,0 +1,10 @@
<script>
let { delay } = $props();
let result = $derived(await delay());
$effect(() => {
console.log(result);
});
</script>
<strong>{result}</strong>

@ -0,0 +1,47 @@
import { tick } from 'svelte';
import { test } from '../../test';
const buttons = `
<button>start A: x = 1</button>
<button>start B: show boundary</button>
<button>resolve A</button>
<button>resolve boundary</button>
<button>reset</button>
`;
export default test({
mode: ['client'],
async test({ assert, target, logs }) {
await tick();
const [start_a, start_b, resolve_a, resolve_b, reset] = target.querySelectorAll('button');
for (const boundary_first of [false, true]) {
logs.length = 0;
start_a.click();
await tick();
start_b.click();
await tick();
// B's pending boundary must transfer effects to A regardless of whether
// it resolves before or after A renders.
if (boundary_first) {
resolve_b.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<p>0</p>`);
assert.deepEqual(logs, []);
resolve_a.click();
} else {
resolve_a.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<p>1</p><span>pending</span>`);
resolve_b.click();
}
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<p>1</p><strong>ready</strong>`);
assert.deepEqual(logs, ['ready']);
reset.click();
await tick();
}
}
});

@ -0,0 +1,31 @@
<script>
import Child from './Child.svelte';
let x = $state(0);
let show = $state(false);
let resolve_a;
let resolve_b;
function delay_a(value) {
if (value === 0) return 0;
return new Promise(resolve => { resolve_a = () => resolve(value); });
}
function delay_b() {
return new Promise(resolve => { resolve_b = () => resolve('ready'); });
}
</script>
<button onclick={() => x = 1}>start A: x = 1</button>
<button onclick={() => show = true}>start B: show boundary</button>
<button onclick={() => resolve_a()}>resolve A</button>
<button onclick={() => resolve_b()}>resolve boundary</button>
<button onclick={() => { show = false; x = 0; }}>reset</button>
<p>{await delay_a(x)}</p>
{#if show && x > 0}
<svelte:boundary>
{#snippet pending()}<span>pending</span>{/snippet}
<Child delay={delay_b} />
</svelte:boundary>
{/if}
Loading…
Cancel
Save