mirror of https://github.com/sveltejs/svelte
Merge f8276d5fbf into 8e98c03e25
commit
0454b356db
@ -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 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,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]);
|
||||||
|
|
||||||
|
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,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,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,73 @@
|
|||||||
|
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]) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -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,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,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,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>
|
||||||
Loading…
Reference in new issue