mirror of https://github.com/sveltejs/svelte
set deriveds to clean when it's latest value (avoids overfiring); save maybe_dirty deriveds in deferred batches (avoids underfiring that would otherwise happen as a result of the first fix)
parent
06cfc31f8d
commit
54d8c3d843
@ -0,0 +1,20 @@
|
||||
<script>
|
||||
let { revision, delay } = $props();
|
||||
const assessment = $derived(await delay('assessment', revision));
|
||||
// Keep the batch pending while the if blocks read parity.
|
||||
const schedule = $derived(await delay('schedule', assessment));
|
||||
const parity = $derived.by(() => {
|
||||
console.log(['parity', assessment]);
|
||||
return assessment % 2;
|
||||
});
|
||||
const matched = $derived.by(() => {
|
||||
console.log(['matched', assessment, schedule]);
|
||||
return assessment === schedule;
|
||||
});
|
||||
</script>
|
||||
|
||||
<p>{assessment}/{parity}</p>
|
||||
{#if parity}<p>odd</p>{/if}
|
||||
{#if schedule >= 0 && parity}<p>also odd</p>{/if}
|
||||
{#if matched}<p>matched</p>{/if}
|
||||
<button onclick={() => console.log(['read', parity])}>read parity</button>
|
||||
@ -0,0 +1,76 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
const buttons = `
|
||||
<button>update</button>
|
||||
<button>resolve assessment 2</button>
|
||||
<button>resolve schedule 2</button>
|
||||
<button>resolve assessment 1</button>
|
||||
`;
|
||||
|
||||
export default test({
|
||||
mode: ['client'],
|
||||
async test({ assert, target, logs }) {
|
||||
await tick();
|
||||
await tick();
|
||||
const [update, resolve_a_2, resolve_schedule_2, resolve_a_1, read] =
|
||||
target.querySelectorAll('button');
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`${buttons}<p>0/0</p><p>matched</p><button>read parity</button>`
|
||||
);
|
||||
assert.deepEqual(logs, [
|
||||
['assessment', 0],
|
||||
['schedule', 0],
|
||||
['parity', 0],
|
||||
['matched', 0, 0]
|
||||
]);
|
||||
logs.length = 0;
|
||||
|
||||
update.click();
|
||||
await tick();
|
||||
update.click();
|
||||
await tick();
|
||||
|
||||
// The dependency changes, but parity stays 0 and its write version stays
|
||||
// unchanged. Both if blocks must share one evaluation in this batch's view.
|
||||
resolve_a_2.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, [
|
||||
['assessment', 1],
|
||||
['assessment', 2],
|
||||
['schedule', 2],
|
||||
['parity', 2],
|
||||
['matched', 2, 0]
|
||||
]);
|
||||
logs.length = 0;
|
||||
|
||||
// Resume the same batch and read parity again from the second if block.
|
||||
// Its dependencies have not changed since its previous evaluation.
|
||||
// In contrast, matched depends on schedule and must be invalidated.
|
||||
resolve_schedule_2.click();
|
||||
await tick();
|
||||
// Committing also merges into the earlier batch and flushes the text effect,
|
||||
// which must reuse the clean parity rather than evaluating it again.
|
||||
assert.deepEqual(logs, [['matched', 2, 2]]);
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`${buttons}<p>2/0</p><p>matched</p><button>read parity</button>`
|
||||
);
|
||||
logs.length = 0;
|
||||
|
||||
// The superseded result must not cause any further evaluations.
|
||||
resolve_a_1.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, []);
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`${buttons}<p>2/0</p><p>matched</p><button>read parity</button>`
|
||||
);
|
||||
|
||||
// Once the competing batches are gone, parity must be globally clean.
|
||||
// An event-handler read has no batch-local status to fall back on.
|
||||
read.click();
|
||||
assert.deepEqual(logs, [['read', 0]]);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,25 @@
|
||||
<script>
|
||||
import Child from './Child.svelte';
|
||||
let revision = $state(0);
|
||||
const pending = new Map();
|
||||
|
||||
function delay(kind, value) {
|
||||
console.log([kind, value]);
|
||||
if (value === 0) return value;
|
||||
return new Promise(resolve => pending.set(`${kind}:${value}`, () => resolve(value)));
|
||||
}
|
||||
|
||||
function resolve(kind, value) {
|
||||
pending.get(`${kind}:${value}`)?.();
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={() => revision++}>update</button>
|
||||
<button onclick={() => resolve('assessment', 2)}>resolve assessment 2</button>
|
||||
<button onclick={() => resolve('schedule', 2)}>resolve schedule 2</button>
|
||||
<button onclick={() => resolve('assessment', 1)}>resolve assessment 1</button>
|
||||
|
||||
<svelte:boundary>
|
||||
<Child {revision} {delay} />
|
||||
{#snippet pending()}loading{/snippet}
|
||||
</svelte:boundary>
|
||||
@ -0,0 +1,29 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
mode: ['client'],
|
||||
async test({ assert, target, logs }) {
|
||||
await tick();
|
||||
const [update, resolve_a, resolve_b] = target.querySelectorAll('button');
|
||||
assert.deepEqual(logs, [0]);
|
||||
|
||||
update.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, [0]);
|
||||
|
||||
// The new if block evaluates parity while the text effect is still deferred.
|
||||
resolve_b.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, [0, 2]);
|
||||
|
||||
// The batch must forget its earlier deferred DIRTY entry for parity.
|
||||
resolve_a.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, [0, 2]);
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
'<button>update</button><button>resolve a</button><button>resolve b</button><p>2:0</p>'
|
||||
);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,22 @@
|
||||
<script>
|
||||
let count = $state(0);
|
||||
const parity = $derived.by(() => {
|
||||
console.log(count);
|
||||
return count % 2;
|
||||
});
|
||||
const pending = new Map();
|
||||
|
||||
function delay(name, value) {
|
||||
if (value === 0) return value;
|
||||
return new Promise(resolve => pending.set(name, () => resolve(value)));
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={() => count += 2}>update</button>
|
||||
<button onclick={() => pending.get('a')?.()}>resolve a</button>
|
||||
<button onclick={() => pending.get('b')?.()}>resolve b</button>
|
||||
|
||||
<p>{await delay('a', count)}:{parity}</p>
|
||||
{#if await delay('b', count)}
|
||||
{#if parity}<p>odd</p>{/if}
|
||||
{/if}
|
||||
@ -0,0 +1,37 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
const buttons = `
|
||||
<button>a</button>
|
||||
<button>b</button>
|
||||
<button>resolve a</button>
|
||||
<button>resolve b</button>
|
||||
`;
|
||||
|
||||
export default test({
|
||||
mode: ['client'],
|
||||
async test({ assert, target }) {
|
||||
await tick();
|
||||
const [a, b, resolve_a, resolve_b] = target.querySelectorAll('button');
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>0:0</p><p>0</p>`);
|
||||
|
||||
// sum is DIRTY and doubled is MAYBE_DIRTY, but neither runs while a is pending.
|
||||
a.click();
|
||||
await tick();
|
||||
|
||||
// The if block evaluates both deriveds with the latest inputs and marks them clean.
|
||||
b.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>0:0</p><p>0</p>`);
|
||||
|
||||
// The earlier batch must restore doubled's MAYBE_DIRTY status as well as sum's
|
||||
// DIRTY status, otherwise it reuses the cached value from before a changed.
|
||||
resolve_a.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>1:2</p><p>0</p>`);
|
||||
|
||||
resolve_b.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>1:4</p><p>1</p><span>ready</span>`);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,21 @@
|
||||
<script>
|
||||
let a = $state(0);
|
||||
let b = $state(0);
|
||||
const sum = $derived(a + b);
|
||||
const doubled = $derived(sum * 2);
|
||||
const pending = new Map();
|
||||
|
||||
function delay(name, value) {
|
||||
if (value === 0) return value;
|
||||
return new Promise(resolve => pending.set(name, () => resolve(value)));
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={() => a++}>a</button>
|
||||
<button onclick={() => b++}>b</button>
|
||||
<button onclick={() => pending.get('a')?.()}>resolve a</button>
|
||||
<button onclick={() => pending.get('b')?.()}>resolve b</button>
|
||||
|
||||
<p>{await delay('a', a)}:{doubled}</p>
|
||||
<p>{await delay('b', b)}</p>
|
||||
{#if b && doubled}<span>ready</span>{/if}
|
||||
Loading…
Reference in new issue