mirror of https://github.com/sveltejs/svelte
parent
e19a7530da
commit
1cdc1709d4
@ -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(), 1); // can also be 2 at this point, would also be ok
|
||||
|
||||
try {
|
||||
resolve.click();
|
||||
await tick();
|
||||
assert.equal(instance.get_calls(), 1);
|
||||
|
||||
// Completing the replacement must not replay the same invalidation.
|
||||
resolve.click();
|
||||
await tick();
|
||||
assert.equal(instance.get_calls(), 1);
|
||||
} 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,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,52 @@
|
||||
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']) {
|
||||
try {
|
||||
preload.click();
|
||||
await tick();
|
||||
increment.click();
|
||||
await tick();
|
||||
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);
|
||||
} 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 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,21 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
const buttons = `<button>fork</button><button>show</button><button>commit</button>`;
|
||||
|
||||
export default test({
|
||||
async test({ assert, target }) {
|
||||
const [fork, show, commit] = target.querySelectorAll('button');
|
||||
|
||||
fork.click();
|
||||
await tick();
|
||||
|
||||
show.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>0</p>`);
|
||||
|
||||
commit.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<span>two</span><p>2</p>`);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,17 @@
|
||||
<script>
|
||||
import { fork } from 'svelte';
|
||||
|
||||
let s1 = $state(0);
|
||||
let show = $state(false);
|
||||
let d2 = $derived(s1 * 2);
|
||||
let f;
|
||||
</script>
|
||||
|
||||
<button onclick={() => (f = fork(() => (s1 = 1)))}>fork</button>
|
||||
<button onclick={() => (show = true)}>show</button>
|
||||
<button onclick={() => f.commit()}>commit</button>
|
||||
|
||||
{#if d2 === 2}<span>two</span>{/if}
|
||||
{#if show}
|
||||
<p>{d2}</p>
|
||||
{/if}
|
||||
@ -0,0 +1,22 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
const buttons = `<button>preload</button><button>other</button><button>commit</button>`;
|
||||
|
||||
// If a fork is auto-discarded it should not throw on user-commit.
|
||||
export default test({
|
||||
mode: ['client'],
|
||||
async test({ assert, target }) {
|
||||
const [preload, other, commit] = target.querySelectorAll('button');
|
||||
|
||||
preload.click();
|
||||
await tick();
|
||||
other.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>true 1</p>`);
|
||||
|
||||
commit.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>true 1</p>`);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,14 @@
|
||||
<script>
|
||||
import { fork } from 'svelte';
|
||||
|
||||
let open = $state(true);
|
||||
let other = $state(0);
|
||||
let pending;
|
||||
let error = $state('');
|
||||
</script>
|
||||
|
||||
<button onclick={() => { pending ??= fork(() => { open = true; }); }}>preload</button>
|
||||
<button onclick={() => other++}>other</button>
|
||||
<button onclick={() => { pending.commit().catch((e) => (error = e.message)); pending = null; }}>commit</button>
|
||||
|
||||
<p>{open} {other} {error}</p>
|
||||
@ -0,0 +1,42 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
const buttons = `
|
||||
<button>fork</button>
|
||||
<button>x = 5</button>
|
||||
<button>commit</button>
|
||||
<button>shift</button>
|
||||
`;
|
||||
|
||||
// A fork writes `x`, then the real world writes `x` as well (with async work still pending).
|
||||
// The fork's write is overtaken and it has nothing left to commit — `commit()` must resolve
|
||||
// rather than throw `fork_discarded`, and the real world's value wins
|
||||
export default test({
|
||||
mode: ['client'],
|
||||
async test({ assert, target }) {
|
||||
await tick();
|
||||
const [fork, x5, commit, shift] = target.querySelectorAll('button');
|
||||
|
||||
fork.click(); // speculative: delay(10)
|
||||
await tick();
|
||||
x5.click(); // real: delay(5); the fork adopts x = 5 and re-runs: delay(5)
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>0</p>`);
|
||||
|
||||
commit.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>0</p>`);
|
||||
|
||||
shift.click(); // the fork's obsolete delay(10)
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>0</p>`);
|
||||
|
||||
shift.click(); // the real delay(5)
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>5</p>`);
|
||||
|
||||
shift.click(); // the fork's delay(5), rejected when the fork was cleaned up
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>5</p>`);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,20 @@
|
||||
<script>
|
||||
import { fork } from 'svelte';
|
||||
|
||||
let x = $state(0);
|
||||
let f;
|
||||
let error = $state('');
|
||||
const deferred = [];
|
||||
|
||||
function delay(value) {
|
||||
if (value === 0) return value;
|
||||
return new Promise((resolve) => deferred.push(() => resolve(value)));
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={() => { f = fork(() => { x = 10; }); }}>fork</button>
|
||||
<button onclick={() => (x = 5)}>x = 5</button>
|
||||
<button onclick={() => { f.commit().catch((e) => (error = e.message)); }}>commit</button>
|
||||
<button onclick={() => deferred.shift()?.()}>shift</button>
|
||||
|
||||
<p>{await delay(x)} {error}</p>
|
||||
@ -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,38 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
const buttons = `
|
||||
<button>fork x = 1</button>
|
||||
<button>update y and discard</button>
|
||||
<button>commit y fork and discard</button>
|
||||
<button>resolve requests</button>
|
||||
<button>reset</button>
|
||||
`;
|
||||
|
||||
export default test({
|
||||
mode: ['client'],
|
||||
async test({ assert, target, logs }) {
|
||||
await tick();
|
||||
const [speculate, update, commit, resolve, reset] = target.querySelectorAll('button');
|
||||
|
||||
for (const action of [update, commit]) {
|
||||
speculate.click();
|
||||
await tick();
|
||||
logs.length = 0;
|
||||
|
||||
// Discard before the queued fork revalidation runs. It must not restart
|
||||
// the async effect and abort the real world's request, whether the write
|
||||
// came from a normal update or another fork's commit.
|
||||
action.click();
|
||||
await tick();
|
||||
resolve.click();
|
||||
await tick();
|
||||
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>0/1</p>`);
|
||||
assert.deepEqual(logs, ['0/1']);
|
||||
|
||||
reset.click();
|
||||
await tick();
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,48 @@
|
||||
<script>
|
||||
import { fork, getAbortSignal } from 'svelte';
|
||||
|
||||
let x = $state(0);
|
||||
let y = $state(0);
|
||||
let f;
|
||||
const resolvers = [];
|
||||
|
||||
function load(x, y) {
|
||||
console.log(`${x}/${y}`);
|
||||
const signal = getAbortSignal();
|
||||
if (y === 0) return `${x}/${y}`;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
resolvers.push(() => resolve(`${x}/${y}`));
|
||||
signal.addEventListener('abort', () => reject(signal.reason));
|
||||
});
|
||||
}
|
||||
|
||||
function speculate() {
|
||||
f = fork(() => { x = 1; });
|
||||
}
|
||||
|
||||
function update_and_discard(use_fork) {
|
||||
if (use_fork) {
|
||||
fork(() => { y = 1; }).commit();
|
||||
} else {
|
||||
y = 1;
|
||||
}
|
||||
f.discard();
|
||||
}
|
||||
|
||||
function finish() {
|
||||
for (const resolve of resolvers.splice(0)) resolve();
|
||||
}
|
||||
|
||||
function reset() {
|
||||
y = 0;
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={speculate}>fork x = 1</button>
|
||||
<button onclick={() => update_and_discard(false)}>update y and discard</button>
|
||||
<button onclick={() => update_and_discard(true)}>commit y fork and discard</button>
|
||||
<button onclick={finish}>resolve requests</button>
|
||||
<button onclick={reset}>reset</button>
|
||||
|
||||
<p>{await load(x, y)}</p>
|
||||
@ -0,0 +1,38 @@
|
||||
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;
|
||||
|
||||
x.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['called with 1,0']);
|
||||
logs.length = 0;
|
||||
|
||||
y.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['called with 0,1', 'called with 1,1']); // if 'called with 1,1' happens a few button clicks later that would also be ok
|
||||
assert.htmlEqual(p.innerHTML, '0');
|
||||
logs.length = 0;
|
||||
|
||||
pop.click(); // the rerunning fork
|
||||
await tick();
|
||||
pop.click(); // the first real world run
|
||||
await tick();
|
||||
assert.deepEqual(logs, []);
|
||||
assert.htmlEqual(p.innerHTML, '1');
|
||||
logs.length = 0;
|
||||
|
||||
commit.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, []);
|
||||
assert.htmlEqual(p.innerHTML, '2');
|
||||
|
||||
pop.click();
|
||||
await tick();
|
||||
}
|
||||
});
|
||||
@ -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,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,48 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
async test({ assert, target, logs }) {
|
||||
await tick();
|
||||
const [fork, update, pop, commit] = target.querySelectorAll('button');
|
||||
const [sum, doubled] = target.querySelectorAll('p');
|
||||
logs.length = 0;
|
||||
|
||||
fork.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['sum 1,0']);
|
||||
logs.length = 0;
|
||||
|
||||
// Revalidate twice to also check that retaining async results does not
|
||||
// prevent the fork from responding to genuine changes to its inputs.
|
||||
for (const y of [1, 2]) {
|
||||
update.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, [`sum 0,${y}`, `sum 1,${y}`]);
|
||||
logs.length = 0;
|
||||
|
||||
pop.click(); // resolve the fork before the real world
|
||||
await tick();
|
||||
assert.deepEqual(logs, [`double ${y + 1}`]);
|
||||
logs.length = 0;
|
||||
|
||||
pop.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, [`double ${y}`]);
|
||||
assert.htmlEqual(sum.innerHTML, String(y));
|
||||
assert.htmlEqual(doubled.innerHTML, String(y * 2));
|
||||
logs.length = 0;
|
||||
}
|
||||
|
||||
commit.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, []);
|
||||
assert.htmlEqual(sum.innerHTML, '3');
|
||||
assert.htmlEqual(doubled.innerHTML, '6');
|
||||
|
||||
pop.click(); // the superseded first fork run
|
||||
await tick();
|
||||
assert.htmlEqual(sum.innerHTML, '3');
|
||||
assert.htmlEqual(doubled.innerHTML, '6');
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,29 @@
|
||||
<script>
|
||||
import { fork } from 'svelte';
|
||||
|
||||
let x = $state(0);
|
||||
let y = $state(0);
|
||||
let f;
|
||||
const deferred = [];
|
||||
|
||||
function delay(x, y) {
|
||||
console.log(`sum ${x},${y}`);
|
||||
const value = x + y;
|
||||
return value ? new Promise((resolve) => deferred.push(() => resolve(value))) : value;
|
||||
}
|
||||
|
||||
async function double(value) {
|
||||
console.log(`double ${value}`);
|
||||
return value * 2;
|
||||
}
|
||||
|
||||
let sum = $derived(await delay(x, y));
|
||||
</script>
|
||||
|
||||
<button onclick={() => { f = fork(() => x++); }}>fork</button>
|
||||
<button onclick={() => y++}>update</button>
|
||||
<button onclick={() => deferred.pop()?.()}>pop</button>
|
||||
<button onclick={() => f.commit()}>commit</button>
|
||||
|
||||
<p>{sum}</p>
|
||||
<p>{await double(sum)}</p>
|
||||
@ -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,32 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
const buttons = `
|
||||
<button>preload</button>
|
||||
<button>reveal</button>
|
||||
<button>hide</button>
|
||||
<button>commit</button>
|
||||
`;
|
||||
|
||||
export default test({
|
||||
async test({ assert, target }) {
|
||||
const [preload, reveal, hide, commit] = target.querySelectorAll('button');
|
||||
|
||||
preload.click();
|
||||
reveal.click();
|
||||
await tick();
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`${buttons}<b>0</b><p>constant</p><p>keyed</p><p>boundary</p>`
|
||||
);
|
||||
|
||||
hide.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<b>0</b>`);
|
||||
|
||||
// The remaining fork write must not resurrect its obsolete branch selection.
|
||||
commit.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<b>1</b>`);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,22 @@
|
||||
<script>
|
||||
import { fork } from 'svelte';
|
||||
|
||||
let show = $state(false);
|
||||
let other = $state(0);
|
||||
let f;
|
||||
</script>
|
||||
|
||||
<button onclick={() => (f = fork(() => { show = true; other = 1; }))}>preload</button>
|
||||
<button onclick={() => (show = true)}>reveal</button>
|
||||
<button onclick={() => (show = false)}>hide</button>
|
||||
<button onclick={() => f.commit()}>commit</button>
|
||||
|
||||
<b>{other}</b>
|
||||
|
||||
{#if show}
|
||||
{#if true}<p>constant</p>{/if}
|
||||
{#key 1}<p>keyed</p>{/key}
|
||||
<svelte:boundary onerror={console.error}>
|
||||
<p>boundary</p>
|
||||
</svelte:boundary>
|
||||
{/if}
|
||||
@ -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,79 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
const buttons = `
|
||||
<button>preload</button>
|
||||
<button>reveal outer</button>
|
||||
<button>reveal and navigate</button>
|
||||
<button>navigate</button>
|
||||
<button>resolve</button>
|
||||
<button>commit</button>
|
||||
<button>discard</button>
|
||||
<button>reset</button>
|
||||
`;
|
||||
|
||||
export default test({
|
||||
async test({ assert, target, logs }) {
|
||||
const [preload, reveal, reveal_and_navigate, navigate, resolve, commit, discard, reset] =
|
||||
target.querySelectorAll('button');
|
||||
|
||||
for (const mode of ['separate', 'together', 'pending']) {
|
||||
for (const finish of [commit, discard]) {
|
||||
try {
|
||||
preload.click();
|
||||
|
||||
if (mode !== 'pending') {
|
||||
resolve.click();
|
||||
await tick();
|
||||
}
|
||||
|
||||
assert.htmlEqual(target.innerHTML, buttons);
|
||||
assert.deepEqual(logs, ['load']);
|
||||
|
||||
if (mode === 'together') {
|
||||
reveal_and_navigate.click();
|
||||
} else {
|
||||
reveal.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<section></section>`);
|
||||
navigate.click();
|
||||
}
|
||||
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<section></section>`);
|
||||
assert.deepEqual(logs, ['load']);
|
||||
|
||||
if (mode === 'pending') {
|
||||
resolve.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<section></section>`);
|
||||
}
|
||||
|
||||
finish.click();
|
||||
await tick();
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`${buttons}<section>${finish === commit ? 'pending <p>2</p>' : ''}</section>`
|
||||
);
|
||||
assert.deepEqual(logs, ['load']);
|
||||
|
||||
if (finish === commit) {
|
||||
navigate.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<section><p>2</p></section>`);
|
||||
assert.deepEqual(logs, ['load']);
|
||||
}
|
||||
|
||||
reset.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, buttons);
|
||||
logs.length = 0;
|
||||
} catch (e) {
|
||||
/** @type {Error} */ (e).message =
|
||||
`${mode}/${finish === commit ? 'commit' : 'discard'}: ${/** @type {Error} */ (e).message}`;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,36 @@
|
||||
<script>
|
||||
import { fork } from 'svelte';
|
||||
import Child from './Child.svelte';
|
||||
|
||||
let outer = $state(false);
|
||||
let inner = $state(false);
|
||||
let navigating = $state(false);
|
||||
let f;
|
||||
|
||||
const deferred = [];
|
||||
|
||||
function load() {
|
||||
console.log('load');
|
||||
return new Promise((resolve) => deferred.push(() => resolve(42)));
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={() => (f = fork(() => { outer = true; inner = true; }))}>preload</button>
|
||||
<button onclick={() => (outer = true)}>reveal outer</button>
|
||||
<button onclick={() => { outer = true; navigating = true; }}>reveal and navigate</button>
|
||||
<button onclick={() => (navigating = !navigating)}>navigate</button>
|
||||
<button onclick={() => deferred.shift()?.()}>resolve</button>
|
||||
<button onclick={() => f.commit()}>commit</button>
|
||||
<button onclick={() => f.discard()}>discard</button>
|
||||
<button onclick={() => { outer = false; inner = false; navigating = false; }}>reset</button>
|
||||
|
||||
{#if outer}
|
||||
<section>
|
||||
{#if inner}
|
||||
<svelte:boundary onerror={(error) => console.log(error.message)}>
|
||||
{#snippet pending()}loading{/snippet}
|
||||
<Child total={await load()} {navigating} />
|
||||
</svelte:boundary>
|
||||
{/if}
|
||||
</section>
|
||||
{/if}
|
||||
@ -0,0 +1,18 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
async test({ assert, target, logs }) {
|
||||
const [change, commit] = target.querySelectorAll('button');
|
||||
|
||||
assert.deepEqual(logs, ['hi']);
|
||||
|
||||
change.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['hi', 'hi']);
|
||||
|
||||
commit.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['hi', 'hi']);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,12 @@
|
||||
<script>
|
||||
import { fork } from 'svelte';
|
||||
|
||||
let x = $state(1);
|
||||
let y = $derived.by(() => {console.log('hi'); return x % 2});
|
||||
let f;
|
||||
</script>
|
||||
|
||||
<button onclick={() => { f = fork(() => x = 3)}}>change</button>
|
||||
<button onclick={() => f.commit()}>commit</button>
|
||||
|
||||
{#if y}hi{/if}
|
||||
@ -0,0 +1,42 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
const buttons = `
|
||||
<button>fork x = y = 1</button>
|
||||
<button>set x = y = 1</button>
|
||||
<button>set z = 1</button>
|
||||
<button>commit fork</button>
|
||||
<button>discard fork</button>
|
||||
`;
|
||||
|
||||
export default test({
|
||||
mode: ['client'],
|
||||
async test({ assert, target, logs }) {
|
||||
await tick();
|
||||
const [speculate, catch_up, update, commit, discard] = target.querySelectorAll('button');
|
||||
|
||||
speculate.click();
|
||||
await tick();
|
||||
catch_up.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>2</p>`);
|
||||
logs.length = 0;
|
||||
|
||||
// All speculative writes are obsolete. Only the real world should react.
|
||||
update.click();
|
||||
await tick();
|
||||
try {
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>3</p>`);
|
||||
assert.deepEqual(logs, ['1/1/1']);
|
||||
|
||||
// Committing an automatically discarded fork should be a no-op and not throw,
|
||||
// as the user cannot really know that something got automatically discarded.
|
||||
commit.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>3</p>`);
|
||||
assert.deepEqual(logs, ['1/1/1', 'committed']);
|
||||
} finally {
|
||||
discard.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,43 @@
|
||||
<script>
|
||||
import { fork } from 'svelte';
|
||||
|
||||
let x = $state(0);
|
||||
let y = $state(0);
|
||||
let z = $state(0);
|
||||
let f;
|
||||
|
||||
function load(x, y, z) {
|
||||
console.log(`${x}/${y}/${z}`);
|
||||
return x + y + z;
|
||||
}
|
||||
|
||||
function speculate() {
|
||||
f = fork(() => { x = 1; y = 1; });
|
||||
}
|
||||
|
||||
function catch_up() {
|
||||
x = 1;
|
||||
y = 1;
|
||||
}
|
||||
|
||||
function update() {
|
||||
z = 1;
|
||||
}
|
||||
|
||||
function discard() {
|
||||
f.discard();
|
||||
}
|
||||
|
||||
async function commit() {
|
||||
await f.commit();
|
||||
console.log('committed');
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={speculate}>fork x = y = 1</button>
|
||||
<button onclick={catch_up}>set x = y = 1</button>
|
||||
<button onclick={update}>set z = 1</button>
|
||||
<button onclick={commit}>commit fork</button>
|
||||
<button onclick={discard}>discard fork</button>
|
||||
|
||||
<p>{await load(x, y, z)}</p>
|
||||
@ -0,0 +1,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,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,17 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
async test({ assert, target }) {
|
||||
const [create, commit] = target.querySelectorAll('button');
|
||||
const [p] = target.querySelectorAll('p');
|
||||
|
||||
create.click();
|
||||
await tick();
|
||||
assert.htmlEqual(p.innerHTML, '0:0');
|
||||
|
||||
commit.click();
|
||||
await tick();
|
||||
assert.htmlEqual(p.innerHTML, '0:0');
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,20 @@
|
||||
<script>
|
||||
import { fork } from 'svelte';
|
||||
|
||||
let source = $state(0);
|
||||
let writable = $derived(source);
|
||||
let pending;
|
||||
</script>
|
||||
|
||||
<button onclick={() => {
|
||||
pending = fork(() => {
|
||||
source = 1;
|
||||
source = 0;
|
||||
writable = 1;
|
||||
writable = 0;
|
||||
});
|
||||
}}>fork</button>
|
||||
|
||||
<button onclick={() => pending.commit()}>commit</button>
|
||||
|
||||
<p>{source}:{writable}</p>
|
||||
Loading…
Reference in new issue