fix edge case around equal values getting set

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

@ -111,6 +111,18 @@ export function read_batch_local_value(reaction) {
return false; return false;
} }
/**
* What a new value of `signal` is compared against (as the `this` of its `equals`). While several batches
* exist (which is when a batch view is set up), that is what the current batch holds, rather than the
* signal itself: the batch's own (e.g. outdated async) value can differ from the real one
* @param {Value} signal
* @returns {{ v: any }}
*/
export function own_value(signal) {
var own = batch_values === null ? undefined : current_batch?.current.get(signal);
return own === undefined ? signal : own;
}
/** @type {Effect | null} */ /** @type {Effect | null} */
let last_scheduled_effect = null; let last_scheduled_effect = null;
@ -882,8 +894,9 @@ export class Batch {
* @param {Value} source * @param {Value} source
* @param {any} value * @param {any} value
* @param {boolean} [is_derived] * @param {boolean} [is_derived]
* @param {boolean} [no_bump] True if the value is seen as equal, but this batch doesn't contain the value yet
*/ */
capture(source, value, is_derived = false) { capture(source, value, is_derived = false, no_bump = false) {
// Fast path for performance: When render/pre/user effects are flushed and this is the sole batch, // Fast path for performance: When render/pre/user effects are flushed and this is the sole batch,
// we don't need to capture the value // we don't need to capture the value
if ( if (
@ -903,7 +916,7 @@ export class Batch {
// Separate method for further optimization; e.g. v8 does only need to invoke // Separate method for further optimization; e.g. v8 does only need to invoke
// CreateFunctionContext in this internal method due to capturing `this` in a closure. // CreateFunctionContext in this internal method due to capturing `this` in a closure.
this.#capture(source, value, is_derived); this.#capture(source, value, is_derived, no_bump);
} }
/** /**
@ -912,13 +925,17 @@ export class Batch {
* @param {Value} source * @param {Value} source
* @param {any} value * @param {any} value
* @param {boolean} is_derived * @param {boolean} is_derived
* @param {boolean} no_bump
*/ */
#capture(source, value, is_derived) { #capture(source, value, is_derived, no_bump) {
if (source.v !== UNINITIALIZED && !this.previous.has(source)) { if (source.v !== UNINITIALIZED && !this.previous.has(source)) {
this.previous.set(source, { v: source.v, wv: source.wv }); this.previous.set(source, { v: source.v, wv: source.wv });
} }
const wv = increment_write_version(); // no_bump true means we see the same value as the real world but we still want to
// record it in our batch in case it becomes important later for batch_values etc
// calculation - we don't want to actually bump the wv etc.
const wv = no_bump ? source.wv : increment_write_version();
// Don't save errors in `batch_values`, or they won't be thrown in `runtime.js#get` // Don't save errors in `batch_values`, or they won't be thrown in `runtime.js#get`
if ((source.f & ERROR_VALUE) === 0) { if ((source.f & ERROR_VALUE) === 0) {
@ -930,7 +947,9 @@ export class Batch {
// A derived computed from inputs that differ from the real values must stay batch-local. // A derived computed from inputs that differ from the real values must stay batch-local.
// This also happens when committing a later batch hides an earlier batch's pending writes. // This also happens when committing a later batch hides an earlier batch's pending writes.
let is_latest_value = let is_latest_value =
!this.is_fork && !(is_derived && read_batch_local_value(/** @type {Derived} */ (source))); !this.is_fork &&
!no_bump &&
!(is_derived && read_batch_local_value(/** @type {Derived} */ (source)));
// A later batch may also own a newer value of the source or one of a derived's dependencies. // A later batch may also own a newer value of the source or one of a derived's dependencies.
// The check above isn't sufficient here: a later batch's write is visible through `batch_values`, // The check above isn't sufficient here: a later batch's write is visible through `batch_values`,
@ -975,7 +994,8 @@ export class Batch {
* Tell this fork that the real world has written `value` to `source`. This overtakes the * Tell this fork that the real world has written `value` to `source`. This overtakes the
* fork's own write to it, unless that is a value the fork computed itself (a derived or * fork's own write to it, unless that is a value the fork computed itself (a derived or
* the result of an async effect the fork ran). The fork is revalidated with the new value, * the result of an async effect the fork ran). The fork is revalidated with the new value,
* or discarded if it has nothing (but deriveds) left to commit. * or discarded if it has nothing left to commit (only deriveds, and values that equal the
* real ones, e.g. ones it merely holds, see `internal_set`).
* @param {Value} source * @param {Value} source
* @param {boolean} is_derived * @param {boolean} is_derived
* @param {any} value * @param {any} value
@ -984,10 +1004,14 @@ export class Batch {
const current = this.current.get(source); const current = this.current.get(source);
this.current.delete(source); this.current.delete(source);
if ([...this.current.values()].every((value) => value.is_derived)) { if (
// The real world has overtaken every write of this fork, so it is obsolete. Discard it [...this.current].every(
// right away (its speculative branches must not be adopted by anyone), and empty ([written, content]) => content.is_derived || written.equals(content.v)
// `current` so that `commit()` can tell this apart from a user-initiated discard )
) {
// The real world has overtaken every write of this fork that would change anything, so it
// is obsolete. Discard it right away (its speculative branches must not be adopted by anyone),
// and empty `current` so that `commit()` can tell this apart from a user-initiated discard
this.current.clear(); this.current.clear();
this.discard(); this.discard();
} else if (current === undefined || current.v !== value) { } else if (current === undefined || current.v !== value) {
@ -1681,6 +1705,11 @@ export function fork(fn) {
// revalidating their producers when inputs change. Capturing them anew gives them fresh write // revalidating their producers when inputs change. Capturing them anew gives them fresh write
// versions, because the real world may have run reactions since that would otherwise outrank them. // versions, because the real world may have run reactions since that would otherwise outrank them.
for (var [source, content] of batch.current) { for (var [source, content] of batch.current) {
// Values that equal the real ones (e.g. ones the fork only holds, see `internal_set`) don't change
// the real world. Capturing them would give them a new write version, which would make every
// reaction that the fork hasn't seen yet look dirty
if (source.equals(content.v)) continue;
batch.capture(source, content.v, content.is_derived); batch.capture(source, content.v, content.is_derived);
// dirty those effects the fork did not see yet, e.g. because a later batch created new branches // dirty those effects the fork did not see yet, e.g. because a later batch created new branches
if (!content.is_derived) batch.mark(source, MAYBE_DIRTY, true); if (!content.is_derived) batch.mark(source, MAYBE_DIRTY, true);

@ -48,6 +48,7 @@ import {
batch_values, batch_values,
current_batch, current_batch,
first_batch, first_batch,
own_value,
previous_batch, previous_batch,
read_batch_local_value read_batch_local_value
} from './batch.js'; } from './batch.js';
@ -243,9 +244,16 @@ export function async_derived(fn, label, location) {
decrement_pending?.(); decrement_pending?.();
deferreds.delete(d); deferreds.delete(d);
if (error === OBSOLETE) return; // Discarding a fork rejects its runs that are still in flight, but this one may have resolved just
// before, with only this handler pending. Its result must not end up in the discarded fork (which
// `commit()` would then reject as discarded, rather than treat as obsolete, see `overtake`).
// TODO ideally we also do this to non-fork unlinked batches but there's some cases where that would
// fail (e.g. when nothing waited for this result, as in a disconnected `$effect.root`); investigate
// and properly fix at some point
if (error === OBSOLETE || (batch.is_fork && !batch.linked)) return;
batch = batch.activate(); batch = batch.activate();
batch.apply();
if (error) { if (error) {
signal.f |= ERROR_VALUE; signal.f |= ERROR_VALUE;
@ -417,8 +425,9 @@ export function execute_derived(derived) {
*/ */
export function update_derived(derived) { export function update_derived(derived) {
var value = execute_derived(derived); var value = execute_derived(derived);
var d = own_value(derived);
if (!derived.equals(value)) { if (d.v === derived.v ? !derived.equals(value) : !derived.equals.call(d, value)) {
if (current_batch !== null || previous_batch !== null) { if (current_batch !== null || previous_batch !== null) {
// `capture` decides whether the underlying value is updated (it isn't in a fork, // `capture` decides whether the underlying value is updated (it isn't in a fork,
// or if a later batch holds a newer value) and records it in the batch either way. // or if a later batch holds a newer value) and records it in the batch either way.
@ -440,8 +449,6 @@ export function update_derived(derived) {
(previous_batch ?? current_batch)?.remove_dirty_reaction(derived); (previous_batch ?? current_batch)?.remove_dirty_reaction(derived);
return; return;
} }
} else if (batch_values?.has(derived) && !derived.equals(batch_values?.get(derived))) {
current_batch?.capture(derived, derived.v, true);
} }
// don't mark derived clean if we're reading it inside a // don't mark derived clean if we're reading it inside a

@ -38,7 +38,8 @@ import {
schedule_effect, schedule_effect,
legacy_updates, legacy_updates,
current_batch, current_batch,
first_batch first_batch,
own_value
} from './batch.js'; } from './batch.js';
import { proxy } from '../proxy.js'; import { proxy } from '../proxy.js';
import { execute_derived } from './deriveds.js'; import { execute_derived } from './deriveds.js';
@ -189,7 +190,8 @@ var count_deps = 0;
* @returns {V} * @returns {V}
*/ */
export function internal_set(source, value, updated_during_traversal = null) { export function internal_set(source, value, updated_during_traversal = null) {
if (!source.equals(value)) { var s = own_value(source);
if (s.v === source.v ? !source.equals(value) : !source.equals.call(s, value)) {
if (is_destroying_effect) { if (is_destroying_effect) {
old_values.set(source, value); old_values.set(source, value);
} else if (!old_values.has(source)) { } else if (!old_values.has(source)) {
@ -277,8 +279,9 @@ export function internal_set(source, value, updated_during_traversal = null) {
if (!batch.is_fork && eager_effects.size > 0 && !eager_effects_deferred) { if (!batch.is_fork && eager_effects.size > 0 && !eager_effects_deferred) {
flush_eager_effects(); flush_eager_effects();
} }
} else if (batch_values?.has(source) && !source.equals(batch_values?.get(source))) { } else if (batch_values !== null && current_batch?.linked && !current_batch.current.has(source)) {
current_batch?.capture(source, source.v); // Value is equal to the real world, still save it for correct computation of batch_values etc.
current_batch.capture(source, value, false, true);
} }
return value; return value;

@ -73,7 +73,7 @@ export type ComponentContextLegacy = ComponentContext & {
l: NonNullable<ComponentContext['l']>; l: NonNullable<ComponentContext['l']>;
}; };
export type Equals = (this: Value, value: unknown) => boolean; export type Equals = (this: Pick<Value, 'v'>, value: unknown) => boolean;
export type TemplateNode = Text | Element | Comment; export type TemplateNode = Text | Element | Comment;

@ -0,0 +1,39 @@
import { tick } from 'svelte';
import { test } from '../../test';
const buttons =
'<button>b = 1</button><button>b = 2, c = 1</button><button>resolve latest pos</button><button>resolve oldest pos</button><button>resolve all</button>';
// A batch's async result equals the real value, which a later (still pending) batch wrote. The batch
// itself sees that later batch's previous value though, so the result changes its world and has to show
export default test({
mode: ['client'],
async test({ assert, target }) {
const [b1, b2c1, resolve_latest, resolve_oldest, resolve_all] =
target.querySelectorAll('button');
resolve_all.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<p>zero</p><span>0</span><i>c0</i>`);
b1.click();
await tick();
b2c1.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<p>zero</p><span>0</span><i>c0</i>`);
// the later batch's result becomes the real value, but that batch still waits for c1
resolve_latest.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<p>zero</p><span>0</span><i>c0</i>`);
// the earlier batch's equal result completes it
resolve_oldest.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<p>pos</p><span>1</span><i>c0</i>`);
resolve_all.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<p>pos</p><span>2</span><i>c1</i>`);
}
});

@ -0,0 +1,34 @@
<script>
let b = $state(0);
let c = $state(0);
/** @type {Map<string, Array<() => void>>} */
const pending = new Map();
/** @param {string} value */
function f(value) {
return new Promise((fulfil) => {
if (!pending.has(value)) pending.set(value, []);
pending.get(value)?.push(() => fulfil(value));
});
}
function resolve_all() {
for (const list of pending.values()) {
for (const fn of list.splice(0)) fn();
}
}
</script>
<button onclick={() => (b = 1)}>b = 1</button>
<button onclick={() => ((b = 2), (c = 1))}>b = 2, c = 1</button>
<button onclick={() => pending.get('pos')?.pop()?.()}>resolve latest pos</button>
<button onclick={() => pending.get('pos')?.shift()?.()}>resolve oldest pos</button>
<button onclick={resolve_all}>resolve all</button>
<svelte:boundary>
<p>{await f(b > 0 ? 'pos' : 'zero')}</p>
<span>{b}</span>
<i>{await f('c' + c)}</i>
{#snippet pending()}{/snippet}
</svelte:boundary>

@ -0,0 +1,47 @@
import { tick } from 'svelte';
import { test } from '../../test';
const buttons =
'<button>b = 2</button><button>b = 0</button><button>resolve k3</button><button>resolve k0</button><button>resolve all</button>';
// A batch's async result that equals the current real value (but not the value the batch sees,
// because an earlier pending batch has a different one in flight) must not be dropped, or the
// earlier batch's result wins although the later batch reverted the input
export default test({
mode: ['client'],
async test({ assert, target }) {
const [b2, b0, resolve_k3, resolve_k0, resolve_all] = target.querySelectorAll('button');
resolve_all.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<b>k0</b><b>k1</b>`);
// batch 1 requests k2/k3
b2.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<b>k0</b><b>k1</b>`);
// batch 2 requests k0/k1, which equal the values currently shown
b0.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<b>k0</b><b>k1</b>`);
resolve_k3.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<b>k0</b><b>k1</b>`);
// batch 2's first result equals the real value
resolve_k0.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<b>k0</b><b>k1</b>`);
resolve_all.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<b>k0</b><b>k1</b>`);
// nothing left over
resolve_all.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<b>k0</b><b>k1</b>`);
}
});

@ -0,0 +1,36 @@
<script>
let b = $state(0);
/** @type {Map<string, Array<() => void>>} */
const pending = new Map();
/** @param {string} value */
function delay(value) {
return new Promise((fulfil) => {
if (!pending.has(value)) pending.set(value, []);
pending.get(value)?.push(() => fulfil(value));
});
}
/** @param {string} value */
function resolve(value) {
pending.get(value)?.shift()?.();
}
function resolve_all() {
for (const list of pending.values()) {
for (const fn of list.splice(0)) fn();
}
}
</script>
<button onclick={() => (b = 2)}>b = 2</button>
<button onclick={() => (b = 0)}>b = 0</button>
<button onclick={() => resolve('k3')}>resolve k3</button>
<button onclick={() => resolve('k0')}>resolve k0</button>
<button onclick={resolve_all}>resolve all</button>
<svelte:boundary>
<b>{await delay('k' + (0 + b))}</b><b>{await delay('k' + (1 + b))}</b>
{#snippet pending()}{/snippet}
</svelte:boundary>

@ -100,25 +100,24 @@ export default test({
await reset(); await reset();
// a real batch adopts an item with a different value after the fork's results are in // a real batch adopts an item with a different value after the fork's results are in
// (TODO also commit the fork here, once the fork re-running the unkeyed item's expression fork_append_2.click();
// with the real world's new value no longer keeps the fork's outdated result) resolve_all.click();
if (finish === 'discard') { await tick();
fork_append_2.click(); instance.log.length = 0;
resolve_all.click();
await tick();
instance.log.length = 0;
append_5.click(); append_5.click();
await settle(); await settle();
assert.htmlEqual(output.innerHTML, html([0, 1, 5])); assert.htmlEqual(output.innerHTML, html([0, 1, 5]));
// keyed: a new item, unkeyed: the adopted item's input changed (and the fork revalidates it) // keyed: a new item, unkeyed: the adopted item's input changed (and the fork revalidates it)
assert.deepEqual(instance.log.sort(), ['cc', 'k5', 'u5', 'u5']); assert.deepEqual(instance.log.sort(), ['cc', 'k5', 'u5', 'u5']);
discard.click(); // the fork's revalidated result for the unkeyed item equals the real one, it must not
await settle(); // commit its outdated result for it
assert.htmlEqual(output.innerHTML, html([0, 1, 5])); if (finish === 'discard') discard.click();
await reset(); else commit.click();
} await settle();
assert.htmlEqual(output.innerHTML, html([0, 1, 5]));
await reset();
} catch (e) { } catch (e) {
/** @type {Error} */ (e).message = `${finish}: ${/** @type {Error} */ (e).message}`; /** @type {Error} */ (e).message = `${finish}: ${/** @type {Error} */ (e).message}`;
throw e; throw e;

@ -0,0 +1,50 @@
import { tick } from 'svelte';
import { test } from '../../test';
const buttons =
'<button>fork</button><button>a = 2</button><button>shift</button><button>commit</button>';
// Like `async-fork-equal-result-dropped`, but a block depends on the async expression: the fork's
// re-run result equals the real value, but differs from the fork's outdated one, so the block has to
// update in the fork (its effects are considered up to date with the fork once it is committed)
export default test({
mode: ['client'],
async test({ assert, target }) {
const [fork, a2, shift, commit] = target.querySelectorAll('button');
shift.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<p>other</p><span>0|0</span>`);
// the fork's result is 1
fork.click();
await tick();
shift.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<p>other</p><span>0|0</span>`);
// the real world overtakes the fork's `a`, the real batch and the fork both run the expression with 2
a2.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<p>other</p><span>0|0</span>`);
// the real batch's result
shift.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<p>other</p><span>2|0</span>`);
// the fork's result, which equals the real value
shift.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<p>other</p><span>2|0</span>`);
commit.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<p>other</p><span>2|1</span>`);
// nothing left over
shift.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<p>other</p><span>2|1</span>`);
}
});

@ -0,0 +1,28 @@
<script>
import { fork } from 'svelte';
let a = $state(0);
let b = $state(0);
/** @type {ReturnType<typeof fork> | null} */
let f = null;
/** @type {Array<() => void>} */
const deferred = [];
/** @param {number} value */
function delay(value) {
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
</script>
<button onclick={() => (f = fork(() => { a = 1; b = 1; }))}>fork</button>
<button onclick={() => (a = 2)}>a = 2</button>
<button onclick={() => deferred.shift()?.()}>shift</button>
<button onclick={() => f?.commit()}>commit</button>
<svelte:boundary>
{#if (await delay(a)) === 1}<p>one</p>{:else}<p>other</p>{/if}
<span>{a}|{b}</span>
{#snippet pending()}{/snippet}
</svelte:boundary>

@ -0,0 +1,49 @@
import { tick } from 'svelte';
import { test } from '../../test';
const buttons =
'<button>fork</button><button>a = 2</button><button>shift</button><button>commit</button>';
// A fork re-runs an async expression (because the real world changed its input) and the result equals
// the real value. The fork must not keep (and commit) its outdated result from before
export default test({
mode: ['client'],
async test({ assert, target }) {
const [fork, a2, shift, commit] = target.querySelectorAll('button');
shift.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<p>0</p><span>0|0</span>`);
// the fork's result is 1
fork.click();
await tick();
shift.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<p>0</p><span>0|0</span>`);
// the real world overtakes the fork's `a`, the real batch and the fork both run the expression with 2
a2.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<p>0</p><span>0|0</span>`);
// the real batch's result
shift.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<p>2</p><span>2|0</span>`);
// the fork's result, which equals the real value
shift.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<p>2</p><span>2|0</span>`);
commit.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<p>2</p><span>2|1</span>`);
// nothing left over
shift.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<p>2</p><span>2|1</span>`);
}
});

@ -0,0 +1,28 @@
<script>
import { fork } from 'svelte';
let a = $state(0);
let b = $state(0);
/** @type {ReturnType<typeof fork> | null} */
let f = null;
/** @type {Array<() => void>} */
const deferred = [];
/** @param {number} value */
function delay(value) {
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
</script>
<button onclick={() => (f = fork(() => { a = 1; b = 1; }))}>fork</button>
<button onclick={() => (a = 2)}>a = 2</button>
<button onclick={() => deferred.shift()?.()}>shift</button>
<button onclick={() => f?.commit()}>commit</button>
<svelte:boundary>
<p>{await delay(a)}</p>
<span>{a}|{b}</span>
{#snippet pending()}{/snippet}
</svelte:boundary>

@ -0,0 +1,47 @@
import { tick } from 'svelte';
import { test } from '../../test';
// A fork whose only remaining values equal the real ones is obsolete. Here, the forks re-run the
// each block in their own view after `list` changed, writing the (unchanged) item values, which
// they hold. Once the real world has overtaken fork A's only change (`show = true`), these must not
// keep it alive: otherwise it would be committed as a real batch instead of being discarded, and
// its `{#await}` would resolve with a stale `b` (from fork B, committed later) and win over the
// real result.
export default test({
mode: ['client'],
async test({ assert, target }) {
await tick();
const [fork_a, fork_b, write_list, write_real, commit_a, commit_b, resolve_all] =
target.querySelectorAll('button');
const buttons = [...target.querySelectorAll('button')].map((b) => b.outerHTML).join('');
const settle = async () => {
for (let i = 0; i < 10; i++) {
await tick();
resolve_all.click();
}
await tick();
};
await settle();
assert.htmlEqual(target.innerHTML, `${buttons}<p>p0</p><s>0</s><s>1</s><q>aw0</q>`);
fork_a.click();
fork_b.click();
// same items, but a new array, so the forks re-run the each block
write_list.click();
await tick();
// overtakes everything fork A changed (and `b` in fork B)
write_real.click();
commit_a.click();
commit_b.click();
await settle();
assert.htmlEqual(
target.innerHTML,
`${buttons}<p>p1</p><section>s2</section><s>0</s><s>1</s><q>aw2</q>`
);
}
});

@ -0,0 +1,46 @@
<script>
import { fork } from 'svelte';
let a = $state(0);
let b = $state(0);
let show = $state(false);
let list = $state([0, 1]);
let fork_a;
let fork_b;
const queued = [];
let initial = true;
// values are available right away during the initial render, after that only once resolved
function delay(value) {
if (initial) return value;
return new Promise((resolve) => queued.push(() => resolve(value)));
}
$effect(() => {
initial = false;
});
</script>
<button onclick={() => (fork_a = fork(() => (show = true)))}>fork a</button>
<button onclick={() => (fork_b = fork(() => { b = 2; a = 1; }))}>fork b</button>
<button onclick={() => (list = [0, 1])}>list = [0, 1]</button>
<button onclick={() => { b = 2; show = true; }}>b = 2, show = true</button>
<button onclick={() => fork_a.commit()}>commit a</button>
<button onclick={() => fork_b.commit()}>commit b</button>
<button onclick={() => { for (const resolve of queued.splice(0)) resolve(); }}>resolve all</button>
<svelte:boundary>
<p>{await delay(`p${a}`)}</p>
{#if show}
<section>{await delay(`s${b}`)}</section>
{/if}
{#each list as x}
<s>{x}</s>
{/each}
{#await delay(`aw${b}`) then v}<q>{v}</q>{/await}
</svelte:boundary>
Loading…
Cancel
Save