mirror of https://github.com/sveltejs/svelte
parent
40bb08482f
commit
d28ea11509
@ -0,0 +1,5 @@
|
||||
---
|
||||
'svelte': patch
|
||||
---
|
||||
|
||||
fix: keep values computed in a batch's view of the world local to that batch, and don't needlessly re-run deriveds while several batches are pending
|
||||
@ -0,0 +1,5 @@
|
||||
<script>
|
||||
let { c } = $props();
|
||||
</script>
|
||||
|
||||
<span>{c}</span>
|
||||
@ -0,0 +1,28 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
// An effect that several batches scheduled must run in each of them, even if another batch is flushed first
|
||||
export default test({
|
||||
mode: ['client'],
|
||||
async test({ assert, target }) {
|
||||
await tick();
|
||||
const [a, b, resolve_and_write, resolve] = target.querySelectorAll('button');
|
||||
const buttons =
|
||||
'<button>a</button><button>b</button><button>resolve and write</button><button>resolve</button>';
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>0 0</p><i>0</i><span>0</span>`);
|
||||
|
||||
a.click();
|
||||
await tick();
|
||||
b.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>0 0</p><i>0</i><span>0</span>`);
|
||||
|
||||
resolve_and_write.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>0 1</p><i>0</i><span>1</span>`);
|
||||
|
||||
resolve.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>1 1</p><i>1</i><span>1</span>`);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,33 @@
|
||||
<script>
|
||||
import Sync from './Sync.svelte';
|
||||
|
||||
let a = $state(0);
|
||||
let b = $state(0);
|
||||
let c = $state(0);
|
||||
|
||||
const registry = [];
|
||||
|
||||
function load(value) {
|
||||
if (!value) return value;
|
||||
return new Promise((resolve) => registry.push(() => resolve(value)));
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={() => a++}>a</button>
|
||||
<button onclick={() => b++}>b</button>
|
||||
<button
|
||||
onclick={() => {
|
||||
// the `<p>` values of both batches resolve into their batches (scheduling the same effect in each
|
||||
// of them) after this third batch was created, but before it is flushed. The first batch stays pending
|
||||
const [p_a, i_a, p_b] = registry.splice(0);
|
||||
registry.push(i_a);
|
||||
p_b();
|
||||
p_a();
|
||||
queueMicrotask(() => c++);
|
||||
}}>resolve and write</button
|
||||
>
|
||||
<button onclick={() => registry.shift()?.()}>resolve</button>
|
||||
|
||||
<p>{await load(a)} {await load(b)}</p>
|
||||
<i>{await load(a)}</i>
|
||||
<Sync {c} />
|
||||
@ -0,0 +1,6 @@
|
||||
<script>
|
||||
let { x } = $props();
|
||||
const result = $derived(await x);
|
||||
</script>
|
||||
|
||||
<p>block: {result}</p>
|
||||
@ -0,0 +1,5 @@
|
||||
<script>
|
||||
let { y } = $props();
|
||||
</script>
|
||||
|
||||
<span>{y}</span>
|
||||
@ -0,0 +1,16 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
// An effect must run in the batch that scheduled it, not in another batch that happens to be flushed first
|
||||
export default test({
|
||||
mode: ['client'],
|
||||
async test({ assert, target }) {
|
||||
await tick();
|
||||
const [button] = target.querySelectorAll('button');
|
||||
assert.htmlEqual(target.innerHTML, '<button>increment</button><p>block: 0</p><span>0</span>');
|
||||
|
||||
button.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, '<button>increment</button><p>block: 1</p><span>1</span>');
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,18 @@
|
||||
<script>
|
||||
import Async from './Async.svelte';
|
||||
import Sync from './Sync.svelte';
|
||||
|
||||
let x = $state(0);
|
||||
let y = $state(0);
|
||||
</script>
|
||||
|
||||
<button
|
||||
onclick={() => {
|
||||
x++;
|
||||
// the second batch is created while the first one is pending, after the first one
|
||||
// scheduled its effects but before it is flushed again
|
||||
queueMicrotask(() => queueMicrotask(() => y++));
|
||||
}}>increment</button
|
||||
>
|
||||
<Async {x} />
|
||||
<Sync {y} />
|
||||
@ -0,0 +1,27 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
const buttons = '<button>a</button><button>b</button><button>shift</button>';
|
||||
|
||||
export default test({
|
||||
async test({ assert, target }) {
|
||||
await tick();
|
||||
const [a, b, shift] = target.querySelectorAll('button');
|
||||
|
||||
// batch A waits on its key; batch B registers its key right away
|
||||
a.click();
|
||||
await tick();
|
||||
b.click();
|
||||
await tick();
|
||||
|
||||
// the older batch commits first
|
||||
shift.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `<p>0</p><p>1</p>${buttons}`);
|
||||
|
||||
// the newer batch commits with its own branch
|
||||
shift.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `<p>1</p><p>B</p>${buttons}`);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,24 @@
|
||||
<script>
|
||||
let a = $state(0);
|
||||
let b = $state(0);
|
||||
let pend = false;
|
||||
|
||||
const deferred = [];
|
||||
|
||||
function delay(value) {
|
||||
if (!pend) return value;
|
||||
return new Promise((resolve) => deferred.push(() => resolve(value)));
|
||||
}
|
||||
|
||||
const k = $derived(b === 0 ? await delay(a) : 'B');
|
||||
</script>
|
||||
|
||||
<p>{await delay(b)}</p>
|
||||
|
||||
{#key k}
|
||||
<p>{k}</p>
|
||||
{/key}
|
||||
|
||||
<button onclick={() => { pend = true; a = 1; }}>a</button>
|
||||
<button onclick={() => { pend = true; b = 1; }}>b</button>
|
||||
<button onclick={() => deferred.shift()()}>shift</button>
|
||||
@ -0,0 +1,35 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
async test({ assert, target, logs }) {
|
||||
await tick();
|
||||
const [x, y, shift] = target.querySelectorAll('button');
|
||||
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
'<p>0</p><button>x</button><button>y</button><button>shift</button>'
|
||||
);
|
||||
assert.deepEqual(logs, []);
|
||||
|
||||
// write x — the batch is pending on its async expression
|
||||
x.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, []);
|
||||
|
||||
// an independent batch runs the effect, which newly depends on `x` —
|
||||
// it reads the held-back value (0) (at some point it read latest, but we decided against doing this)
|
||||
y.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['effect 0 1']);
|
||||
|
||||
// the pending batch settles and commits x === 1, so the effect should re-run
|
||||
shift.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['effect 0 1', 'effect 1 1']);
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
'<p>1</p><button>x</button><button>y</button><button>shift</button>'
|
||||
);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,23 @@
|
||||
<script>
|
||||
let x = $state(0);
|
||||
let y = $state(0);
|
||||
let pend = false;
|
||||
|
||||
const deferred = [];
|
||||
|
||||
function delay(value) {
|
||||
if (!pend) return value;
|
||||
return new Promise((resolve) => deferred.push(() => resolve(value)));
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (y > 0) {
|
||||
console.log(`effect ${x} ${y}`);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<p>{await delay(x)}</p>
|
||||
<button onclick={() => { pend = true; x += 1; }}>x</button>
|
||||
<button onclick={() => y++}>y</button>
|
||||
<button onclick={() => deferred.shift()?.()}>shift</button>
|
||||
@ -0,0 +1,31 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
// Same as `async-commit-stale-reader-changed`, but the effect reads `x` through a derived
|
||||
// that the pending batch never evaluated, so only the derived's dependency was written by it
|
||||
export default test({
|
||||
async test({ assert, target, logs }) {
|
||||
await tick();
|
||||
const [x, y, shift] = target.querySelectorAll('button');
|
||||
|
||||
// write x — the batch is pending on its async expression
|
||||
x.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, []);
|
||||
|
||||
// an independent batch runs the effect, which newly depends on `doubled` —
|
||||
// it is computed from the held-back value of `x` (0)
|
||||
y.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['effect 0 1']);
|
||||
|
||||
// the pending batch settles and commits x === 1, so the effect should re-run
|
||||
shift.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['effect 0 1', 'effect 2 1']);
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
'<p>1</p><button>x</button><button>y</button><button>shift</button>'
|
||||
);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,24 @@
|
||||
<script>
|
||||
let x = $state(0);
|
||||
let y = $state(0);
|
||||
let pend = false;
|
||||
let doubled = $derived(x * 2);
|
||||
|
||||
const deferred = [];
|
||||
|
||||
function delay(value) {
|
||||
if (!pend) return value;
|
||||
return new Promise((resolve) => deferred.push(() => resolve(value)));
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (y > 0) {
|
||||
console.log(`effect ${doubled} ${y}`);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<p>{await delay(x)}</p>
|
||||
<button onclick={() => { pend = true; x += 1; }}>x</button>
|
||||
<button onclick={() => y++}>y</button>
|
||||
<button onclick={() => deferred.shift()?.()}>shift</button>
|
||||
@ -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,44 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
async test({ assert, target, logs }) {
|
||||
await tick();
|
||||
const [a, b, log, resolve] = target.querySelectorAll('button');
|
||||
const [p] = target.querySelectorAll('p');
|
||||
|
||||
assert.deepEqual(logs, ['effect 0']);
|
||||
logs.length = 0;
|
||||
|
||||
// batch A is pending
|
||||
a.click();
|
||||
await tick();
|
||||
assert.htmlEqual(p.innerHTML, '0 0');
|
||||
|
||||
// batch B commits right away, in a view of the world without A's change
|
||||
b.click();
|
||||
await tick();
|
||||
assert.htmlEqual(p.innerHTML, '0 -1');
|
||||
assert.deepEqual(logs, ['effect -1']);
|
||||
logs.length = 0;
|
||||
|
||||
// outside of any batch, `d` has its real value (and is only computed once
|
||||
// for that, as it wasn't computed with the real values before)
|
||||
log.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['real 0 3']);
|
||||
logs.length = 0;
|
||||
|
||||
// batch A commits: `d` is 0 again, like before B — but the effect last saw -1
|
||||
// (a value that only existed in B's view), so it must re-run. `d` is not re-computed
|
||||
resolve.click();
|
||||
await tick();
|
||||
assert.htmlEqual(p.innerHTML, '1 0');
|
||||
assert.deepEqual(logs, ['effect 0']);
|
||||
logs.length = 0;
|
||||
|
||||
log.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['real 0 3']);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,25 @@
|
||||
<script>
|
||||
let a = $state(0);
|
||||
let b = $state(0);
|
||||
let runs = 0;
|
||||
|
||||
// in batch B (b = -1), which doesn't see A's change (a = 1), `d` is -1 — but that
|
||||
// is not the real value of `d`, which is 0 (a + b with both changes applied)
|
||||
let d = $derived((runs++, a + b));
|
||||
|
||||
const queued = [];
|
||||
|
||||
function push(v) {
|
||||
if (!v) return v;
|
||||
return new Promise((resolve) => queued.push(() => resolve(v)));
|
||||
}
|
||||
|
||||
$effect(() => console.log(`effect ${d}`));
|
||||
</script>
|
||||
|
||||
<button onclick={() => a++}>a++</button>
|
||||
<button onclick={() => (b = -1)}>b = -1</button>
|
||||
<button onclick={() => console.log(`real ${d} ${runs}`)}>log</button>
|
||||
<button onclick={() => queued.shift()?.()}>resolve</button>
|
||||
|
||||
<p>{await push(a)} {d}</p>
|
||||
@ -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}
|
||||
@ -0,0 +1,33 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
// Batch B (x) commits while an earlier batch A (y) is still pending, but B stays around because
|
||||
// of the async work in the new boundary. While B flushes its effects, an effect creates a new batch C,
|
||||
// and `d` is computed for the first time afterwards (so it's captured in both B and C). When A
|
||||
// commits later, it sees B's previous values, which must be consistent (`d` must be `x * 2`).
|
||||
export default test({
|
||||
async test({ assert, target, logs }) {
|
||||
await tick();
|
||||
const [y, x, shift] = target.querySelectorAll('button');
|
||||
assert.deepEqual(logs, [0, 0, 0]);
|
||||
|
||||
// A
|
||||
y.click();
|
||||
await tick();
|
||||
|
||||
// B
|
||||
x.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, [0, 0, 0, 0, 1, 2]);
|
||||
|
||||
// A commits
|
||||
shift.click();
|
||||
await tick();
|
||||
|
||||
for (let i = 0; i < logs.length; i += 3) {
|
||||
assert.equal(logs[i + 2], logs[i + 1] * 2, `torn read: ${logs.slice(i, i + 3)}`);
|
||||
}
|
||||
|
||||
assert.deepEqual(logs.slice(-3), [1, 1, 2]);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,38 @@
|
||||
<script>
|
||||
import { untrack } from 'svelte';
|
||||
|
||||
let x = $state(0);
|
||||
let y = $state(0);
|
||||
let z = $state(0);
|
||||
let d = $derived(x * 2);
|
||||
|
||||
const queue = [];
|
||||
function push(v) {
|
||||
return new Promise((r) => queue.push(() => r(v)));
|
||||
}
|
||||
|
||||
// creates a new batch while the batch that changed `x` flushes its effects...
|
||||
$effect(() => {
|
||||
if (x) untrack(() => z++);
|
||||
});
|
||||
|
||||
// ...before `d` is computed for the first time in that batch
|
||||
$effect(() => {
|
||||
console.log(y, x, d);
|
||||
});
|
||||
</script>
|
||||
|
||||
<button onclick={() => y++}>y</button>
|
||||
<button onclick={() => x++}>x</button>
|
||||
<button onclick={() => queue.shift()()}>shift</button>
|
||||
|
||||
<p>{y ? await push(y) : y}</p>
|
||||
|
||||
{#if x}
|
||||
<svelte:boundary>
|
||||
<p>{await push(x)}</p>
|
||||
{#snippet pending()}
|
||||
<p>loading</p>
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
{/if}
|
||||
@ -0,0 +1,9 @@
|
||||
<script>
|
||||
let { big } = $props();
|
||||
|
||||
$effect(() => {
|
||||
console.log(`effect ${big}`);
|
||||
});
|
||||
</script>
|
||||
|
||||
<p>child</p>
|
||||
@ -0,0 +1,37 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
mode: ['client'],
|
||||
async test({ assert, target, logs, raf }) {
|
||||
await tick();
|
||||
const [x, s, toggle, shift] = target.querySelectorAll('button');
|
||||
assert.deepEqual(logs, ['effect false']);
|
||||
|
||||
// keep an earlier batch pending, so that the next one commits while time travelling
|
||||
x.click();
|
||||
await tick();
|
||||
|
||||
// `big` stays `false`, so the effect must not re-run
|
||||
s.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['effect false']);
|
||||
|
||||
shift.click();
|
||||
await tick();
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
'<button>x</button><button>s</button><button>toggle</button><button>shift</button><p>1</p><div><p>child</p></div>'
|
||||
);
|
||||
|
||||
// outro, then reintro before the outro finished: the branch is resumed
|
||||
toggle.click();
|
||||
await tick();
|
||||
raf.tick(50);
|
||||
toggle.click();
|
||||
await tick();
|
||||
raf.tick(200);
|
||||
|
||||
assert.deepEqual(logs, ['effect false']);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,29 @@
|
||||
<script>
|
||||
import { fade } from 'svelte/transition';
|
||||
import Child from './Child.svelte';
|
||||
|
||||
let x = $state(0);
|
||||
let s = $state(0);
|
||||
let show = $state(true);
|
||||
const big = $derived(s > 100);
|
||||
|
||||
const queued = [];
|
||||
|
||||
function push(v) {
|
||||
if (v === 0) return v;
|
||||
return new Promise((resolve) => queued.push(() => resolve(v)));
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={() => x++}>x</button>
|
||||
<button onclick={() => s++}>s</button>
|
||||
<button onclick={() => (show = !show)}>toggle</button>
|
||||
<button onclick={() => queued.shift()?.()}>shift</button>
|
||||
|
||||
<p>{await push(x)}</p>
|
||||
|
||||
{#if show}
|
||||
<div transition:fade={{ duration: 100 }}>
|
||||
<Child {big} />
|
||||
</div>
|
||||
{/if}
|
||||
@ -0,0 +1,39 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
const buttons =
|
||||
'<button>b = 1</button><button>b = 2, c = 1</button><button>resolve latest pos</button><button>resolve oldest pos</button><button>resolve all</button>';
|
||||
|
||||
// A batch's async result equals the real value, which a later (still pending) batch wrote. The batch
|
||||
// itself sees that later batch's previous value though, so the result changes its world and has to show
|
||||
export default test({
|
||||
mode: ['client'],
|
||||
async test({ assert, target }) {
|
||||
const [b1, b2c1, resolve_latest, resolve_oldest, resolve_all] =
|
||||
target.querySelectorAll('button');
|
||||
|
||||
resolve_all.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>zero</p><span>0</span><i>c0</i>`);
|
||||
|
||||
b1.click();
|
||||
await tick();
|
||||
b2c1.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>zero</p><span>0</span><i>c0</i>`);
|
||||
|
||||
// the later batch's result becomes the real value, but that batch still waits for c1
|
||||
resolve_latest.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>zero</p><span>0</span><i>c0</i>`);
|
||||
|
||||
// the earlier batch's equal result completes it
|
||||
resolve_oldest.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>pos</p><span>1</span><i>c0</i>`);
|
||||
|
||||
resolve_all.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>pos</p><span>2</span><i>c1</i>`);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,34 @@
|
||||
<script>
|
||||
let b = $state(0);
|
||||
let c = $state(0);
|
||||
|
||||
/** @type {Map<string, Array<() => void>>} */
|
||||
const pending = new Map();
|
||||
|
||||
/** @param {string} value */
|
||||
function f(value) {
|
||||
return new Promise((fulfil) => {
|
||||
if (!pending.has(value)) pending.set(value, []);
|
||||
pending.get(value)?.push(() => fulfil(value));
|
||||
});
|
||||
}
|
||||
|
||||
function resolve_all() {
|
||||
for (const list of pending.values()) {
|
||||
for (const fn of list.splice(0)) fn();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={() => (b = 1)}>b = 1</button>
|
||||
<button onclick={() => ((b = 2), (c = 1))}>b = 2, c = 1</button>
|
||||
<button onclick={() => pending.get('pos')?.pop()?.()}>resolve latest pos</button>
|
||||
<button onclick={() => pending.get('pos')?.shift()?.()}>resolve oldest pos</button>
|
||||
<button onclick={resolve_all}>resolve all</button>
|
||||
|
||||
<svelte:boundary>
|
||||
<p>{await f(b > 0 ? 'pos' : 'zero')}</p>
|
||||
<span>{b}</span>
|
||||
<i>{await f('c' + c)}</i>
|
||||
{#snippet pending()}{/snippet}
|
||||
</svelte:boundary>
|
||||
@ -0,0 +1,47 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
const buttons =
|
||||
'<button>b = 2</button><button>b = 0</button><button>resolve k3</button><button>resolve k0</button><button>resolve all</button>';
|
||||
|
||||
// A batch's async result that equals the current real value (but not the value the batch sees,
|
||||
// because an earlier pending batch has a different one in flight) must not be dropped, or the
|
||||
// earlier batch's result wins although the later batch reverted the input
|
||||
export default test({
|
||||
mode: ['client'],
|
||||
async test({ assert, target }) {
|
||||
const [b2, b0, resolve_k3, resolve_k0, resolve_all] = target.querySelectorAll('button');
|
||||
|
||||
resolve_all.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<b>k0</b><b>k1</b>`);
|
||||
|
||||
// batch 1 requests k2/k3
|
||||
b2.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<b>k0</b><b>k1</b>`);
|
||||
|
||||
// batch 2 requests k0/k1, which equal the values currently shown
|
||||
b0.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<b>k0</b><b>k1</b>`);
|
||||
|
||||
resolve_k3.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<b>k0</b><b>k1</b>`);
|
||||
|
||||
// batch 2's first result equals the real value
|
||||
resolve_k0.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<b>k0</b><b>k1</b>`);
|
||||
|
||||
resolve_all.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<b>k0</b><b>k1</b>`);
|
||||
|
||||
// nothing left over
|
||||
resolve_all.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<b>k0</b><b>k1</b>`);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,36 @@
|
||||
<script>
|
||||
let b = $state(0);
|
||||
|
||||
/** @type {Map<string, Array<() => void>>} */
|
||||
const pending = new Map();
|
||||
|
||||
/** @param {string} value */
|
||||
function delay(value) {
|
||||
return new Promise((fulfil) => {
|
||||
if (!pending.has(value)) pending.set(value, []);
|
||||
pending.get(value)?.push(() => fulfil(value));
|
||||
});
|
||||
}
|
||||
|
||||
/** @param {string} value */
|
||||
function resolve(value) {
|
||||
pending.get(value)?.shift()?.();
|
||||
}
|
||||
|
||||
function resolve_all() {
|
||||
for (const list of pending.values()) {
|
||||
for (const fn of list.splice(0)) fn();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={() => (b = 2)}>b = 2</button>
|
||||
<button onclick={() => (b = 0)}>b = 0</button>
|
||||
<button onclick={() => resolve('k3')}>resolve k3</button>
|
||||
<button onclick={() => resolve('k0')}>resolve k0</button>
|
||||
<button onclick={resolve_all}>resolve all</button>
|
||||
|
||||
<svelte:boundary>
|
||||
<b>{await delay('k' + (0 + b))}</b><b>{await delay('k' + (1 + b))}</b>
|
||||
{#snippet pending()}{/snippet}
|
||||
</svelte:boundary>
|
||||
@ -0,0 +1,40 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
const buttons =
|
||||
'<button>b = 1</button><button>b = 2, c = 1</button><button>resolve latest pos</button><button>resolve oldest pos</button><button>resolve all</button>';
|
||||
|
||||
// Like `async-equal-result-dropped-later-batch`, but the result is read by something that doesn't wait
|
||||
// for it: a batch's async result equals the real value, which a later (still pending) batch wrote. The
|
||||
// batch itself sees that later batch's previous value though, so the result changes its world, and
|
||||
// whatever read the previous value has to update
|
||||
export default test({
|
||||
mode: ['client'],
|
||||
async test({ assert, target }) {
|
||||
await tick();
|
||||
const [b1, b2c1, resolve_latest, resolve_oldest, resolve_all] =
|
||||
target.querySelectorAll('button');
|
||||
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>zero</p><span>0</span><i>c0</i>`);
|
||||
|
||||
b1.click();
|
||||
await tick();
|
||||
b2c1.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>zero</p><span>0</span><i>c0</i>`);
|
||||
|
||||
// the later batch's result becomes the real value, but that batch still waits for c1
|
||||
resolve_latest.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>zero</p><span>0</span><i>c0</i>`);
|
||||
|
||||
// the earlier batch's equal result completes it
|
||||
resolve_oldest.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>pos</p><span>1</span><i>c0</i>`);
|
||||
|
||||
resolve_all.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>pos</p><span>2</span><i>c1</i>`);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,39 @@
|
||||
<script>
|
||||
let b = $state(0);
|
||||
let c = $state(0);
|
||||
|
||||
/** @type {Map<string, Array<() => void>>} */
|
||||
const pending = new Map();
|
||||
let initial = true;
|
||||
|
||||
/** @param {string} value */
|
||||
function f(value) {
|
||||
if (initial) return value;
|
||||
return new Promise((fulfil) => {
|
||||
if (!pending.has(value)) pending.set(value, []);
|
||||
pending.get(value)?.push(() => fulfil(value));
|
||||
});
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
initial = false;
|
||||
});
|
||||
|
||||
function resolve_all() {
|
||||
for (const list of pending.values()) {
|
||||
for (const fn of list.splice(0)) fn();
|
||||
}
|
||||
}
|
||||
|
||||
let p = $derived(await f(b > 0 ? 'pos' : 'zero'));
|
||||
</script>
|
||||
|
||||
<button onclick={() => (b = 1)}>b = 1</button>
|
||||
<button onclick={() => ((b = 2), (c = 1))}>b = 2, c = 1</button>
|
||||
<button onclick={() => pending.get('pos')?.pop()?.()}>resolve latest pos</button>
|
||||
<button onclick={() => pending.get('pos')?.shift()?.()}>resolve oldest pos</button>
|
||||
<button onclick={resolve_all}>resolve all</button>
|
||||
|
||||
<p>{p}</p>
|
||||
{#if true}<span>{b}</span>{/if}
|
||||
<i>{await f('c' + c)}</i>
|
||||
@ -0,0 +1,41 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
const buttons = `
|
||||
<button>fork</button>
|
||||
<button>update</button>
|
||||
<button>resolve</button>
|
||||
<button>discard</button>
|
||||
`;
|
||||
|
||||
export default test({
|
||||
async test({ assert, target, logs }) {
|
||||
const [fork_button, update, resolve, discard] = target.querySelectorAll('button');
|
||||
|
||||
fork_button.click();
|
||||
await tick();
|
||||
assert.equal(logs.length, 1);
|
||||
assert.htmlEqual(target.innerHTML, buttons);
|
||||
|
||||
// Transfer an invalidation into the fork while its async work is pending.
|
||||
update.click();
|
||||
await tick();
|
||||
assert.equal(logs.length, 1); // can also be 2 at this point, would also be ok
|
||||
|
||||
try {
|
||||
resolve.click();
|
||||
await tick();
|
||||
assert.equal(logs.length, 1);
|
||||
|
||||
// Completing the replacement must not replay the same invalidation.
|
||||
resolve.click();
|
||||
await tick();
|
||||
assert.equal(logs.length, 1);
|
||||
} finally {
|
||||
discard.click();
|
||||
await tick();
|
||||
}
|
||||
|
||||
assert.htmlEqual(target.innerHTML, buttons);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,38 @@
|
||||
<script>
|
||||
import { fork } from 'svelte';
|
||||
|
||||
let sharedState = $state(0);
|
||||
let show = $state(false);
|
||||
let f;
|
||||
let calls = 0;
|
||||
|
||||
const resolvers = [];
|
||||
|
||||
// Only evaluated in the fork, with a fresh object on each evaluation.
|
||||
const searchParams = $derived({ value: sharedState });
|
||||
|
||||
function load(value) {
|
||||
console.log(calls += 1);
|
||||
return new Promise((resolve) => resolvers.push(() => resolve(value)));
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
onclick={() => {
|
||||
f = fork(() => {
|
||||
sharedState = 1;
|
||||
show = true;
|
||||
});
|
||||
}}
|
||||
>fork</button
|
||||
>
|
||||
<button onclick={() => sharedState++}>update</button>
|
||||
<button onclick={() => resolvers.shift()?.()}>resolve</button>
|
||||
<button onclick={() => f?.discard()}>discard</button>
|
||||
|
||||
{#if show}
|
||||
<svelte:boundary>
|
||||
{#snippet pending()}loading{/snippet}
|
||||
<p>{await load(searchParams.value)}</p>
|
||||
</svelte:boundary>
|
||||
{/if}
|
||||
@ -0,0 +1,35 @@
|
||||
import { flushSync, tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
const buttons = `
|
||||
<button>preload</button>
|
||||
<button>increment</button>
|
||||
<button>commit</button>
|
||||
<button>merge</button>
|
||||
<button>resolve</button>
|
||||
`;
|
||||
|
||||
export default test({
|
||||
async test({ assert, target, logs }) {
|
||||
const [preload, increment, commit, merge, resolve] = target.querySelectorAll('button');
|
||||
|
||||
preload.click();
|
||||
flushSync(() => increment.click());
|
||||
assert.deepEqual(logs, [0, 1]); // [0] would also be ok; the 1 must be done in the context of the fork (impossible to assert so you gotta check manually)
|
||||
|
||||
commit.click();
|
||||
assert.deepEqual(logs, [0, 1]);
|
||||
|
||||
flushSync(() => merge.click());
|
||||
await tick();
|
||||
assert.deepEqual(logs, [0, 1]);
|
||||
assert.htmlEqual(target.innerHTML, buttons);
|
||||
|
||||
// Resolve the obsolete request for 0, then the existing request for 1.
|
||||
resolve.click();
|
||||
resolve.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, [0, 1]);
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>1</p>`);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,25 @@
|
||||
<script>
|
||||
import { fork } from 'svelte';
|
||||
|
||||
let show = $state(false);
|
||||
let count = $state(0);
|
||||
let gate = $state(1);
|
||||
let f;
|
||||
|
||||
const deferred = [];
|
||||
|
||||
function load(value) {
|
||||
console.log(value);
|
||||
return new Promise((resolve) => deferred.push(() => resolve(value)));
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={() => (f = fork(() => (show = true)))}>preload</button>
|
||||
<button onclick={() => count++}>increment</button>
|
||||
<button onclick={() => f.commit()}>commit</button>
|
||||
<button onclick={() => gate++}>merge</button>
|
||||
<button onclick={() => deferred.shift()?.()}>resolve</button>
|
||||
|
||||
{#if show && gate > 0}
|
||||
<p>{await load(count)}</p>
|
||||
{/if}
|
||||
@ -0,0 +1,54 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
const buttons = `
|
||||
<button>fork a</button>
|
||||
<button>fork b</button>
|
||||
<button>reveal</button>
|
||||
<button>commit a</button>
|
||||
<button>commit b</button>
|
||||
<button>reset</button>
|
||||
`;
|
||||
|
||||
// A branch that was created by fork `a` must not show `a`'s values when it is
|
||||
// revealed by another batch (be it a real one or another fork), and `a` must
|
||||
// still show its own values when it's committed afterwards
|
||||
export default test({
|
||||
async test({ assert, target }) {
|
||||
const [fork_a, fork_b, reveal, commit_a, commit_b, reset] = target.querySelectorAll('button');
|
||||
|
||||
for (const mode of ['real', 'fork']) {
|
||||
try {
|
||||
fork_a.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, buttons);
|
||||
|
||||
if (mode === 'real') {
|
||||
reveal.click();
|
||||
} else {
|
||||
fork_b.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, buttons);
|
||||
commit_b.click();
|
||||
}
|
||||
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<b>static</b><p>0</p><span>0</span>`);
|
||||
|
||||
commit_a.click();
|
||||
await tick();
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`${buttons}<b>static</b><p>1</p><span>1</span><span>2</span>`
|
||||
);
|
||||
|
||||
reset.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, buttons);
|
||||
} catch (e) {
|
||||
/** @type {Error} */ (e).message = `${mode}: ${/** @type {Error} */ (e).message}`;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,22 @@
|
||||
<script>
|
||||
import { fork } from 'svelte';
|
||||
|
||||
let show = $state(false);
|
||||
let count = $state(0);
|
||||
let items = $state([0]);
|
||||
let a;
|
||||
let b;
|
||||
</script>
|
||||
|
||||
<button onclick={() => (a = fork(() => { show = true; count = 1; items = [1, 2]; }))}>fork a</button>
|
||||
<button onclick={() => (b = fork(() => (show = true)))}>fork b</button>
|
||||
<button onclick={() => (show = true)}>reveal</button>
|
||||
<button onclick={() => a.commit()}>commit a</button>
|
||||
<button onclick={() => b.commit()}>commit b</button>
|
||||
<button onclick={() => { a?.discard(); b?.discard(); show = false; count = 0; items = [0]; }}>reset</button>
|
||||
|
||||
{#if show}
|
||||
<svelte:boundary><b>static</b></svelte:boundary>
|
||||
<p>{count}</p>
|
||||
{#each items as item (item)}<span>{item}</span>{/each}
|
||||
{/if}
|
||||
@ -0,0 +1,53 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
// The fallback of an each block, created by a fork, is adopted by a real batch: it has to run the
|
||||
// fallback's async expression itself, since the fork's result only exists in the fork
|
||||
export default test({
|
||||
mode: ['client'],
|
||||
async test({ assert, target, logs }) {
|
||||
const [fork_empty, empty, reset, resolve_all, discard, commit] =
|
||||
target.querySelectorAll('button');
|
||||
const [output] = target.querySelectorAll('main');
|
||||
|
||||
const settle = async () => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await tick();
|
||||
resolve_all.click();
|
||||
}
|
||||
await tick();
|
||||
};
|
||||
|
||||
for (const finish of ['discard', 'commit']) {
|
||||
for (const resolved of [true, false]) {
|
||||
const mode = `${finish}, ${resolved ? 'resolved' : 'pending'}`;
|
||||
|
||||
try {
|
||||
fork_empty.click();
|
||||
if (resolved) {
|
||||
resolve_all.click();
|
||||
await tick();
|
||||
}
|
||||
assert.deepEqual(logs.splice(0), ['empty']);
|
||||
|
||||
empty.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs.splice(0), ['empty']);
|
||||
|
||||
if (finish === 'discard') discard.click();
|
||||
else commit.click();
|
||||
await settle();
|
||||
assert.htmlEqual(output.innerHTML, '<p>empty</p>');
|
||||
assert.deepEqual(logs, []);
|
||||
|
||||
reset.click();
|
||||
await settle();
|
||||
assert.htmlEqual(output.innerHTML, '<b>0</b>');
|
||||
} catch (e) {
|
||||
/** @type {Error} */ (e).message = `${mode}: ${/** @type {Error} */ (e).message}`;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,25 @@
|
||||
<script>
|
||||
import { fork } from 'svelte';
|
||||
|
||||
let list = $state([0]);
|
||||
const queued = [];
|
||||
let current_fork;
|
||||
|
||||
function push(value) {
|
||||
console.log(value);
|
||||
return new Promise((resolve) => queued.push(() => resolve(value)));
|
||||
}
|
||||
|
||||
function resolve_all() {
|
||||
for (const r of queued.splice(0)) r();
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={() => (current_fork = fork(() => (list = [])))}>fork empty</button>
|
||||
<button onclick={() => (list = [])}>empty</button>
|
||||
<button onclick={() => (list = [0])}>reset</button>
|
||||
<button onclick={resolve_all}>resolve all</button>
|
||||
<button onclick={() => current_fork?.discard()}>discard</button>
|
||||
<button onclick={() => current_fork?.commit()}>commit</button>
|
||||
|
||||
<main>{#each list as x (x)}<b>{x}</b>{:else}<p>{await push('empty')}</p>{/each}</main>
|
||||
@ -0,0 +1,41 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
const buttons = `
|
||||
<button>fork</button>
|
||||
<button>resolve</button>
|
||||
<button>commit</button>
|
||||
`;
|
||||
|
||||
export default test({
|
||||
async test({ assert, target, logs }) {
|
||||
await tick();
|
||||
const [fork_button, resolve, commit] = target.querySelectorAll('button');
|
||||
|
||||
assert.deepEqual(logs, [[0, true]]);
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>0</p>`);
|
||||
|
||||
fork_button.click();
|
||||
|
||||
await tick();
|
||||
assert.deepEqual(logs, [
|
||||
[0, true],
|
||||
[2, true]
|
||||
]);
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>0</p>`);
|
||||
|
||||
// `delayed` changes, but `nonnegative` stays true. The async expression has
|
||||
// already consumed the fork's `doubled`, so it should not run again.
|
||||
resolve.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, [
|
||||
[0, true],
|
||||
[2, true]
|
||||
]);
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>0</p>`);
|
||||
|
||||
commit.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>2</p>`);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,27 @@
|
||||
<script>
|
||||
import { fork } from 'svelte';
|
||||
|
||||
const resolvers = [];
|
||||
let f;
|
||||
|
||||
let count = $state(0);
|
||||
let doubled = $derived(count * 2);
|
||||
let delayed = $derived(await delay(count));
|
||||
let nonnegative = $derived(delayed >= 0);
|
||||
|
||||
function delay(value) {
|
||||
if (value === 0) return value;
|
||||
return new Promise((resolve) => resolvers.push(() => resolve(value)));
|
||||
}
|
||||
|
||||
function load(value, nonnegative) {
|
||||
console.log([value, nonnegative]);
|
||||
return value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={() => (f = fork(() => count++))}>fork</button>
|
||||
<button onclick={() => resolvers.shift()?.()}>resolve</button>
|
||||
<button onclick={() => f?.commit()}>commit</button>
|
||||
|
||||
<p>{await load(doubled, nonnegative)}</p>
|
||||
@ -0,0 +1,50 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
const buttons =
|
||||
'<button>fork</button><button>a = 2</button><button>shift</button><button>commit</button>';
|
||||
|
||||
// Like `async-fork-equal-result-dropped`, but a block depends on the async expression: the fork's
|
||||
// re-run result equals the real value, but differs from the fork's outdated one, so the block has to
|
||||
// update in the fork (its effects are considered up to date with the fork once it is committed)
|
||||
export default test({
|
||||
mode: ['client'],
|
||||
async test({ assert, target }) {
|
||||
const [fork, a2, shift, commit] = target.querySelectorAll('button');
|
||||
|
||||
shift.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>other</p><span>0|0</span>`);
|
||||
|
||||
// the fork's result is 1
|
||||
fork.click();
|
||||
await tick();
|
||||
shift.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>other</p><span>0|0</span>`);
|
||||
|
||||
// the real world overtakes the fork's `a`, the real batch and the fork both run the expression with 2
|
||||
a2.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>other</p><span>0|0</span>`);
|
||||
|
||||
// the real batch's result
|
||||
shift.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>other</p><span>2|0</span>`);
|
||||
|
||||
// the fork's result, which equals the real value
|
||||
shift.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>other</p><span>2|0</span>`);
|
||||
|
||||
commit.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>other</p><span>2|1</span>`);
|
||||
|
||||
// nothing left over
|
||||
shift.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>other</p><span>2|1</span>`);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,28 @@
|
||||
<script>
|
||||
import { fork } from 'svelte';
|
||||
|
||||
let a = $state(0);
|
||||
let b = $state(0);
|
||||
|
||||
/** @type {ReturnType<typeof fork> | null} */
|
||||
let f = null;
|
||||
|
||||
/** @type {Array<() => void>} */
|
||||
const deferred = [];
|
||||
|
||||
/** @param {number} value */
|
||||
function delay(value) {
|
||||
return new Promise((resolve) => deferred.push(() => resolve(value)));
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={() => (f = fork(() => { a = 1; b = 1; }))}>fork</button>
|
||||
<button onclick={() => (a = 2)}>a = 2</button>
|
||||
<button onclick={() => deferred.shift()?.()}>shift</button>
|
||||
<button onclick={() => f?.commit()}>commit</button>
|
||||
|
||||
<svelte:boundary>
|
||||
{#if (await delay(a)) === 1}<p>one</p>{:else}<p>other</p>{/if}
|
||||
<span>{a}|{b}</span>
|
||||
{#snippet pending()}{/snippet}
|
||||
</svelte:boundary>
|
||||
@ -0,0 +1,8 @@
|
||||
<script>
|
||||
let { total, navigating } = $props();
|
||||
const pages = $derived(Math.ceil(total / 28));
|
||||
const pending = $derived(navigating && pages > 1);
|
||||
</script>
|
||||
|
||||
{#if pending}pending{/if}
|
||||
<p>{pages}</p>
|
||||
@ -0,0 +1,50 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
const buttons = `
|
||||
<button>preload</button>
|
||||
<button>navigate</button>
|
||||
<button>commit</button>
|
||||
<button>discard</button>
|
||||
`;
|
||||
|
||||
export default test({
|
||||
async test({ assert, target, logs }) {
|
||||
const [preload, navigate, commit, discard] = target.querySelectorAll('button');
|
||||
|
||||
preload.click();
|
||||
// Let the async child resolve, without committing its fork.
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
assert.htmlEqual(target.innerHTML, buttons);
|
||||
assert.deepEqual(logs, []);
|
||||
|
||||
// A real-world update must not evaluate the speculative child in the real world.
|
||||
navigate.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, buttons);
|
||||
assert.deepEqual(logs, []);
|
||||
discard.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, buttons);
|
||||
assert.deepEqual(logs, []);
|
||||
|
||||
navigate.click();
|
||||
await tick();
|
||||
preload.click();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
navigate.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, buttons);
|
||||
assert.deepEqual(logs, []);
|
||||
|
||||
commit.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}pending <p>2</p>`);
|
||||
assert.deepEqual(logs, []);
|
||||
|
||||
navigate.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>2</p>`);
|
||||
assert.deepEqual(logs, []);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,20 @@
|
||||
<script>
|
||||
import { fork } from 'svelte';
|
||||
import Child from './Child.svelte';
|
||||
|
||||
let show = $state(false);
|
||||
let navigating = $state(false);
|
||||
let f;
|
||||
</script>
|
||||
|
||||
<button onclick={() => (f = fork(() => (show = true)))}>preload</button>
|
||||
<button onclick={() => (navigating = !navigating)}>navigate</button>
|
||||
<button onclick={() => f.commit()}>commit</button>
|
||||
<button onclick={() => f.discard()}>discard</button>
|
||||
|
||||
{#if show}
|
||||
<svelte:boundary onerror={(error) => console.log(error.message)}>
|
||||
{#snippet pending()}loading{/snippet}
|
||||
<Child total={await Promise.resolve(42)} {navigating} />
|
||||
</svelte:boundary>
|
||||
{/if}
|
||||
@ -0,0 +1,14 @@
|
||||
<script>
|
||||
let { x, s, slow, log } = $props();
|
||||
|
||||
// async result of x; `d` only flips when it crosses 0, so incrementing x doesn't change it
|
||||
let sx = $derived(await slow(x));
|
||||
let d = $derived(sx > 0);
|
||||
|
||||
$effect(() => {
|
||||
log(`${s}/${d}`);
|
||||
});
|
||||
</script>
|
||||
|
||||
<p>{sx}</p>
|
||||
<p>{await slow(s)}</p>
|
||||
@ -0,0 +1,33 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
// When a later batch's write is hidden from an earlier batch, its write version must be
|
||||
// hidden as well — otherwise reactions of that source look dirty to the earlier batch
|
||||
// and re-run even though they see the old (unchanged) value
|
||||
export default test({
|
||||
async test({ assert, target, logs }) {
|
||||
const [x, s, resolve] = target.querySelectorAll('button');
|
||||
|
||||
resolve.click();
|
||||
await tick();
|
||||
resolve.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['0/true']);
|
||||
|
||||
x.click(); // B1: slow(x = 2) pending; `d` stays true
|
||||
await tick();
|
||||
s.click(); // B2: slow(s = 1) pending; the $effect is dirtied by `s` and deferred in B2
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['0/true']);
|
||||
|
||||
// B1 commits while B2 is pending. In B1's world nothing the effect depends on changed
|
||||
// (`s` is hidden and still 0, `d` is still true), so it must not run
|
||||
resolve.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['0/true']);
|
||||
|
||||
resolve.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['0/true', '1/true']);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,23 @@
|
||||
<script>
|
||||
import Child from './Child.svelte';
|
||||
let x = $state(1);
|
||||
let s = $state(0);
|
||||
|
||||
const resolvers = [];
|
||||
function slow(v) {
|
||||
return new Promise((r) => resolvers.push(() => r(v)));
|
||||
}
|
||||
function log(v) {
|
||||
console.log(v);
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={() => x++}>x</button>
|
||||
<button onclick={() => s++}>s</button>
|
||||
<button onclick={() => resolvers.shift()?.()}>resolve</button>
|
||||
<button onclick={() => resolvers.pop()?.()}>resolve last</button>
|
||||
|
||||
<svelte:boundary>
|
||||
{#snippet pending()}loading{/snippet}
|
||||
<Child {x} {s} {slow} {log} />
|
||||
</svelte:boundary>
|
||||
@ -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,49 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
const buttons = `
|
||||
<button>A0</button>
|
||||
<button>A</button>
|
||||
<button>B</button>
|
||||
<button>resolve q</button>
|
||||
<button>resolve p</button>
|
||||
<button>resolve t</button>
|
||||
<button>resolve r</button>
|
||||
<button>init</button>
|
||||
`;
|
||||
|
||||
// Test ensure dependencies on earlier batches are also merged correctly
|
||||
export default test({
|
||||
async test({ assert, target }) {
|
||||
const [A0, A, B, resolve_q, resolve_p, resolve_t, resolve_r, init] =
|
||||
target.querySelectorAll('button');
|
||||
|
||||
init.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>q0 r0 p0 t0 s0</p>`);
|
||||
|
||||
A0.click(); // q=1, r=1 -> slow(q=1), slow(r=1)
|
||||
await tick();
|
||||
A.click(); // q=2, p=1, s=1 -> slow(q=2), slow(p=1); depends on A0
|
||||
await tick();
|
||||
B.click(); // s=2, t=1 -> slow(t=1); depends on A
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>q0 r0 p0 t0 s0</p>`);
|
||||
|
||||
// A's runs resolve -> A merges into A0, which is still waiting on slow(r=1)
|
||||
resolve_q.click();
|
||||
resolve_p.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>q0 r0 p0 t0 s0</p>`);
|
||||
|
||||
// B's run resolves -> B must wait for A0
|
||||
resolve_t.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>q0 r0 p0 t0 s0</p>`);
|
||||
|
||||
// A0's run resolves -> everything commits at once
|
||||
resolve_r.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>q2 r1 p1 t1 s2</p>`);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,27 @@
|
||||
<script>
|
||||
let q = $state(0);
|
||||
let r = $state(0);
|
||||
let p = $state(0);
|
||||
let s = $state(0);
|
||||
let t = $state(0);
|
||||
|
||||
const resolvers = { q: [], r: [], p: [], t: [] };
|
||||
|
||||
function slow(kind, v) {
|
||||
return new Promise((resolve) => resolvers[kind].push(() => resolve(v)));
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={() => { q++; r++; }}>A0</button>
|
||||
<button onclick={() => { q++; p++; s++; }}>A</button>
|
||||
<button onclick={() => { s++; t++; }}>B</button>
|
||||
<button onclick={() => resolvers.q.pop()?.()}>resolve q</button>
|
||||
<button onclick={() => resolvers.p.pop()?.()}>resolve p</button>
|
||||
<button onclick={() => resolvers.t.pop()?.()}>resolve t</button>
|
||||
<button onclick={() => resolvers.r.shift()?.()}>resolve r</button>
|
||||
<button onclick={() => { for (const k in resolvers) resolvers[k].shift()?.(); }}>init</button>
|
||||
|
||||
<svelte:boundary>
|
||||
{#snippet pending()}loading{/snippet}
|
||||
<p>q{await slow('q', q)} r{await slow('r', r)} p{await slow('p', p)} t{await slow('t', t)} s{s}</p>
|
||||
</svelte:boundary>
|
||||
@ -0,0 +1,32 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
const buttons = `
|
||||
<button>show</button>
|
||||
<button>gate</button>
|
||||
<button>resolve</button>
|
||||
`;
|
||||
|
||||
export default test({
|
||||
async test({ assert, target, logs }) {
|
||||
const [show, gate, resolve] = target.querySelectorAll('button');
|
||||
|
||||
show.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['load']);
|
||||
|
||||
// An unrelated update re-runs the block while the first batch is still pending.
|
||||
// It sees `show = false` (the pending batch's write is held back) and commits
|
||||
// without the branch — but the pending batch still wants it, so the branch
|
||||
// (and its in-flight request) must survive rather than be recreated
|
||||
gate.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['load']);
|
||||
assert.htmlEqual(target.innerHTML, buttons);
|
||||
|
||||
resolve.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['load']);
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>loaded</p>`);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,19 @@
|
||||
<script>
|
||||
let show = $state(false);
|
||||
let gate = $state(1);
|
||||
|
||||
const deferred = [];
|
||||
|
||||
function load() {
|
||||
console.log('load');
|
||||
return new Promise((resolve) => deferred.push(() => resolve('loaded')));
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={() => (show = true)}>show</button>
|
||||
<button onclick={() => gate++}>gate</button>
|
||||
<button onclick={() => deferred.shift()?.()}>resolve</button>
|
||||
|
||||
{#if show && gate > 0}
|
||||
<p>{await load()}</p>
|
||||
{/if}
|
||||
Loading…
Reference in new issue