mirror of https://github.com/sveltejs/svelte
parent
4bdf9e3db4
commit
762e5cb6d8
@ -0,0 +1,36 @@
|
||||
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 latest value (1) rather than the held-back one (0)
|
||||
y.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['effect 1 1']);
|
||||
|
||||
// the pending batch settles and commits x === 1 — exactly the value
|
||||
// the effect already saw, so it should not re-run
|
||||
shift.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['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,33 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
const buttons = '<button>x</button><button>show</button><button>shift</button>';
|
||||
|
||||
export default test({
|
||||
async test({ assert, target, logs }) {
|
||||
await tick();
|
||||
const [x, show, shift] = target.querySelectorAll('button');
|
||||
|
||||
assert.htmlEqual(target.innerHTML, `<p>true</p><p>1</p>${buttons}`);
|
||||
assert.deepEqual(logs, []);
|
||||
|
||||
// write x — the batch is pending on its async expression, and claims
|
||||
// the `positive` derived (marked through x)
|
||||
x.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, []);
|
||||
|
||||
// an independent batch runs the effect, which reads the claimed derived
|
||||
// through the pending batch's overlay (x = 1, so `positive` is true)
|
||||
show.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['positive true']);
|
||||
|
||||
// the pending batch settles and commits x = 2 — `positive` recomputes
|
||||
// to the same value (true), so the effect should not re-run
|
||||
shift.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['positive true']);
|
||||
assert.htmlEqual(target.innerHTML, `<p>true</p><p>2</p>${buttons}`);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,26 @@
|
||||
<script>
|
||||
let x = $state(1);
|
||||
let show = $state(false);
|
||||
let pend = false;
|
||||
|
||||
const deferred = [];
|
||||
|
||||
let positive = $derived(x > 0);
|
||||
|
||||
function delay(value) {
|
||||
if (!pend) return value;
|
||||
return new Promise((resolve) => deferred.push(() => resolve(value)));
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (show) {
|
||||
console.log(`positive ${positive}`);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<p>{positive}</p>
|
||||
<p>{await delay(x)}</p>
|
||||
<button onclick={() => { pend = true; x += 1; }}>x</button>
|
||||
<button onclick={() => (show = true)}>show</button>
|
||||
<button onclick={() => deferred.shift()?.()}>shift</button>
|
||||
@ -0,0 +1,37 @@
|
||||
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.deepEqual(logs, ['effect _ 0']);
|
||||
|
||||
// write x — the batch is pending on its async expression
|
||||
x.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['effect _ 0']);
|
||||
|
||||
// the effect newly depends on `x` — it reads the latest value (1)
|
||||
// rather than the held-back one (0)
|
||||
y.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['effect _ 0', 'effect 1 1']);
|
||||
|
||||
// the effect re-runs and no longer depends on `x` at all
|
||||
y.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['effect _ 0', 'effect 1 1', 'effect _ 2']);
|
||||
|
||||
// the pending batch settles and commits x = 1 — the effect no longer
|
||||
// depends on `x`, so it should not re-run
|
||||
shift.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['effect _ 0', 'effect 1 1', 'effect _ 2']);
|
||||
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);
|
||||
|
||||
const deferred = [];
|
||||
|
||||
function delay(value) {
|
||||
if (!value) return value;
|
||||
return new Promise((resolve) => deferred.push(() => resolve(value)));
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (y === 1) {
|
||||
console.log(`effect ${x} ${y}`);
|
||||
} else {
|
||||
console.log(`effect _ ${y}`);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<p>{await delay(x)}</p>
|
||||
<button onclick={() => x++}>x</button>
|
||||
<button onclick={() => y++}>y</button>
|
||||
<button onclick={() => deferred.shift()?.()}>shift</button>
|
||||
@ -0,0 +1,33 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
async test({ assert, target, logs }) {
|
||||
await tick();
|
||||
const [revert, y, shift] = target.querySelectorAll('button');
|
||||
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
'<p>0</p><button>revert</button><button>y</button><button>shift</button>'
|
||||
);
|
||||
assert.deepEqual(logs, []);
|
||||
|
||||
// write x and revert it within the same batch — the batch is pending
|
||||
// (its async expression re-runs), with previous === current for `x`
|
||||
revert.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, []);
|
||||
|
||||
// an independent batch runs the effect, which now reads `x` through
|
||||
// the pending batch's overlay (seeing the held-back value 0)
|
||||
y.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['effect 0 1']);
|
||||
|
||||
// the pending batch settles and commits x === 0, i.e. exactly the value
|
||||
// the effect already saw — it should not re-run
|
||||
shift.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['effect 0 1']);
|
||||
}
|
||||
});
|
||||
@ -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 -= 1; }}>revert</button>
|
||||
<button onclick={() => y++}>y</button>
|
||||
<button onclick={() => deferred.shift()?.()}>shift</button>
|
||||
@ -0,0 +1,16 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
async test({ assert, target }) {
|
||||
await tick();
|
||||
const [increment] = target.querySelectorAll('button');
|
||||
|
||||
assert.htmlEqual(target.innerHTML, '<button>increment</button><p>0</p>');
|
||||
|
||||
increment.click();
|
||||
await tick();
|
||||
|
||||
assert.htmlEqual(target.innerHTML, '<button>increment</button><p>2</p>');
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,14 @@
|
||||
<script>
|
||||
let count = $state(0);
|
||||
let runs = 0;
|
||||
|
||||
function delay(value, eager_value) {
|
||||
if (runs++ > 10) throw new Error('infinite loop detected')
|
||||
if (value === 0) return 0;
|
||||
return Promise.resolve(value + eager_value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={() => count += 1}>increment</button>
|
||||
|
||||
<p>{await delay(count, $state.eager(count))}</p>
|
||||
@ -0,0 +1,36 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
const buttons = '<button>count</button><button>eager</button><button>shift</button>';
|
||||
|
||||
export default test({
|
||||
// running more than once per bump
|
||||
async test({ assert, target, logs }) {
|
||||
await tick();
|
||||
const [count, eager, shift] = target.querySelectorAll('button');
|
||||
|
||||
assert.htmlEqual(target.innerHTML, `<p>-1</p>${buttons}`);
|
||||
assert.deepEqual(logs, []);
|
||||
|
||||
// count++ creates a pending batch; the inner block (containing the
|
||||
// $state.eager expression) is created inside it and belongs to it.
|
||||
// it does not depend on `count`
|
||||
count.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['inner 0']);
|
||||
|
||||
// an eager version bump re-runs the inner block in the eager batch's
|
||||
// world — since the block doesn't depend on the pending batch's
|
||||
// changes, it sees exactly the same values the owner's world would
|
||||
eager.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['inner 0', 'inner 1']);
|
||||
|
||||
// the pending batch settles — nothing the inner block sees has
|
||||
// changed since its eager run, so it should not be re-run
|
||||
shift.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['inner 0', 'inner 1']);
|
||||
assert.htmlEqual(target.innerHTML, `<p>0</p><span>1</span>${buttons}`);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,26 @@
|
||||
<script>
|
||||
let count = $state(-1);
|
||||
let eag = $state(0);
|
||||
|
||||
const deferred = [];
|
||||
|
||||
function delay(value) {
|
||||
if (value < 0) return value;
|
||||
return new Promise((resolve) => deferred.push(() => resolve(value)));
|
||||
}
|
||||
|
||||
function log(value) {
|
||||
console.log(`inner ${value}`);
|
||||
return value >= 0;
|
||||
}
|
||||
</script>
|
||||
|
||||
<p>{await delay(count)}</p>
|
||||
{#if count >= 0}
|
||||
{#if log($state.eager(eag))}
|
||||
<span>{eag}</span>
|
||||
{/if}
|
||||
{/if}
|
||||
<button onclick={() => count++}>count</button>
|
||||
<button onclick={() => eag++}>eager</button>
|
||||
<button onclick={() => deferred.shift()?.()}>shift</button>
|
||||
@ -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, instance }) {
|
||||
const [fork_button, update, resolve, discard] = target.querySelectorAll('button');
|
||||
|
||||
fork_button.click();
|
||||
await tick();
|
||||
assert.equal(instance.get_calls(), 1);
|
||||
assert.htmlEqual(target.innerHTML, buttons);
|
||||
|
||||
// Transfer an invalidation into the fork while its async work is pending.
|
||||
update.click();
|
||||
await tick();
|
||||
assert.equal(instance.get_calls(), 2); // can also be 1 at this point already, would also be ok
|
||||
|
||||
try {
|
||||
resolve.click();
|
||||
await tick();
|
||||
assert.equal(instance.get_calls(), 2);
|
||||
|
||||
// Completing the replacement must not replay the same invalidation.
|
||||
resolve.click();
|
||||
await tick();
|
||||
assert.equal(instance.get_calls(), 2);
|
||||
} finally {
|
||||
discard.click();
|
||||
await tick();
|
||||
}
|
||||
|
||||
assert.htmlEqual(target.innerHTML, buttons);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,42 @@
|
||||
<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 });
|
||||
|
||||
export function get_calls() {
|
||||
return calls;
|
||||
}
|
||||
|
||||
function load(value) {
|
||||
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,45 @@
|
||||
import { flushSync, tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
const buttons = `
|
||||
<button>preload</button>
|
||||
<button>increment</button>
|
||||
<button>commit</button>
|
||||
<button>reveal</button>
|
||||
<button>discard</button>
|
||||
<button>preload second</button>
|
||||
<button>commit second</button>
|
||||
<button>reset</button>
|
||||
`;
|
||||
|
||||
export default test({
|
||||
async test({ assert, target }) {
|
||||
const [preload, increment, commit, reveal, discard, preload_second, commit_second, reset] =
|
||||
target.querySelectorAll('button');
|
||||
|
||||
for (const mode of ['commit', 'reveal', 'second-fork']) {
|
||||
preload.click();
|
||||
flushSync(() => increment.click());
|
||||
assert.htmlEqual(target.innerHTML, buttons);
|
||||
|
||||
if (mode === 'commit') {
|
||||
commit.click();
|
||||
await tick();
|
||||
} else if (mode === 'reveal') {
|
||||
flushSync(() => reveal.click());
|
||||
discard.click();
|
||||
} else {
|
||||
preload_second.click();
|
||||
discard.click();
|
||||
commit_second.click();
|
||||
await tick();
|
||||
}
|
||||
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>1 2</p>`);
|
||||
flushSync(() => increment.click());
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>2 4</p>`);
|
||||
flushSync(() => reset.click());
|
||||
assert.htmlEqual(target.innerHTML, buttons);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,22 @@
|
||||
<script>
|
||||
import { fork } from 'svelte';
|
||||
|
||||
let show = $state(false);
|
||||
let count = $state(0);
|
||||
let doubled = $derived(count * 2);
|
||||
let f;
|
||||
let other;
|
||||
</script>
|
||||
|
||||
<button onclick={() => (f = fork(() => (show = true)))}>preload</button>
|
||||
<button onclick={() => count++}>increment</button>
|
||||
<button onclick={() => f.commit()}>commit</button>
|
||||
<button onclick={() => (show = true)}>reveal</button>
|
||||
<button onclick={() => f.discard()}>discard</button>
|
||||
<button onclick={() => (other = fork(() => (show = true)))}>preload second</button>
|
||||
<button onclick={() => other.commit()}>commit second</button>
|
||||
<button onclick={() => { show = false; count = 0; }}>reset</button>
|
||||
|
||||
{#if show}
|
||||
<p>{count} {doubled}</p>
|
||||
{/if}
|
||||
@ -0,0 +1,43 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
const buttons =
|
||||
'<button>fork</button><button>y</button><button>shift</button><button>commit</button>';
|
||||
|
||||
export default test({
|
||||
async test({ assert, target }) {
|
||||
await tick();
|
||||
const [forkButton, y, shift, commit] = target.querySelectorAll('button');
|
||||
|
||||
assert.htmlEqual(target.innerHTML, `<p>0</p>${buttons}`);
|
||||
|
||||
// speculative world: x becomes 1, async expression runs with x + y = 1
|
||||
forkButton.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `<p>0</p>${buttons}`);
|
||||
|
||||
// real world: y becomes 1, async expression runs with x + y = 1
|
||||
// (computed with the pre-fork x = 0) — this run supersedes the
|
||||
// fork's validation of the effect
|
||||
y.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `<p>0</p>${buttons}`);
|
||||
|
||||
// commit the fork while the real batch is still pending — x = 1 is
|
||||
// written, and the async expression must eventually re-run with the
|
||||
// committed value, because its in-flight run used x = 0
|
||||
commit.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `<p>0</p>${buttons}`);
|
||||
|
||||
// resolve all in-flight runs (superseded ones are no-ops)
|
||||
for (let i = 0; i < 4; i += 1) {
|
||||
shift.click();
|
||||
await tick();
|
||||
}
|
||||
|
||||
// x = 1, y = 1 — anything else means the effect resolved with a value
|
||||
// computed from stale inputs and was never re-run
|
||||
assert.htmlEqual(target.innerHTML, `<p>2</p>${buttons}`);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,20 @@
|
||||
<script>
|
||||
import { fork } from 'svelte';
|
||||
|
||||
let x = $state(0);
|
||||
let y = $state(0);
|
||||
let f;
|
||||
|
||||
const deferred = [];
|
||||
|
||||
function delay(value) {
|
||||
if (!value) return value;
|
||||
return new Promise((resolve) => deferred.push(() => resolve(value)));
|
||||
}
|
||||
</script>
|
||||
|
||||
<p>{await delay(x + y)}</p>
|
||||
<button onclick={() => { f = fork(() => x++); }}>fork</button>
|
||||
<button onclick={() => y++}>y</button>
|
||||
<button onclick={() => deferred.shift()?.()}>shift</button>
|
||||
<button onclick={() => f.commit()}>commit</button>
|
||||
@ -0,0 +1,34 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
async test({ assert, target, logs }) {
|
||||
await tick();
|
||||
const [x, y, shift, pop, commit] = target.querySelectorAll('button');
|
||||
const [p] = target.querySelectorAll('p');
|
||||
logs.length = 0;
|
||||
|
||||
y.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['called with 0,1']);
|
||||
logs.length = 0;
|
||||
|
||||
x.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['called with 1,1']);
|
||||
assert.htmlEqual(p.innerHTML, '0');
|
||||
logs.length = 0;
|
||||
|
||||
shift.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, []);
|
||||
assert.htmlEqual(p.innerHTML, '1');
|
||||
|
||||
commit.click();
|
||||
await tick();
|
||||
pop.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, []);
|
||||
assert.htmlEqual(p.innerHTML, '2');
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,23 @@
|
||||
<script>
|
||||
import { fork } from 'svelte';
|
||||
|
||||
let x = $state(0);
|
||||
let y = $state(0);
|
||||
let f;
|
||||
|
||||
const deferred = [];
|
||||
|
||||
function delay(_, value) {
|
||||
if (!value) return value;
|
||||
return new Promise((resolve) => deferred.push(() => resolve(value)));
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<button onclick={() => {f = fork(() => x++)}}>x</button>
|
||||
<button onclick={() => y++}>y</button>
|
||||
<button onclick={() => deferred.shift()?.()}>shift</button>
|
||||
<button onclick={() => deferred.pop()?.()}>pop</button>
|
||||
<button onclick={() => f.commit()}>commit</button>
|
||||
|
||||
<p>{await delay(console.log('called with ' + x + ',' + y), x + y)}</p>
|
||||
@ -0,0 +1,42 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
async test({ assert, target, logs }) {
|
||||
await tick();
|
||||
const [forkButton, real, shift, discard] = target.querySelectorAll('button');
|
||||
|
||||
assert.deepEqual(logs, ['b 0']);
|
||||
logs.length = 0;
|
||||
|
||||
// speculative world: fork writes b and c, runs the async expression
|
||||
forkButton.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['b 1']);
|
||||
|
||||
// real world: b++ re-runs the async expression for real
|
||||
real.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['b 1', 'b 1']);
|
||||
|
||||
// resolve the fork's in-flight run — the fork is still speculative,
|
||||
// nothing should be committed or re-run
|
||||
shift.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['b 1', 'b 1']);
|
||||
|
||||
// resolve the real run — the real batch commits b = 1. The fork's world
|
||||
// value of `b` is also 1 (its own write, now also committed), so the
|
||||
// fork's async expression sees unchanged inputs and should not re-run
|
||||
shift.click();
|
||||
await tick();
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
'<p>1 0</p><button>fork</button><button>real</button><button>shift</button><button>discard</button>'
|
||||
);
|
||||
assert.deepEqual(logs, ['b 1', 'b 1']);
|
||||
|
||||
discard.click();
|
||||
await tick();
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,20 @@
|
||||
<script>
|
||||
import { fork } from 'svelte';
|
||||
|
||||
let b = $state(0);
|
||||
let c = $state(0);
|
||||
let f;
|
||||
|
||||
const deferred = [];
|
||||
|
||||
function delay(_, value) {
|
||||
if (!value) return value;
|
||||
return new Promise((resolve) => deferred.push(() => resolve(value)));
|
||||
}
|
||||
</script>
|
||||
|
||||
<p>{await delay(console.log(`b ${b}`), b)} {c}</p>
|
||||
<button onclick={() => { f = fork(() => { b += 1; c += 1; }); }}>fork</button>
|
||||
<button onclick={() => b++}>real</button>
|
||||
<button onclick={() => deferred.shift()?.()}>shift</button>
|
||||
<button onclick={() => f.discard()}>discard</button>
|
||||
@ -0,0 +1,61 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
const buttons = `
|
||||
<button>up</button>
|
||||
<button>down</button>
|
||||
<button>show1</button>
|
||||
<button>show2</button>
|
||||
<button>shift a</button>
|
||||
<button>shift t</button>
|
||||
`;
|
||||
|
||||
export default test({
|
||||
async test({ assert, target }) {
|
||||
await tick();
|
||||
|
||||
const [up, down, show1, show2, shift_a, shift_t] = target.querySelectorAll('button');
|
||||
|
||||
// batch B: reveals boundary 1 -> commits with pending snippet, stays live
|
||||
show1.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons} <p>loading 1...</p>`);
|
||||
|
||||
// batch C: reveals boundary 2 -> commits with pending snippet, stays live
|
||||
show2.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons} <p>loading 1...</p> <p>loading 2...</p>`);
|
||||
|
||||
// batch A: writes a=1; the async-a effect is owned by C, so A merges with
|
||||
// C and stays pending. B remains separate
|
||||
up.click();
|
||||
await tick();
|
||||
|
||||
// B's continuation first-reads `a`, which is overlaid by the pending
|
||||
// merged batch. B has already committed its UI, so it cannot entangle —
|
||||
// it reads the latest value (1) instead
|
||||
shift_t.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons} <p>late read: 1</p> <p>loading 2...</p>`);
|
||||
|
||||
// revert `a` to 0 inside the pending batch — its eventual commit leaves
|
||||
// `a` unchanged. The write re-runs the late reader (it acquired `a` as a
|
||||
// dependency), so it re-awaits a fresh deferred
|
||||
down.click();
|
||||
await tick();
|
||||
|
||||
// resolve the pending batch's async-a runs -> it commits
|
||||
shift_a.click();
|
||||
await tick();
|
||||
shift_a.click();
|
||||
await tick();
|
||||
shift_a.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons} <p>late read: 1</p> <p>async a: 0</p>`);
|
||||
|
||||
// resolve the late reader's re-run -> it converges on the committed value
|
||||
shift_t.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons} <p>late read: 0</p> <p>async a: 0</p>`);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,49 @@
|
||||
<script>
|
||||
let a = $state(0);
|
||||
let show1 = $state(false);
|
||||
let show2 = $state(false);
|
||||
|
||||
let deferreds = [];
|
||||
|
||||
function push(key, v) {
|
||||
const d = Promise.withResolvers();
|
||||
deferreds.push({ key, v, d });
|
||||
return d.promise;
|
||||
}
|
||||
|
||||
function shift(key, override) {
|
||||
const i = deferreds.findIndex((d) => d.key === key);
|
||||
if (i === -1) return;
|
||||
const [{ v, d }] = deferreds.splice(i, 1);
|
||||
d.resolve(override ?? v);
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={() => a++}>up</button>
|
||||
<button onclick={() => a--}>down</button>
|
||||
<button onclick={() => (show1 = true)}>show1</button>
|
||||
<button onclick={() => (show2 = true)}>show2</button>
|
||||
<button onclick={() => shift('a')}>shift a</button>
|
||||
<button onclick={() => shift('t', 1)}>shift t</button>
|
||||
|
||||
{#if show1}
|
||||
<svelte:boundary>
|
||||
<!-- no state deps initially; reads `a` for the first time only when
|
||||
the awaited value (controlled by the test) says so -->
|
||||
<p>late read: {(await push('t', 0)) > 0 ? a : -1}</p>
|
||||
|
||||
{#snippet pending()}
|
||||
<p>loading 1...</p>
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
{/if}
|
||||
|
||||
{#if show2}
|
||||
<svelte:boundary>
|
||||
<p>async a: {await push('a', a)}</p>
|
||||
|
||||
{#snippet pending()}
|
||||
<p>loading 2...</p>
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
{/if}
|
||||
@ -0,0 +1,30 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
async test({ assert, target }) {
|
||||
await tick();
|
||||
const [a_b, b, resolve] = target.querySelectorAll('button');
|
||||
|
||||
a_b.click();
|
||||
await tick();
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
'<button>a_b 0_0</button> <button>b 0</button> <button>resolve</button> 0'
|
||||
);
|
||||
|
||||
b.click();
|
||||
await tick();
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
'<button>a_b 0_0</button> <button>b 0</button> <button>resolve</button> 0'
|
||||
);
|
||||
|
||||
resolve.click();
|
||||
await tick();
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
'<button>a_b 1_2</button> <button>b 2</button> <button>resolve</button> 1'
|
||||
);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,18 @@
|
||||
<script>
|
||||
let a = $state(0);
|
||||
let b = $state(0);
|
||||
let deferreds = [];
|
||||
const value = () => (`${a}_${b}`);
|
||||
|
||||
function push(value) {
|
||||
if (!value) return value;
|
||||
return new Promise((resolve) => {
|
||||
deferreds.push(() => resolve(value));
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={() => {a++;b++}}>a_b {value()}</button>
|
||||
<button onclick={() => (b++)}>b {b}</button>
|
||||
<button onclick={() => (deferreds.shift()?.())}>resolve</button>
|
||||
{await push(a)}
|
||||
@ -0,0 +1,7 @@
|
||||
<script>
|
||||
let { x } = $props();
|
||||
console.log(x.x);
|
||||
$effect(() => console.log('$effect: '+ x.x))
|
||||
</script>
|
||||
|
||||
{x.x}
|
||||
@ -0,0 +1,44 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
async test({ assert, target, logs }) {
|
||||
const [x, y, resolve] = target.querySelectorAll('button');
|
||||
|
||||
x.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['universe']);
|
||||
|
||||
y.click();
|
||||
await tick();
|
||||
// the new branch reads `x`, which the pending batch has written, as a new
|
||||
// dependency — the two batches entangle, so the new branch is held back
|
||||
// until the async work completes. Its $effect is deferred, but the
|
||||
// init-time console.log necessarily runs eagerly (with the latest value)
|
||||
assert.deepEqual(logs, ['universe', 'universe']);
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`
|
||||
<button>x</button>
|
||||
<button>y++</button>
|
||||
<button>resolve</button>
|
||||
`
|
||||
);
|
||||
|
||||
resolve.click();
|
||||
await tick();
|
||||
// both branches commit together, fully consistent
|
||||
assert.deepEqual(logs, ['universe', 'universe', '$effect: universe', '$effect: universe']);
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`
|
||||
<button>x</button>
|
||||
<button>y++</button>
|
||||
<button>resolve</button>
|
||||
universe
|
||||
universe
|
||||
universe
|
||||
`
|
||||
);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,28 @@
|
||||
<script>
|
||||
import Child from './Child.svelte';
|
||||
|
||||
let x = $state();
|
||||
let y = $state(0);
|
||||
let deferred = [];
|
||||
|
||||
function delay(s) {
|
||||
const d = Promise.withResolvers();
|
||||
deferred.push(() => d.resolve(s))
|
||||
return d.promise;
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={() => x = {x:'universe'}}>x</button>
|
||||
|
||||
<button onclick={() => y++}>y++</button>
|
||||
|
||||
<button onclick={() => deferred.shift()()}>resolve</button>
|
||||
|
||||
{#if x?.x === 'universe'}
|
||||
{await delay(x.x)}
|
||||
<Child {x} />
|
||||
{/if}
|
||||
|
||||
{#if y > 0}
|
||||
<Child {x} />
|
||||
{/if}
|
||||
@ -0,0 +1,51 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
async test({ assert, target }) {
|
||||
const [x, y, resolve] = target.querySelectorAll('button');
|
||||
|
||||
x.click();
|
||||
await tick();
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`
|
||||
<button>x</button>
|
||||
<button>y++</button>
|
||||
<button>resolve</button>
|
||||
<h1>WORLD</h1>
|
||||
`
|
||||
);
|
||||
|
||||
y.click();
|
||||
await tick();
|
||||
// the new branch reads `upper` — a derived owned by the pending batch — as
|
||||
// a new dependency. The two batches entangle, so the new branch is held
|
||||
// back until the async work completes (rather than rendering with the
|
||||
// derived's pre-write value, 'WORLD')
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`
|
||||
<button>x</button>
|
||||
<button>y++</button>
|
||||
<button>resolve</button>
|
||||
<h1>WORLD</h1>
|
||||
`
|
||||
);
|
||||
|
||||
resolve.click();
|
||||
await tick();
|
||||
// both branches commit together, fully consistent
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`
|
||||
<button>x</button>
|
||||
<button>y++</button>
|
||||
<button>resolve</button>
|
||||
<h1>UNIVERSE</h1>
|
||||
universe
|
||||
<p>UNIVERSE</p>
|
||||
`
|
||||
);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,29 @@
|
||||
<script>
|
||||
let x = $state({ x: 'world' });
|
||||
let y = $state(0);
|
||||
let deferred = [];
|
||||
|
||||
const upper = $derived(x.x.toUpperCase());
|
||||
|
||||
function delay(s) {
|
||||
const d = Promise.withResolvers();
|
||||
deferred.push(() => d.resolve(s));
|
||||
return d.promise;
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={() => (x = { x: 'universe' })}>x</button>
|
||||
|
||||
<button onclick={() => y++}>y++</button>
|
||||
|
||||
<button onclick={() => deferred.shift()()}>resolve</button>
|
||||
|
||||
<h1>{upper}</h1>
|
||||
|
||||
{#if x.x === 'universe'}
|
||||
{await delay(x.x)}
|
||||
{/if}
|
||||
|
||||
{#if y > 0}
|
||||
<p>{upper}</p>
|
||||
{/if}
|
||||
@ -0,0 +1,63 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
async test({ assert, target }) {
|
||||
const [a, t, shift_a, shift_t] = target.querySelectorAll('button');
|
||||
|
||||
shift_a.click();
|
||||
shift_t.click();
|
||||
await tick();
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`
|
||||
<button>a</button>
|
||||
<button>t</button>
|
||||
<button>shift a</button>
|
||||
<button>shift t</button>
|
||||
<p>async a: 0</p>
|
||||
<p>late read: -1</p>
|
||||
`
|
||||
);
|
||||
|
||||
// batch A: writes `a`, stays pending (its promise is unresolved)
|
||||
a.click();
|
||||
await tick();
|
||||
|
||||
// batch B: writes `t`; resolve its promise so the continuation
|
||||
// reads `a` for the first time while A is still pending. This
|
||||
// entangles B with A — both worlds are held back and commit together
|
||||
t.click();
|
||||
await tick();
|
||||
shift_t.click();
|
||||
await tick();
|
||||
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`
|
||||
<button>a</button>
|
||||
<button>t</button>
|
||||
<button>shift a</button>
|
||||
<button>shift t</button>
|
||||
<p>async a: 0</p>
|
||||
<p>late read: -1</p>
|
||||
`
|
||||
);
|
||||
|
||||
// commit the merged batch
|
||||
shift_a.click();
|
||||
await tick();
|
||||
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`
|
||||
<button>a</button>
|
||||
<button>t</button>
|
||||
<button>shift a</button>
|
||||
<button>shift t</button>
|
||||
<p>async a: 1</p>
|
||||
<p>late read: 1</p>
|
||||
`
|
||||
);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,35 @@
|
||||
<script>
|
||||
let a = $state(0);
|
||||
let t = $state(0);
|
||||
|
||||
let deferreds = [];
|
||||
|
||||
function push(key, v) {
|
||||
const d = Promise.withResolvers();
|
||||
deferreds.push({ key, v, d });
|
||||
return d.promise;
|
||||
}
|
||||
|
||||
function shift(key) {
|
||||
const i = deferreds.findIndex((d) => d.key === key);
|
||||
if (i === -1) return;
|
||||
const [{ v, d }] = deferreds.splice(i, 1);
|
||||
d.resolve(v);
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={() => a++}>a</button>
|
||||
<button onclick={() => t++}>t</button>
|
||||
<button onclick={() => shift('a')}>shift a</button>
|
||||
<button onclick={() => shift('t')}>shift t</button>
|
||||
|
||||
<svelte:boundary>
|
||||
<p>async a: {await push('a', a)}</p>
|
||||
|
||||
<!-- reads `a` after an await, but only once t > 0 -->
|
||||
<p>late read: {(await push('t', t), t > 0 ? a : -1)}</p>
|
||||
|
||||
{#snippet pending()}
|
||||
<p>loading...</p>
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
@ -0,0 +1,41 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
mode: ['client'],
|
||||
async test({ assert, target }) {
|
||||
await tick();
|
||||
const [update, show, resolve] = target.querySelectorAll('button');
|
||||
|
||||
update.click();
|
||||
await tick();
|
||||
|
||||
show.click();
|
||||
await tick();
|
||||
|
||||
// the template effect newly depends on `value`, which the pending batch
|
||||
// has written — it reads the latest value rather than the pre-write one
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`
|
||||
<button>update</button>
|
||||
<button>show</button>
|
||||
<button>resolve</button>
|
||||
<p>1</p>
|
||||
`
|
||||
);
|
||||
|
||||
resolve.click();
|
||||
await tick();
|
||||
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`
|
||||
<button>update</button>
|
||||
<button>show</button>
|
||||
<button>resolve</button>
|
||||
<p>1</p>
|
||||
`
|
||||
);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,17 @@
|
||||
<script>
|
||||
let value = $state();
|
||||
let show = $state(false);
|
||||
const deferred = Promise.withResolvers();
|
||||
|
||||
function wait(value) {
|
||||
return value === undefined ? '' : deferred.promise;
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={() => (value = { x: 1 })}>update</button>
|
||||
<button onclick={() => (show = true)}>show</button>
|
||||
<button onclick={() => deferred.resolve('')}>resolve</button>
|
||||
|
||||
{await wait(value)}
|
||||
|
||||
<p>{show ? value.x : ''}</p>
|
||||
@ -0,0 +1,31 @@
|
||||
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');
|
||||
|
||||
assert.htmlEqual(target.innerHTML, `<p>0</p><p>0</p>${buttons}`);
|
||||
|
||||
// batch A writes `a` and pends on its async expression
|
||||
a.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `<p>0</p><p>0</p>${buttons}`);
|
||||
|
||||
// batch B writes `b` — it only shares the `{add(a, b)}` template expression
|
||||
// (a leaf) with batch A, so the two are independent: B commits right away,
|
||||
// rendering its own view of the world (in which `a` is still 0)
|
||||
b.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `<p>0</p><p>1</p>${buttons}`);
|
||||
|
||||
// the async expression settles — A commits and brings the shared
|
||||
// template expression up to date
|
||||
shift.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `<p>1</p><p>2</p>${buttons}`);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,23 @@
|
||||
<script>
|
||||
let a = $state(0);
|
||||
let b = $state(0);
|
||||
const c = $derived(add(a, b))
|
||||
let pend = false;
|
||||
|
||||
const deferred = [];
|
||||
|
||||
function delay(value) {
|
||||
if (!pend) return value;
|
||||
return new Promise((resolve) => deferred.push(() => resolve(value)));
|
||||
}
|
||||
|
||||
function add(x, y) {
|
||||
return x + y;
|
||||
}
|
||||
</script>
|
||||
|
||||
<p>{await delay(a)}</p>
|
||||
<p>{c}</p>
|
||||
<button onclick={() => { pend = true; a++; }}>a</button>
|
||||
<button onclick={() => b++}>b</button>
|
||||
<button onclick={() => deferred.shift()?.()}>shift</button>
|
||||
@ -0,0 +1,38 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
const buttons = '<button>z</button><button>w</button><button>shift</button>';
|
||||
|
||||
export default test({
|
||||
async test({ assert, target, logs }) {
|
||||
await tick();
|
||||
const [z, w, shift] = target.querySelectorAll('button');
|
||||
|
||||
assert.htmlEqual(target.innerHTML, `<p>true</p><p>0</p>${buttons}`);
|
||||
assert.deepEqual(logs, ['eval true']);
|
||||
|
||||
// with no other batch pending, the equality cut-off works: `c`
|
||||
// recomputes to the same value, the template expression is not
|
||||
// re-evaluated
|
||||
z.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['eval true']);
|
||||
|
||||
// write w — the batch is pending on its async expression
|
||||
w.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['eval true']);
|
||||
|
||||
// z++ recomputes `c` to the same value again — the template expression
|
||||
// should still not be re-evaluated, even though another batch is pending
|
||||
z.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['eval true']);
|
||||
|
||||
// the pending batch settles — nothing `c` depends on changed
|
||||
shift.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['eval true']);
|
||||
assert.htmlEqual(target.innerHTML, `<p>true</p><p>1</p>${buttons}`);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,25 @@
|
||||
<script>
|
||||
let z = $state(1);
|
||||
let w = $state(0);
|
||||
let pend = false;
|
||||
|
||||
const deferred = [];
|
||||
|
||||
let c = $derived(z > 0);
|
||||
|
||||
function delay(value) {
|
||||
if (!pend) return value;
|
||||
return new Promise((resolve) => deferred.push(() => resolve(value)));
|
||||
}
|
||||
|
||||
function log(value) {
|
||||
console.log(`eval ${value}`);
|
||||
return value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<p>{log(c)}</p>
|
||||
<p>{await delay(w)}</p>
|
||||
<button onclick={() => z++}>z</button>
|
||||
<button onclick={() => { pend = true; w += 1; }}>w</button>
|
||||
<button onclick={() => deferred.shift()?.()}>shift</button>
|
||||
@ -0,0 +1,36 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
const buttons = '<button>b</button><button>z</button><button>shift</button>';
|
||||
|
||||
export default test({
|
||||
async test({ assert, target, logs }) {
|
||||
await tick();
|
||||
const [b, z, shift] = target.querySelectorAll('button');
|
||||
|
||||
assert.htmlEqual(target.innerHTML, `<p>0</p><p>a</p>${buttons}`);
|
||||
assert.deepEqual(logs, ['fetch a']);
|
||||
|
||||
// write x = 'b' — the batch is pending on its async expression
|
||||
b.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['fetch a', 'fetch b']);
|
||||
|
||||
// an independent batch runs the effect, which writes x = 'b' — the
|
||||
// real value is already 'b', so this is a no-op: the async expression
|
||||
// must not re-run (no needless refetch), and the batch must not be
|
||||
// entangled with the pending one (z commits immediately)
|
||||
z.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['fetch a', 'fetch b']);
|
||||
assert.htmlEqual(target.innerHTML, `<p>1</p><p>a</p>${buttons}`);
|
||||
|
||||
// the pending batch settles
|
||||
shift.click();
|
||||
await tick();
|
||||
shift.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['fetch a', 'fetch b']);
|
||||
assert.htmlEqual(target.innerHTML, `<p>1</p><p>b</p>${buttons}`);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,25 @@
|
||||
<script>
|
||||
let x = $state('a');
|
||||
let z = $state(0);
|
||||
let pend = false;
|
||||
|
||||
const deferred = [];
|
||||
|
||||
function delay(value) {
|
||||
console.log(`fetch ${value}`);
|
||||
if (!pend) return value;
|
||||
return new Promise((resolve) => deferred.push(() => resolve(value)));
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (z > 0) {
|
||||
x = 'b';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<p>{z}</p>
|
||||
<p>{await delay(x)}</p>
|
||||
<button onclick={() => { pend = true; x = 'b'; }}>b</button>
|
||||
<button onclick={() => z++}>z</button>
|
||||
<button onclick={() => deferred.shift()?.()}>shift</button>
|
||||
@ -0,0 +1,33 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
const buttons = '<button>b</button><button>reset</button><button>shift</button>';
|
||||
|
||||
export default test({
|
||||
async test({ assert, target, logs }) {
|
||||
await tick();
|
||||
const [b, reset, shift] = target.querySelectorAll('button');
|
||||
|
||||
assert.htmlEqual(target.innerHTML, `<p>0</p><p>a</p>${buttons}`);
|
||||
assert.deepEqual(logs, ['fetch a']);
|
||||
|
||||
// write x = 'b' — the batch is pending on its async expression
|
||||
b.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['fetch a', 'fetch b']);
|
||||
|
||||
// an independent batch runs the effect, which resets x = 'a'. The real
|
||||
// (pending) value is 'b', so this is a genuine change and must not be
|
||||
// swallowed just because the effect's world still shows 'a'
|
||||
reset.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['fetch a', 'fetch b', 'fetch a']);
|
||||
|
||||
// resolve all in-flight runs — the reset must win
|
||||
shift.click();
|
||||
await tick();
|
||||
shift.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `<p>1</p><p>a</p>${buttons}`);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,25 @@
|
||||
<script>
|
||||
let x = $state('a');
|
||||
let z = $state(0);
|
||||
let pend = false;
|
||||
|
||||
const deferred = [];
|
||||
|
||||
function delay(value) {
|
||||
console.log(`fetch ${value}`);
|
||||
if (!pend) return value;
|
||||
return new Promise((resolve) => deferred.push(() => resolve(value)));
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (z > 0) {
|
||||
x = 'a';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<p>{z}</p>
|
||||
<p>{await delay(x)}</p>
|
||||
<button onclick={() => { pend = true; x = 'b'; }}>b</button>
|
||||
<button onclick={() => z++}>reset</button>
|
||||
<button onclick={() => deferred.shift()?.()}>shift</button>
|
||||
Loading…
Reference in new issue