diff --git a/.changeset/big-webs-sing.md b/.changeset/big-webs-sing.md new file mode 100644 index 0000000000..946a41d881 --- /dev/null +++ b/.changeset/big-webs-sing.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: leave stale promises to wait for a later resolution, instead of rejecting diff --git a/.changeset/cruel-boxes-serve.md b/.changeset/cruel-boxes-serve.md new file mode 100644 index 0000000000..592cec4d01 --- /dev/null +++ b/.changeset/cruel-boxes-serve.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: reapply context after transforming error during SSR diff --git a/.changeset/public-mammals-float.md b/.changeset/public-mammals-float.md new file mode 100644 index 0000000000..d890c9e070 --- /dev/null +++ b/.changeset/public-mammals-float.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: remove unnecessary `increment_pending` calls diff --git a/.changeset/stupid-baboons-fall.md b/.changeset/stupid-baboons-fall.md new file mode 100644 index 0000000000..66895ad015 --- /dev/null +++ b/.changeset/stupid-baboons-fall.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: abort running obsolete async branches diff --git a/packages/svelte/src/internal/client/dom/blocks/async.js b/packages/svelte/src/internal/client/dom/blocks/async.js index 43af3d8dd3..170529a6b9 100644 --- a/packages/svelte/src/internal/client/dom/blocks/async.js +++ b/packages/svelte/src/internal/client/dom/blocks/async.js @@ -1,5 +1,5 @@ /** @import { Blocker, TemplateNode, Value } from '#client' */ -import { flatten, increment_pending } from '../../reactivity/async.js'; +import { flatten } from '../../reactivity/async.js'; import { get } from '../../runtime.js'; import { hydrate_next, @@ -42,8 +42,6 @@ export function async(node, blockers = [], expressions = [], fn) { return; } - const decrement_pending = increment_pending(); - if (was_hydrating) { var previous_hydrate_node = hydrate_node; set_hydrate_node(end); @@ -64,8 +62,6 @@ export function async(node, blockers = [], expressions = [], fn) { if (was_hydrating) { set_hydrating(false); } - - decrement_pending(); } }); } diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index 2e55aa791a..d0e8b6860f 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -56,33 +56,38 @@ export function flatten(blockers, sync, async, fn) { /** @param {Value[]} values */ function finish(values) { + if ((parent.f & DESTROYED) !== 0) { + return; + } + restore(); try { fn(values); } catch (error) { - if ((parent.f & DESTROYED) === 0) { - invoke_error_boundary(error, parent); - } + invoke_error_boundary(error, parent); } unset_context(); } + var decrement_pending = increment_pending(); + // Fast path: blockers but no async expressions if (async.length === 0) { - /** @type {Promise} */ (blocker_promise).then(() => finish(sync.map(d))); + /** @type {Promise} */ (blocker_promise) + .then(() => finish(sync.map(d))) + .finally(decrement_pending); + return; } - var decrement_pending = increment_pending(); - // Full path: has async expressions function run() { Promise.all(async.map((expression) => async_derived(expression))) .then((result) => finish([...sync.map(d), ...result])) .catch((error) => invoke_error_boundary(error, parent)) - .finally(() => decrement_pending()); + .finally(decrement_pending); } if (blocker_promise) { @@ -328,7 +333,7 @@ export function run(thunks) { // wait one more tick, so that template effects are // guaranteed to run before `$effect(...)` .then(() => Promise.resolve()) - .finally(() => decrement_pending()); + .finally(decrement_pending); return blockers; } @@ -352,8 +357,8 @@ export function increment_pending() { boundary.update_pending_count(1, batch); batch.increment(blocking, effect); - return (skip = false) => { + return () => { boundary.update_pending_count(-1, batch); - batch.decrement(blocking, effect, skip); + batch.decrement(blocking, effect); }; } diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 4239cda04b..a106806721 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -92,6 +92,9 @@ let uid = 1; export class Batch { id = uid++; + /** True as soon as `#process()` was called */ + #started = false; + /** * The current values of any signals that are updated in this batch. * Tuple format: [value, is_derived] (note: is_derived is false for deriveds, too, if they were overridden via assignment) @@ -107,6 +110,13 @@ export class Batch { */ previous = new Map(); + /** + * Async effects which this batch doesn't take into account anymore when calculating blockers, + * as it has a value for it already. + * @type {Set} + */ + unblocked = new Set(); + /** * When the batch is committed (and the DOM is updated), we need to remove old branches * and append new ones by calling the functions added inside (if/each/key/etc) blocks @@ -127,10 +137,9 @@ export class Batch { #fork_commit_callbacks = new Set(); /** - * Async effects that are currently in flight - * @type {Map} + * The number of async effects that are currently in flight */ - #pending = new Map(); + #pending = 0; /** * Async effects that are currently in flight, _not_ inside a pending boundary @@ -198,6 +207,8 @@ export class Batch { #is_blocked() { for (const batch of this.#blockers) { for (const effect of batch.#blocking_pending.keys()) { + if (this.unblocked.has(effect)) continue; + var skipped = false; var e = effect; @@ -255,6 +266,8 @@ export class Batch { } #process() { + this.#started = true; + if (flush_count++ > 1000) { batches.delete(this); infinite_loop_guard(); @@ -322,7 +335,7 @@ export class Batch { reset_branch(e, t); } } else { - if (this.#pending.size === 0) { + if (this.#pending === 0) { batches.delete(this); } @@ -342,6 +355,8 @@ export class Batch { this.#deferred?.resolve(); } + var next_batch = /** @type {Batch | null} */ (/** @type {unknown} */ (current_batch)); + // Order matters here - we need to commit and THEN continue flushing new batches, not the other way around, // else we could start flushing a new batch and then, if it has pending work, rebase it right afterwards, which is wrong. // In sync mode flushSync can cause #commit to wrongfully think that there needs to be a rebase, so we only do it in async mode @@ -350,8 +365,6 @@ export class Batch { this.#commit(); } - var next_batch = /** @type {Batch | null} */ (/** @type {unknown} */ (current_batch)); - // Edge case: During traversal new branches might create effects that run immediately and set state, // causing an effect and therefore a root to be scheduled again. We need to traverse the current batch // once more in that case - most of the time this will just clean up dirty branches. @@ -537,6 +550,8 @@ export class Batch { sources.push(source); } + if (!batch.#started) continue; + // Re-run async/block effects that depend on distinct values changed in both batches var others = [...batch.current.keys()].filter((s) => !this.current.has(s)); @@ -630,8 +645,7 @@ export class Batch { * @param {Effect} effect */ increment(blocking, effect) { - let pending_count = this.#pending.get(effect) ?? 0; - this.#pending.set(effect, pending_count + 1); + this.#pending += 1; if (blocking) { let blocking_pending_count = this.#blocking_pending.get(effect) ?? 0; @@ -642,16 +656,9 @@ export class Batch { /** * @param {boolean} blocking * @param {Effect} effect - * @param {boolean} skip - whether to skip updates (because this is triggered by a stale reaction) */ - decrement(blocking, effect, skip) { - let pending_count = this.#pending.get(effect) ?? 0; - - if (pending_count === 1) { - this.#pending.delete(effect); - } else { - this.#pending.set(effect, pending_count - 1); - } + decrement(blocking, effect) { + this.#pending -= 1; if (blocking) { let blocking_pending_count = this.#blocking_pending.get(effect) ?? 0; @@ -663,12 +670,15 @@ export class Batch { } } - if (this.#decrement_queued || skip) return; + if (this.#decrement_queued) return; this.#decrement_queued = true; queue_micro_task(() => { this.#decrement_queued = false; - this.flush(); + + if (batches.has(this)) { + this.flush(); + } }); } @@ -722,7 +732,7 @@ export class Batch { if (!is_flushing_sync) { queue_micro_task(() => { - if (!batches.has(batch) || batch.#pending.size > 0) { + if (batch.#started) { // a flushSync happened in the meantime return; } diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index eb934d96ff..794da4f2b0 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -100,6 +100,8 @@ export function derived(fn) { return signal; } +const OBSOLETE = {}; + /** * @template V * @param {() => V | Promise} fn @@ -118,7 +120,7 @@ export function async_derived(fn, label, location) { var promise = /** @type {Promise} */ (/** @type {unknown} */ (undefined)); var signal = source(/** @type {V} */ (UNINITIALIZED)); - if (DEV) signal.label = label; + if (DEV) signal.label = label ?? fn.toString(); // only suspend in async deriveds created on initialisation var should_suspend = !active_reaction; @@ -141,7 +143,13 @@ export function async_derived(fn, label, location) { // If this code is changed at some point, make sure to still access the then property // of fn() to read any signals it might access, so that we track them as dependencies. // We call `unset_context` to undo any `save` calls that happen inside `fn()` - Promise.resolve(fn()).then(d.resolve, d.reject).finally(unset_context); + Promise.resolve(fn()) + .then(d.resolve, (e) => { + // if the promise was rejected by the user, via `getAbortSignal`, then + // wait for a subsequent resolution instead of flushing the batch + if (e !== STALE_REACTION) d.reject(e); + }) + .finally(unset_context); } catch (error) { d.reject(error); unset_context(); @@ -180,15 +188,13 @@ export function async_derived(fn, label, location) { } if (/** @type {Boundary} */ (parent.b).is_rendered()) { - deferreds.get(batch)?.reject(STALE_REACTION); - deferreds.delete(batch); // delete to ensure correct order in Map iteration below + deferreds.get(batch)?.reject(OBSOLETE); } else { // While the boundary is still showing pending, a new run supersedes all older in-flight runs // for this async expression. Cancel eagerly so resolution cannot commit stale values. for (const d of deferreds.values()) { - d.reject(STALE_REACTION); + d.reject(OBSOLETE); } - deferreds.clear(); } deferreds.set(batch, d); @@ -203,16 +209,10 @@ export function async_derived(fn, label, location) { reactivity_loss_tracker = null; } - if (decrement_pending) { - // don't trigger an update if we're only here because - // the promise was superseded before it could resolve - var skip = error === STALE_REACTION; - decrement_pending(skip); - } + decrement_pending?.(); + deferreds.delete(batch); - if (error === STALE_REACTION || (effect.f & DESTROYED) !== 0) { - return; - } + if (error === OBSOLETE) return; batch.activate(); @@ -230,16 +230,21 @@ export function async_derived(fn, label, location) { // All prior async derived runs are now stale for (const [b, d] of deferreds) { - deferreds.delete(b); - if (b === batch) break; - d.resolve(value); + if (b.id < batch.id) { + // Don't delete + resolve directly, instead only do that once + // the current batch commits. This way we avoid tearing when + // `b` is rendering through the early resolve while `batch` is + // still pending. + batch.unblocked.add(effect); + batch.oncommit(() => d.resolve(value)); + } } if (DEV && location !== undefined) { recent_async_deriveds.add(signal); setTimeout(() => { - if (recent_async_deriveds.has(signal)) { + if (recent_async_deriveds.has(signal) && (effect.f & DESTROYED) === 0) { w.await_waterfall(/** @type {string} */ (signal.label), location); recent_async_deriveds.delete(signal); } @@ -255,7 +260,7 @@ export function async_derived(fn, label, location) { teardown(() => { for (const d of deferreds.values()) { - d.reject(STALE_REACTION); + d.reject(OBSOLETE); } }); diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 785e67a54b..1cc57b1605 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -43,7 +43,7 @@ import { define_property } from '../../shared/utils.js'; import { get_next_sibling, remove_node, append_child } from '../dom/operations.js'; import { component_context, dev_current_component_function, dev_stack } from '../context.js'; import { Batch, collected_effects, current_batch } from './batch.js'; -import { flatten, increment_pending } from './async.js'; +import { flatten } from './async.js'; import { without_reactive_context } from '../dom/elements/bindings/shared.js'; import { set_signal_status } from './status.js'; import { push_renderer, current_renderer } from '../custom-renderer/state.js'; @@ -398,16 +398,8 @@ export function template_effect(fn, sync = [], async = [], blockers = []) { * @param {Blocker[]} blockers */ export function deferred_template_effect(fn, sync = [], async = [], blockers = []) { - if (async.length > 0 || blockers.length > 0) { - var decrement_pending = increment_pending(); - } - flatten(blockers, sync, async, (values) => { create_effect(EFFECT, () => fn(...values.map(get))); - - if (decrement_pending) { - decrement_pending(); - } }); } diff --git a/packages/svelte/src/internal/server/renderer.js b/packages/svelte/src/internal/server/renderer.js index d2ab35a1f2..35aac64721 100644 --- a/packages/svelte/src/internal/server/renderer.js +++ b/packages/svelte/src/internal/server/renderer.js @@ -715,7 +715,12 @@ export class Renderer { const { context, failed, transformError } = item.#boundary; set_ssr_context(context); - let transformed = await transformError(error); + + let promise = transformError(error); + set_ssr_context(null); + + let transformed = await promise; + set_ssr_context(context); // Render the failed snippet instead of the partial children content const failed_renderer = new Renderer(item.global, item); diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts index 454ae2f766..6f30fb5d98 100644 --- a/packages/svelte/tests/runtime-legacy/shared.ts +++ b/packages/svelte/tests/runtime-legacy/shared.ts @@ -60,6 +60,8 @@ export interface RuntimeTest = Record void; after_test?: () => void; + /** If true, flushSync() will not be called before invoking test() */ + skip_initial_flushSync?: boolean; test?: (args: { variant: 'dom' | 'hydrate'; assert: Assert; @@ -505,7 +507,7 @@ async function run_test_variant( try { if (config.test) { - flushSync(); + if (!config.skip_initial_flushSync) flushSync(); if (variant === 'hydrate' && cwd.includes('async-')) { // wait for pending boundaries to render @@ -543,7 +545,7 @@ async function run_test_variant( } } finally { if (runes) { - unmount(instance); + await unmount(instance); } else { instance.$destroy(); } diff --git a/packages/svelte/tests/runtime-runes/samples/async-batch-order/_config.js b/packages/svelte/tests/runtime-runes/samples/async-batch-order/_config.js new file mode 100644 index 0000000000..53cceb9d54 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-batch-order/_config.js @@ -0,0 +1,30 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + const [increment, shift, middle] = target.querySelectorAll('button'); + const [div] = target.querySelectorAll('div'); + + increment.click(); + await tick(); + increment.click(); + await tick(); + increment.click(); + await tick(); + middle.click(); // resolve the second increment which will make the if block go away and the first batch discarded + await tick(); + assert.htmlEqual(div.innerHTML, '2 2'); + + shift.click(); + await tick(); + shift.click(); + await tick(); + shift.click(); + await tick(); + shift.click(); + await tick(); + assert.htmlEqual(div.innerHTML, '3 3'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-batch-order/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-batch-order/main.svelte new file mode 100644 index 0000000000..0289380d78 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-batch-order/main.svelte @@ -0,0 +1,21 @@ + + +
+ {a} {await delay(a)} + {#if a < 2} + {await delay(a)} + {/if} +
+ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-commit-preserve-new-batch/_config.js b/packages/svelte/tests/runtime-runes/samples/async-commit-preserve-new-batch/_config.js new file mode 100644 index 0000000000..922feed515 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-commit-preserve-new-batch/_config.js @@ -0,0 +1,19 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +// Tests that batch.#commit() does not null out a potentially new current_batch +export default test({ + skip_initial_flushSync: true, // test that the initial batch is flushed without an explicit flushSync() call + async test({ assert, target }) { + await tick(); + + const [button] = target.querySelectorAll('button'); + const [updates] = target.querySelectorAll('p'); + + assert.htmlEqual(updates.innerHTML, 'false'); + + button.click(); + await tick(); + assert.htmlEqual(updates.innerHTML, 'true'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-commit-preserve-new-batch/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-commit-preserve-new-batch/main.svelte new file mode 100644 index 0000000000..f7dae33b7e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-commit-preserve-new-batch/main.svelte @@ -0,0 +1,30 @@ + + + + +

{updated}

+ + + {await new Promise(() => {})} + + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-obsolete-branch-no-effect-runs/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-obsolete-branch-no-effect-runs/Child.svelte new file mode 100644 index 0000000000..1d9bdfada2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-obsolete-branch-no-effect-runs/Child.svelte @@ -0,0 +1,6 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/async-obsolete-branch-no-effect-runs/_config.js b/packages/svelte/tests/runtime-runes/samples/async-obsolete-branch-no-effect-runs/_config.js new file mode 100644 index 0000000000..83364706e5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-obsolete-branch-no-effect-runs/_config.js @@ -0,0 +1,27 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs, warnings }) { + const [increment, resolve] = target.querySelectorAll('button'); + + increment.click(); + await tick(); + assert.deepEqual(logs, []); + + resolve.click(); + await tick(); + assert.deepEqual(logs, []); + + resolve.click(); + await tick(); + assert.deepEqual(logs, []); + + resolve.click(); + await tick(); + assert.deepEqual(logs, [1, 2]); + + // no await waterfall / inert derived warnings + assert.deepEqual(warnings, []); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-obsolete-branch-no-effect-runs/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-obsolete-branch-no-effect-runs/main.svelte new file mode 100644 index 0000000000..fe01ae457e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-obsolete-branch-no-effect-runs/main.svelte @@ -0,0 +1,31 @@ + + + + + + + {#if count % 2 === 0} + {@const double = count * 2} +

true

+ {await push(count)} {double} + + {:else} +

false

+ + {/if} + + {#snippet pending()} +

loading...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-stale-derived-4/_config.js b/packages/svelte/tests/runtime-runes/samples/async-stale-derived-4/_config.js new file mode 100644 index 0000000000..e1555c0062 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-stale-derived-4/_config.js @@ -0,0 +1,28 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + + const [increment, hide, pop] = target.querySelectorAll('button'); + + increment.click(); + await tick(); + pop.click(); + await tick(); + hide.click(); // hides the if block, which cancels the pending async inside, which means the batch can complete + await tick(); + assert.htmlEqual( + target.innerHTML, + ` 1` + ); + + pop.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` 1` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-stale-derived-4/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-stale-derived-4/main.svelte new file mode 100644 index 0000000000..5ff3263d39 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-stale-derived-4/main.svelte @@ -0,0 +1,21 @@ + + + + + + + +{await push(count)} +{#if show} + {await push(count)} +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-stale-derived-5/_config.js b/packages/svelte/tests/runtime-runes/samples/async-stale-derived-5/_config.js new file mode 100644 index 0000000000..05d92e9df2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-stale-derived-5/_config.js @@ -0,0 +1,33 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + + const [increment, shift] = target.querySelectorAll('button'); + + increment.click(); + await tick(); + increment.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + `

1 = 1

fizz: true

buzz: true

` + ); + + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + `

1 = 1

fizz: true

buzz: true

` + ); + + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + `

3 = 3

fizz: true

buzz: false

` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-stale-derived-5/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-stale-derived-5/main.svelte new file mode 100644 index 0000000000..b2a40e65b3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-stale-derived-5/main.svelte @@ -0,0 +1,44 @@ + + + + + + +

{n} = {await push(n)}

+ +{#if true} +

fizz: {fizz}

+{/if} + +{#if true} +

buzz: {buzz}

+{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-stale-derived-6/_config.js b/packages/svelte/tests/runtime-runes/samples/async-stale-derived-6/_config.js new file mode 100644 index 0000000000..5b951d6c49 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-stale-derived-6/_config.js @@ -0,0 +1,34 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + const [button1, button2, pop, shift] = target.querySelectorAll('button'); + const [p] = target.querySelectorAll('p'); + + button1.click(); + await tick(); + button2.click(); + await tick(); + assert.htmlEqual(p.innerHTML, `0 + 0 = 0 | 0 0`); + + shift.click(); + await tick(); + assert.htmlEqual(p.innerHTML, `0 + 0 = 0 | 0 0`); + + pop.click(); + await tick(); + assert.htmlEqual(p.innerHTML, `0 + 0 = 0 | 0 0`); + + pop.click(); + await tick(); + assert.htmlEqual(p.innerHTML, `1 + 0 = 1 | 1 0`); + + shift.click(); + await tick(); + pop.click(); + await tick(); + assert.htmlEqual(p.innerHTML, `1 + 2 = 3 | 1 1`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-stale-derived-6/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-stale-derived-6/main.svelte new file mode 100644 index 0000000000..4515e7a488 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-stale-derived-6/main.svelte @@ -0,0 +1,21 @@ + + + + + + + +

{a} + {b} = {await push(a + b)} | {await push(c, 2)} {await push(d, 2)}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-stale-derived-7/_config.js b/packages/svelte/tests/runtime-runes/samples/async-stale-derived-7/_config.js new file mode 100644 index 0000000000..a6f1833d67 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-stale-derived-7/_config.js @@ -0,0 +1,34 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + const [button1, button2, shift_1, pop_1, shift_2] = target.querySelectorAll('button'); + const [p] = target.querySelectorAll('p'); + + button1.click(); + await tick(); + button2.click(); + await tick(); + assert.htmlEqual(p.innerHTML, `0 + 0 = 0 | 0 0`); + + pop_1.click(); + await tick(); + shift_2.click(); + await tick(); + assert.htmlEqual(p.innerHTML, `0 + 0 = 0 | 0 0`); + + // Check that the first batch can still resolve before the second even if one of its async values + // is already superseeded (but the subsequent batch as a whole is still pending). + shift_1.click(); + await tick(); + assert.htmlEqual(p.innerHTML, `1 + 0 = 1 | 1 0`); + + shift_1.click(); + await tick(); + shift_2.click(); + await tick(); + assert.htmlEqual(p.innerHTML, `1 + 2 = 3 | 1 1`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-stale-derived-7/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-stale-derived-7/main.svelte new file mode 100644 index 0000000000..fec684c257 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-stale-derived-7/main.svelte @@ -0,0 +1,22 @@ + + + + + + + + +

{a} + {b} = {await push(a + b)} | {await push(c, 2)} {await push(d, 2)}