Merge branch 'main' into svelte-custom-renderer

svelte-custom-renderer-single-type-argument
Paolo Ricciuti 4 months ago committed by GitHub
commit c75ebe8b31
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: leave stale promises to wait for a later resolution, instead of rejecting

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: reapply context after transforming error during SSR

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: remove unnecessary `increment_pending` calls

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: abort running obsolete async branches

@ -1,5 +1,5 @@
/** @import { Blocker, TemplateNode, Value } from '#client' */ /** @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 { get } from '../../runtime.js';
import { import {
hydrate_next, hydrate_next,
@ -42,8 +42,6 @@ export function async(node, blockers = [], expressions = [], fn) {
return; return;
} }
const decrement_pending = increment_pending();
if (was_hydrating) { if (was_hydrating) {
var previous_hydrate_node = hydrate_node; var previous_hydrate_node = hydrate_node;
set_hydrate_node(end); set_hydrate_node(end);
@ -64,8 +62,6 @@ export function async(node, blockers = [], expressions = [], fn) {
if (was_hydrating) { if (was_hydrating) {
set_hydrating(false); set_hydrating(false);
} }
decrement_pending();
} }
}); });
} }

@ -56,33 +56,38 @@ export function flatten(blockers, sync, async, fn) {
/** @param {Value[]} values */ /** @param {Value[]} values */
function finish(values) { function finish(values) {
if ((parent.f & DESTROYED) !== 0) {
return;
}
restore(); restore();
try { try {
fn(values); fn(values);
} catch (error) { } catch (error) {
if ((parent.f & DESTROYED) === 0) { invoke_error_boundary(error, parent);
invoke_error_boundary(error, parent);
}
} }
unset_context(); unset_context();
} }
var decrement_pending = increment_pending();
// Fast path: blockers but no async expressions // Fast path: blockers but no async expressions
if (async.length === 0) { if (async.length === 0) {
/** @type {Promise<any>} */ (blocker_promise).then(() => finish(sync.map(d))); /** @type {Promise<any>} */ (blocker_promise)
.then(() => finish(sync.map(d)))
.finally(decrement_pending);
return; return;
} }
var decrement_pending = increment_pending();
// Full path: has async expressions // Full path: has async expressions
function run() { function run() {
Promise.all(async.map((expression) => async_derived(expression))) Promise.all(async.map((expression) => async_derived(expression)))
.then((result) => finish([...sync.map(d), ...result])) .then((result) => finish([...sync.map(d), ...result]))
.catch((error) => invoke_error_boundary(error, parent)) .catch((error) => invoke_error_boundary(error, parent))
.finally(() => decrement_pending()); .finally(decrement_pending);
} }
if (blocker_promise) { if (blocker_promise) {
@ -328,7 +333,7 @@ export function run(thunks) {
// wait one more tick, so that template effects are // wait one more tick, so that template effects are
// guaranteed to run before `$effect(...)` // guaranteed to run before `$effect(...)`
.then(() => Promise.resolve()) .then(() => Promise.resolve())
.finally(() => decrement_pending()); .finally(decrement_pending);
return blockers; return blockers;
} }
@ -352,8 +357,8 @@ export function increment_pending() {
boundary.update_pending_count(1, batch); boundary.update_pending_count(1, batch);
batch.increment(blocking, effect); batch.increment(blocking, effect);
return (skip = false) => { return () => {
boundary.update_pending_count(-1, batch); boundary.update_pending_count(-1, batch);
batch.decrement(blocking, effect, skip); batch.decrement(blocking, effect);
}; };
} }

@ -92,6 +92,9 @@ let uid = 1;
export class Batch { export class Batch {
id = uid++; id = uid++;
/** True as soon as `#process()` was called */
#started = false;
/** /**
* The current values of any signals that are updated in this batch. * 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) * 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(); 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<Effect>}
*/
unblocked = new Set();
/** /**
* When the batch is committed (and the DOM is updated), we need to remove old branches * 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 * 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(); #fork_commit_callbacks = new Set();
/** /**
* Async effects that are currently in flight * The number of async effects that are currently in flight
* @type {Map<Effect, number>}
*/ */
#pending = new Map(); #pending = 0;
/** /**
* Async effects that are currently in flight, _not_ inside a pending boundary * Async effects that are currently in flight, _not_ inside a pending boundary
@ -198,6 +207,8 @@ export class Batch {
#is_blocked() { #is_blocked() {
for (const batch of this.#blockers) { for (const batch of this.#blockers) {
for (const effect of batch.#blocking_pending.keys()) { for (const effect of batch.#blocking_pending.keys()) {
if (this.unblocked.has(effect)) continue;
var skipped = false; var skipped = false;
var e = effect; var e = effect;
@ -255,6 +266,8 @@ export class Batch {
} }
#process() { #process() {
this.#started = true;
if (flush_count++ > 1000) { if (flush_count++ > 1000) {
batches.delete(this); batches.delete(this);
infinite_loop_guard(); infinite_loop_guard();
@ -322,7 +335,7 @@ export class Batch {
reset_branch(e, t); reset_branch(e, t);
} }
} else { } else {
if (this.#pending.size === 0) { if (this.#pending === 0) {
batches.delete(this); batches.delete(this);
} }
@ -342,6 +355,8 @@ export class Batch {
this.#deferred?.resolve(); 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, // 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. // 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 // 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(); 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, // 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 // 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. // 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); sources.push(source);
} }
if (!batch.#started) continue;
// Re-run async/block effects that depend on distinct values changed in both batches // 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)); var others = [...batch.current.keys()].filter((s) => !this.current.has(s));
@ -630,8 +645,7 @@ export class Batch {
* @param {Effect} effect * @param {Effect} effect
*/ */
increment(blocking, effect) { increment(blocking, effect) {
let pending_count = this.#pending.get(effect) ?? 0; this.#pending += 1;
this.#pending.set(effect, pending_count + 1);
if (blocking) { if (blocking) {
let blocking_pending_count = this.#blocking_pending.get(effect) ?? 0; let blocking_pending_count = this.#blocking_pending.get(effect) ?? 0;
@ -642,16 +656,9 @@ export class Batch {
/** /**
* @param {boolean} blocking * @param {boolean} blocking
* @param {Effect} effect * @param {Effect} effect
* @param {boolean} skip - whether to skip updates (because this is triggered by a stale reaction)
*/ */
decrement(blocking, effect, skip) { decrement(blocking, effect) {
let pending_count = this.#pending.get(effect) ?? 0; this.#pending -= 1;
if (pending_count === 1) {
this.#pending.delete(effect);
} else {
this.#pending.set(effect, pending_count - 1);
}
if (blocking) { if (blocking) {
let blocking_pending_count = this.#blocking_pending.get(effect) ?? 0; 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; this.#decrement_queued = true;
queue_micro_task(() => { queue_micro_task(() => {
this.#decrement_queued = false; this.#decrement_queued = false;
this.flush();
if (batches.has(this)) {
this.flush();
}
}); });
} }
@ -722,7 +732,7 @@ export class Batch {
if (!is_flushing_sync) { if (!is_flushing_sync) {
queue_micro_task(() => { queue_micro_task(() => {
if (!batches.has(batch) || batch.#pending.size > 0) { if (batch.#started) {
// a flushSync happened in the meantime // a flushSync happened in the meantime
return; return;
} }

@ -100,6 +100,8 @@ export function derived(fn) {
return signal; return signal;
} }
const OBSOLETE = {};
/** /**
* @template V * @template V
* @param {() => V | Promise<V>} fn * @param {() => V | Promise<V>} fn
@ -118,7 +120,7 @@ export function async_derived(fn, label, location) {
var promise = /** @type {Promise<V>} */ (/** @type {unknown} */ (undefined)); var promise = /** @type {Promise<V>} */ (/** @type {unknown} */ (undefined));
var signal = source(/** @type {V} */ (UNINITIALIZED)); 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 // only suspend in async deriveds created on initialisation
var should_suspend = !active_reaction; 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 // 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. // 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()` // 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) { } catch (error) {
d.reject(error); d.reject(error);
unset_context(); unset_context();
@ -180,15 +188,13 @@ export function async_derived(fn, label, location) {
} }
if (/** @type {Boundary} */ (parent.b).is_rendered()) { if (/** @type {Boundary} */ (parent.b).is_rendered()) {
deferreds.get(batch)?.reject(STALE_REACTION); deferreds.get(batch)?.reject(OBSOLETE);
deferreds.delete(batch); // delete to ensure correct order in Map iteration below
} else { } else {
// While the boundary is still showing pending, a new run supersedes all older in-flight runs // 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 this async expression. Cancel eagerly so resolution cannot commit stale values.
for (const d of deferreds.values()) { for (const d of deferreds.values()) {
d.reject(STALE_REACTION); d.reject(OBSOLETE);
} }
deferreds.clear();
} }
deferreds.set(batch, d); deferreds.set(batch, d);
@ -203,16 +209,10 @@ export function async_derived(fn, label, location) {
reactivity_loss_tracker = null; reactivity_loss_tracker = null;
} }
if (decrement_pending) { decrement_pending?.();
// don't trigger an update if we're only here because deferreds.delete(batch);
// the promise was superseded before it could resolve
var skip = error === STALE_REACTION;
decrement_pending(skip);
}
if (error === STALE_REACTION || (effect.f & DESTROYED) !== 0) { if (error === OBSOLETE) return;
return;
}
batch.activate(); batch.activate();
@ -230,16 +230,21 @@ export function async_derived(fn, label, location) {
// All prior async derived runs are now stale // All prior async derived runs are now stale
for (const [b, d] of deferreds) { for (const [b, d] of deferreds) {
deferreds.delete(b); if (b.id < batch.id) {
if (b === batch) break; // Don't delete + resolve directly, instead only do that once
d.resolve(value); // 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) { if (DEV && location !== undefined) {
recent_async_deriveds.add(signal); recent_async_deriveds.add(signal);
setTimeout(() => { 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); w.await_waterfall(/** @type {string} */ (signal.label), location);
recent_async_deriveds.delete(signal); recent_async_deriveds.delete(signal);
} }
@ -255,7 +260,7 @@ export function async_derived(fn, label, location) {
teardown(() => { teardown(() => {
for (const d of deferreds.values()) { for (const d of deferreds.values()) {
d.reject(STALE_REACTION); d.reject(OBSOLETE);
} }
}); });

@ -43,7 +43,7 @@ import { define_property } from '../../shared/utils.js';
import { get_next_sibling, remove_node, append_child } from '../dom/operations.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 { component_context, dev_current_component_function, dev_stack } from '../context.js';
import { Batch, collected_effects, current_batch } from './batch.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 { without_reactive_context } from '../dom/elements/bindings/shared.js';
import { set_signal_status } from './status.js'; import { set_signal_status } from './status.js';
import { push_renderer, current_renderer } from '../custom-renderer/state.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 * @param {Blocker[]} blockers
*/ */
export function deferred_template_effect(fn, sync = [], async = [], 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) => { flatten(blockers, sync, async, (values) => {
create_effect(EFFECT, () => fn(...values.map(get))); create_effect(EFFECT, () => fn(...values.map(get)));
if (decrement_pending) {
decrement_pending();
}
}); });
} }

@ -715,7 +715,12 @@ export class Renderer {
const { context, failed, transformError } = item.#boundary; const { context, failed, transformError } = item.#boundary;
set_ssr_context(context); 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 // Render the failed snippet instead of the partial children content
const failed_renderer = new Renderer(item.global, item); const failed_renderer = new Renderer(item.global, item);

@ -60,6 +60,8 @@ export interface RuntimeTest<Props extends Record<string, any> = Record<string,
id_prefix?: string; id_prefix?: string;
before_test?: () => void; before_test?: () => void;
after_test?: () => void; after_test?: () => void;
/** If true, flushSync() will not be called before invoking test() */
skip_initial_flushSync?: boolean;
test?: (args: { test?: (args: {
variant: 'dom' | 'hydrate'; variant: 'dom' | 'hydrate';
assert: Assert; assert: Assert;
@ -505,7 +507,7 @@ async function run_test_variant(
try { try {
if (config.test) { if (config.test) {
flushSync(); if (!config.skip_initial_flushSync) flushSync();
if (variant === 'hydrate' && cwd.includes('async-')) { if (variant === 'hydrate' && cwd.includes('async-')) {
// wait for pending boundaries to render // wait for pending boundaries to render
@ -543,7 +545,7 @@ async function run_test_variant(
} }
} finally { } finally {
if (runes) { if (runes) {
unmount(instance); await unmount(instance);
} else { } else {
instance.$destroy(); instance.$destroy();
} }

@ -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');
}
});

@ -0,0 +1,21 @@
<script>
let a = $state(0);
const deferred = [];
function delay(value) {
if (!value) return value;
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
</script>
<div>
{a} {await delay(a)}
{#if a < 2}
{await delay(a)}
{/if}
</div>
<button onclick={() => {a++;}}>a++</button>
<button onclick={() => deferred.shift()?.()}>shift</button>
<button onclick={() => deferred[2]()}>middle</button>

@ -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');
}
});

@ -0,0 +1,30 @@
<script>
let count = $state(-1);
let payload = $state(false);
let updated = $state(false);
$effect(() => {
if (payload) {
updated = true;
}
});
function update() {
count = 0;
queueMicrotask(() => {
payload = true;
});
}
</script>
<button onclick={update}>update</button>
<p>{updated}</p>
<svelte:boundary>
{await new Promise(() => {})}
{#snippet pending()}
<p>pending</p>
{/snippet}
</svelte:boundary>

@ -0,0 +1,6 @@
<script>
let { count } = $props();
let double = $derived(count * 2);
$effect.pre(() => console.log(count, double));
</script>

@ -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, []);
}
});

@ -0,0 +1,31 @@
<script>
import Child from "./Child.svelte";
let count = $state(0);
let deferreds = [];
function push(v) {
return new Promise((resolve, reject) => {
deferreds.push({ resolve: () => resolve(v), reject });
});
}
</script>
<button onclick={() => count += 1}>increment</button>
<button onclick={() => deferreds.shift()?.resolve()}>resolve</button>
<svelte:boundary>
{#if count % 2 === 0}
{@const double = count * 2}
<p>true</p>
{await push(count)} {double}
<Child count={await push(count)} />
{:else}
<p>false</p>
<Child count={await push(count)} />
{/if}
{#snippet pending()}
<p>loading...</p>
{/snippet}
</svelte:boundary>

@ -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,
`<button>increment</button> <button>hide</button> <button>pop</button> 1`
);
pop.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<button>increment</button> <button>hide</button> <button>pop</button> 1`
);
}
});

@ -0,0 +1,21 @@
<script>
let show = $state(true);
let count = $state(0);
const queue = [];
function push(value) {
if (!value) return value;
return new Promise(r => queue.push(() => r(value)));
}
</script>
<button onclick={() => count += 1}>increment</button>
<button onclick={() => show = false}>hide</button>
<!-- pop() so that the outer one resolves first, not the one inside the if block -->
<button onclick={() => queue.pop()?.()}>pop</button>
{await push(count)}
{#if show}
{await push(count)}
{/if}

@ -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,
`<button>3</button><button>shift</button><p>1 = 1</p><p>fizz: true</p><p>buzz: true</p>`
);
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<button>3</button><button>shift</button><p>1 = 1</p><p>fizz: true</p><p>buzz: true</p>`
);
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<button>3</button><button>shift</button><p>3 = 3</p><p>fizz: true</p><p>buzz: false</p>`
);
}
});

@ -0,0 +1,44 @@
<script>
import { getAbortSignal } from 'svelte';
const queue = [];
let n = $state(1);
let fizz = $state(true);
let buzz = $state(true);
function increment() {
n++;
fizz = n % 3 === 0;
buzz = n % 5 === 0;
}
function push(value) {
if (value === 1) return 1;
const d = Promise.withResolvers();
queue.push(() => d.resolve(value));
const signal = getAbortSignal();
signal.onabort = () => d.reject(signal.reason);
return d.promise;
}
</script>
<button onclick={increment}>
{$state.eager(n)}
</button>
<button onclick={() => queue.shift()?.()}>shift</button>
<p>{n} = {await push(n)}</p>
{#if true}
<p>fizz: {fizz}</p>
{/if}
{#if true}
<p>buzz: {buzz}</p>
{/if}

@ -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`);
}
});

@ -0,0 +1,21 @@
<script>
const queue1 = [];
const queue2 = [];
let a = $state(0);
let b = $state(0);
let c = $state(0);
let d = $state(0)
function push(value, where = 1) {
if (!value) return value;
return new Promise(r => (where === 1 ? queue1 : queue2).push(() => r(value)));
}
</script>
<button onclick={() => {a++;c++}}>a / c</button>
<button onclick={() => {b+=2;d++}}>b / d</button>
<button onclick={() => queue1.pop()?.()}>pop 1</button>
<button onclick={() => queue2.shift()?.()}>shift 2</button>
<p>{a} + {b} = {await push(a + b)} | {await push(c, 2)} {await push(d, 2)}</p>

@ -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`);
}
});

@ -0,0 +1,22 @@
<script>
const queue1 = [];
const queue2 = [];
let a = $state(0);
let b = $state(0);
let c = $state(0);
let d = $state(0)
function push(value, where = 1) {
if (!value) return value;
return new Promise(r => (where === 1 ? queue1 : queue2).push(() => r(value)));
}
</script>
<button onclick={() => {a++;c++}}>a / c</button>
<button onclick={() => {b+=2;d++}}>b / d</button>
<button onclick={() => queue1.shift()?.()}>shift 1</button>
<button onclick={() => queue1.pop()?.()}>pop 1</button>
<button onclick={() => queue2.shift()?.()}>shift 2</button>
<p>{a} + {b} = {await push(a + b)} | {await push(c, 2)} {await push(d, 2)}</p>
Loading…
Cancel
Save