From 08abbd3c96a81b2e2a3cd620680c63f7a8e076f1 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 9 Oct 2026 21:45:45 +0200 Subject: [PATCH] fix edge case around equal values getting set --- .../src/internal/client/reactivity/batch.js | 49 ++++++++++++++---- .../internal/client/reactivity/deriveds.js | 15 ++++-- .../src/internal/client/reactivity/sources.js | 11 ++-- .../svelte/src/internal/client/types.d.ts | 2 +- .../_config.js | 39 +++++++++++++++ .../main.svelte | 34 +++++++++++++ .../async-equal-result-dropped/_config.js | 47 +++++++++++++++++ .../async-equal-result-dropped/main.svelte | 36 +++++++++++++ .../_config.js | 33 ++++++------ .../_config.js | 50 +++++++++++++++++++ .../main.svelte | 28 +++++++++++ .../_config.js | 49 ++++++++++++++++++ .../main.svelte | 28 +++++++++++ .../_config.js | 47 +++++++++++++++++ .../main.svelte | 46 +++++++++++++++++ 15 files changed, 478 insertions(+), 36 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-equal-result-dropped-later-batch/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-equal-result-dropped-later-batch/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-equal-result-dropped/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-equal-result-dropped/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-equal-result-dropped-block/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-equal-result-dropped-block/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-equal-result-dropped/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-equal-result-dropped/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-obsolete-equal-values/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-obsolete-equal-values/main.svelte diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index eca4a35c48..bccb078250 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -111,6 +111,18 @@ export function read_batch_local_value(reaction) { 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} */ let last_scheduled_effect = null; @@ -882,8 +894,9 @@ export class Batch { * @param {Value} source * @param {any} value * @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, // we don't need to capture the value if ( @@ -903,7 +916,7 @@ export class Batch { // 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. - 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 {any} value * @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)) { 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` 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. // This also happens when committing a later batch hides an earlier batch's pending writes. 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. // 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 * 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, - * 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 {boolean} is_derived * @param {any} value @@ -984,10 +1004,14 @@ export class Batch { const current = this.current.get(source); this.current.delete(source); - if ([...this.current.values()].every((value) => value.is_derived)) { - // The real world has overtaken every write of this fork, 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 + if ( + [...this.current].every( + ([written, content]) => content.is_derived || written.equals(content.v) + ) + ) { + // 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.discard(); } 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 // versions, because the real world may have run reactions since that would otherwise outrank them. 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); // 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); diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index c5b659221e..d2919a7e45 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -48,6 +48,7 @@ import { batch_values, current_batch, first_batch, + own_value, previous_batch, read_batch_local_value } from './batch.js'; @@ -243,9 +244,16 @@ export function async_derived(fn, label, location) { decrement_pending?.(); 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.apply(); if (error) { signal.f |= ERROR_VALUE; @@ -417,8 +425,9 @@ export function execute_derived(derived) { */ export function update_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) { // `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. @@ -440,8 +449,6 @@ export function update_derived(derived) { (previous_batch ?? current_batch)?.remove_dirty_reaction(derived); 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 diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 5fb761eeec..24d15550e8 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -38,7 +38,8 @@ import { schedule_effect, legacy_updates, current_batch, - first_batch + first_batch, + own_value } from './batch.js'; import { proxy } from '../proxy.js'; import { execute_derived } from './deriveds.js'; @@ -189,7 +190,8 @@ var count_deps = 0; * @returns {V} */ 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) { old_values.set(source, value); } 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) { flush_eager_effects(); } - } else if (batch_values?.has(source) && !source.equals(batch_values?.get(source))) { - current_batch?.capture(source, source.v); + } else if (batch_values !== null && current_batch?.linked && !current_batch.current.has(source)) { + // 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; diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index 2320e7b510..9608a0d2e9 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -73,7 +73,7 @@ export type ComponentContextLegacy = ComponentContext & { l: NonNullable; }; -export type Equals = (this: Value, value: unknown) => boolean; +export type Equals = (this: Pick, value: unknown) => boolean; export type TemplateNode = Text | Element | Comment; diff --git a/packages/svelte/tests/runtime-runes/samples/async-equal-result-dropped-later-batch/_config.js b/packages/svelte/tests/runtime-runes/samples/async-equal-result-dropped-later-batch/_config.js new file mode 100644 index 0000000000..e487dea160 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-equal-result-dropped-later-batch/_config.js @@ -0,0 +1,39 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +const buttons = + ''; + +// 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}

zero

0c0`); + + b1.click(); + await tick(); + b2c1.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}

zero

0c0`); + + // 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}

zero

0c0`); + + // the earlier batch's equal result completes it + resolve_oldest.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}

pos

1c0`); + + resolve_all.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}

pos

2c1`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-equal-result-dropped-later-batch/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-equal-result-dropped-later-batch/main.svelte new file mode 100644 index 0000000000..024cc72c98 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-equal-result-dropped-later-batch/main.svelte @@ -0,0 +1,34 @@ + + + + + + + + + +

{await f(b > 0 ? 'pos' : 'zero')}

+ {b} + {await f('c' + c)} + {#snippet pending()}{/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-equal-result-dropped/_config.js b/packages/svelte/tests/runtime-runes/samples/async-equal-result-dropped/_config.js new file mode 100644 index 0000000000..44218567ae --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-equal-result-dropped/_config.js @@ -0,0 +1,47 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +const buttons = + ''; + +// 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}k0k1`); + + // batch 1 requests k2/k3 + b2.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}k0k1`); + + // batch 2 requests k0/k1, which equal the values currently shown + b0.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}k0k1`); + + resolve_k3.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}k0k1`); + + // batch 2's first result equals the real value + resolve_k0.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}k0k1`); + + resolve_all.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}k0k1`); + + // nothing left over + resolve_all.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}k0k1`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-equal-result-dropped/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-equal-result-dropped/main.svelte new file mode 100644 index 0000000000..d3cfaadd9d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-equal-result-dropped/main.svelte @@ -0,0 +1,36 @@ + + + + + + + + + + {await delay('k' + (0 + b))}{await delay('k' + (1 + b))} + {#snippet pending()}{/snippet} + diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-each-item-adopted-variants/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-each-item-adopted-variants/_config.js index dafc2da84c..0939678a6e 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-fork-each-item-adopted-variants/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-each-item-adopted-variants/_config.js @@ -100,25 +100,24 @@ export default test({ await reset(); // 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 - // with the real world's new value no longer keeps the fork's outdated result) - if (finish === 'discard') { - fork_append_2.click(); - resolve_all.click(); - await tick(); - instance.log.length = 0; + fork_append_2.click(); + resolve_all.click(); + await tick(); + instance.log.length = 0; - append_5.click(); - await settle(); - assert.htmlEqual(output.innerHTML, html([0, 1, 5])); - // 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']); + append_5.click(); + await settle(); + assert.htmlEqual(output.innerHTML, html([0, 1, 5])); + // 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']); - discard.click(); - await settle(); - assert.htmlEqual(output.innerHTML, html([0, 1, 5])); - await reset(); - } + // the fork's revalidated result for the unkeyed item equals the real one, it must not + // commit its outdated result for it + if (finish === 'discard') discard.click(); + else commit.click(); + await settle(); + assert.htmlEqual(output.innerHTML, html([0, 1, 5])); + await reset(); } catch (e) { /** @type {Error} */ (e).message = `${finish}: ${/** @type {Error} */ (e).message}`; throw e; diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-equal-result-dropped-block/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-equal-result-dropped-block/_config.js new file mode 100644 index 0000000000..7d9a66b8a3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-equal-result-dropped-block/_config.js @@ -0,0 +1,50 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +const buttons = + ''; + +// 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}

other

0|0`); + + // the fork's result is 1 + fork.click(); + await tick(); + shift.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}

other

0|0`); + + // 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}

other

0|0`); + + // the real batch's result + shift.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}

other

2|0`); + + // the fork's result, which equals the real value + shift.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}

other

2|0`); + + commit.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}

other

2|1`); + + // nothing left over + shift.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}

other

2|1`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-equal-result-dropped-block/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-equal-result-dropped-block/main.svelte new file mode 100644 index 0000000000..d43ace7050 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-equal-result-dropped-block/main.svelte @@ -0,0 +1,28 @@ + + + + + + + + + {#if (await delay(a)) === 1}

one

{:else}

other

{/if} + {a}|{b} + {#snippet pending()}{/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-equal-result-dropped/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-equal-result-dropped/_config.js new file mode 100644 index 0000000000..c5a1979aed --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-equal-result-dropped/_config.js @@ -0,0 +1,49 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +const buttons = + ''; + +// 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}

0

0|0`); + + // the fork's result is 1 + fork.click(); + await tick(); + shift.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}

0

0|0`); + + // 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}

0

0|0`); + + // the real batch's result + shift.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}

2

2|0`); + + // the fork's result, which equals the real value + shift.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}

2

2|0`); + + commit.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}

2

2|1`); + + // nothing left over + shift.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}

2

2|1`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-equal-result-dropped/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-equal-result-dropped/main.svelte new file mode 100644 index 0000000000..38e3f5a43f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-equal-result-dropped/main.svelte @@ -0,0 +1,28 @@ + + + + + + + + +

{await delay(a)}

+ {a}|{b} + {#snippet pending()}{/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-obsolete-equal-values/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-obsolete-equal-values/_config.js new file mode 100644 index 0000000000..7fc6e5a71d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-obsolete-equal-values/_config.js @@ -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}

p0

01aw0`); + + 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}

p1

s2
01aw2` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-obsolete-equal-values/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-obsolete-equal-values/main.svelte new file mode 100644 index 0000000000..df285c7c97 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-obsolete-equal-values/main.svelte @@ -0,0 +1,46 @@ + + + + + + + + + + + +

{await delay(`p${a}`)}

+ + {#if show} +
{await delay(`s${b}`)}
+ {/if} + + {#each list as x} + {x} + {/each} + + {#await delay(`aw${b}`) then v}{v}{/await} +