ensure offscreen each items always last; reschedule effects from earlier batches when unskipping

async-another-try-pt-3
Simon Holthausen 3 days ago
parent 11eddcce48
commit d822c50b75
No known key found for this signature in database

@ -166,6 +166,13 @@ function destroy_effects(state, to_destroy, remove_dom = true) {
const fragment = document.createDocumentFragment();
move_effect(e, fragment);
// `reconcile` expects offscreen items to come after all onscreen items
if (e !== state.effect.last) {
link(state, e.prev, e.next);
link(state, state.effect.last, e);
link(state, e, null);
}
} else {
destroy_effect(to_destroy[i], remove_dom);
}

@ -404,18 +404,24 @@ export class Batch {
* @param {Effect} effect
*/
unskip_effect(effect) {
var tracked = this.#skipped_branches.get(effect);
if (tracked) {
this.#skipped_branches.delete(effect);
for (var e of tracked.d) {
this.schedule(e, DIRTY);
}
for (var batch = /** @type {Batch | null} */ (this); batch !== null; batch = batch.prev) {
// Earlier (non-fork) batches that skip this branch have reset the effects inside it that are
// dirty because of their changes. This batch sees those changes as well, so it runs them, too
var tracked =
batch === this || !batch.is_fork ? batch.#skipped_branches.get(effect) : undefined;
if (tracked) {
for (var e of tracked.d) {
this.schedule(e, DIRTY);
}
for (e of tracked.m) {
this.schedule(e, MAYBE_DIRTY);
for (e of tracked.m) {
this.schedule(e, MAYBE_DIRTY);
}
}
}
this.#skipped_branches.delete(effect);
this.unskipped_branches.add(effect);
}

@ -0,0 +1,34 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client'],
async test({ assert, target }) {
await tick();
const [skip, readd, resolve_earlier, resolve_all] = target.querySelectorAll('button');
const buttons =
'<button>c = 2, list = [3]</button><button>list = [5, 1]</button><button>resolve 6</button><button>resolve all</button>';
assert.htmlEqual(target.innerHTML, `${buttons}<s>0|0</s><s>1|1</s>`);
// the second item is skipped by this batch, which also changes what it shows
skip.click();
await tick();
// the second item is needed again by a later batch
readd.click();
await tick();
// resolve the earlier batch first, so that it commits first
resolve_earlier.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<s>3|6</s>`);
for (let i = 0; i < 5; i++) {
await tick();
resolve_all.click();
}
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<s>5|10</s><s>1|2</s>`);
}
});

@ -0,0 +1,35 @@
<script>
let list = $state([0, 1]);
let c = $state(1);
const queued = [];
function delay(value) {
return new Promise((resolve) => queued.push({ value, resolve: () => resolve(value) }));
}
function resolve(value) {
for (const r of queued.filter((r) => r.value === value)) {
queued.splice(queued.indexOf(r), 1);
r.resolve();
}
}
function resolve_all() {
for (const r of queued.splice(0)) r.resolve();
}
// nothing (including the buttons) shows up until the initial render resolves
queueMicrotask(resolve_all);
</script>
<button
onclick={() => {
c = 2;
list = [3];
}}>c = 2, list = [3]</button
>
<button onclick={() => (list = [5, 1])}>list = [5, 1]</button>
<button onclick={() => resolve(6)}>resolve 6</button>
<button onclick={resolve_all}>resolve all</button>
{#each list as x}<s>{x}|{await delay(x * c)}</s>{/each}

@ -0,0 +1,28 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client'],
async test({ assert, target }) {
await tick();
const [set_b, list_1, list_2, resolve_all] = target.querySelectorAll('button');
const buttons =
'<button>b = 2</button><button>list = [2, 1]</button><button>list = [2, 0, 3]</button><button>resolve all</button>';
assert.htmlEqual(target.innerHTML, `${buttons}<b>0</b><b>1</b>`);
set_b.click();
await tick();
list_1.click();
await tick();
list_2.click();
for (let i = 0; i < 5; i++) {
await tick();
resolve_all.click();
}
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<b>4</b><b>2</b><b>5</b>`);
}
});

@ -0,0 +1,23 @@
<script>
let list = $state([0, 1]);
let b = $state(0);
const queued = [];
function delay(value) {
return new Promise((resolve) => queued.push(() => resolve(value)));
}
function resolve_all() {
for (const r of queued.splice(0)) r();
}
// nothing (including the buttons) shows up until the initial render resolves
queueMicrotask(resolve_all);
</script>
<button onclick={() => (b = 2)}>b = 2</button>
<button onclick={() => (list = [2, 1])}>list = [2, 1]</button>
<button onclick={() => (list = [2, 0, 3])}>list = [2, 0, 3]</button>
<button onclick={resolve_all}>resolve all</button>
{#each list as x (x)}<b>{await delay(x + b)}</b>{/each}

@ -0,0 +1,34 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client'],
async test({ assert, target }) {
await tick();
const [skip, show, resolve_earlier, resolve_all] = target.querySelectorAll('button');
const buttons =
'<button>b = 1, show = false</button><button>c = 1, show = true</button><button>resolve p1</button><button>resolve all</button>';
assert.htmlEqual(target.innerHTML, `${buttons}<p>p0</p><u>c0</u><i>i0</i>`);
// the branch is skipped by this batch, which also changes what the branch shows
skip.click();
await tick();
// the branch is needed again by a later batch
show.click();
await tick();
// resolve the earlier batch first, so that it commits first
resolve_earlier.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<p>p1</p><u>c0</u>`);
for (let i = 0; i < 5; i++) {
await tick();
resolve_all.click();
}
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<p>p1</p><u>c1</u><i>i1</i>`);
}
});

@ -0,0 +1,43 @@
<script>
let show = $state(true);
let b = $state(0);
let c = $state(0);
const queued = [];
function delay(value) {
return new Promise((resolve) => queued.push({ value, resolve: () => resolve(value) }));
}
function resolve(value) {
for (const r of queued.filter((r) => r.value === value)) {
queued.splice(queued.indexOf(r), 1);
r.resolve();
}
}
function resolve_all() {
for (const r of queued.splice(0)) r.resolve();
}
// nothing (including the buttons) shows up until the initial render resolves
queueMicrotask(resolve_all);
</script>
<button
onclick={() => {
b = 1;
show = false;
}}>b = 1, show = false</button
>
<button
onclick={() => {
c = 1;
show = true;
}}>c = 1, show = true</button
>
<button onclick={() => resolve('p1')}>resolve p1</button>
<button onclick={resolve_all}>resolve all</button>
<p>{await delay('p' + b)}</p>
<u>{await delay('c' + c)}</u>
{#if show}<i>{await delay('i' + b)}</i>{/if}
Loading…
Cancel
Save