mirror of https://github.com/sveltejs/svelte
feat: allow `await` in components (#15844)
* tidy
* tidy
* yes it can, apparently
* tidy up
* unused
* complete merge
* WIP
* simplify
* debugging help
* WIP
* unused
* partial merge
* WIP
* fix
* add test
* rename
* fix
* unused
* oops
* chore: merge main into async branch (#16197)
* chore: merge main into async branch
* adjust test
* fix: make effects depend on state created inside them (#16198)
* make effects depend on state created inside them
* fix, add github action
* disable test in async mode
* make batch.#deferred private
* fix settled when awaits occur inside pending boundary
* tweak
* change behaviour of `tick()` to be requestAnimationFrame-based
* get rid of a bunch of Promise.resolve chains
* more
* more
* fix test
* disallow `flushSync()` inside effects
* regenerate
* handle errors in block expressions
* make validate_each_keys async-aware
* for unowned deriveds, throw errors lazily
* rename ASYNC_ERROR -> ERROR_VALUE, and avoid conflicts with other flags now that it's used with deriveds as well as sources
* invoke boundary directly
* local effect pending
* update test
* fix
* fix
* fix weird bug in tests
* delete old changeset that somehow got left over here
* Update .changeset/eleven-weeks-dance.md
* update error details
* unused
* simplify
* tweak
* tweak
* tweak
* tweak
* tidy up
* handle errors in async block expressions
* tweak
* groundwork for async attribute_effect
* dry out
* fix async directives
* tidy up
* initialize option values before initing select values
* simplify init_select
* simplify
* tweak
* tidy up
* tweak
* on second thoughts just simplify it here
* tidy
* handle awaits in `<slot>`
* unused
* tidy up
* tidy up
* dry out
* dry out
* Revert "dry out"
This reverts commit 25855163bf
.
* dry out
* dry out
* use let for block-scoped stuff
* dry out
* dry out
* tidy up
* only wrap awaits in `$.save` when necessary
* oops
* remove TODO comment (just checked)
* oops, leftover
* simplify
* unused
* remove logging
* tweak
* unused
* unused
* remove logging
* partial fix
* fix
* remove unused EFFECT_HAS_DERIVED
* Update packages/svelte/src/reactivity/create-subscriber.js
Co-authored-by: Elliott Johnson <sejohnson@torchcloudconsulting.com>
* Update packages/svelte/src/index-client.js
Co-authored-by: Elliott Johnson <sejohnson@torchcloudconsulting.com>
* Update packages/svelte/src/internal/client/runtime.js
Co-authored-by: Elliott Johnson <sejohnson@torchcloudconsulting.com>
* unused
* Update packages/svelte/src/internal/client/reactivity/sources.js
Co-authored-by: Elliott Johnson <sejohnson@torchcloudconsulting.com>
* Update packages/svelte/src/internal/client/reactivity/deriveds.js
Co-authored-by: Elliott Johnson <sejohnson@torchcloudconsulting.com>
* Update packages/svelte/src/internal/client/reactivity/deriveds.js
Co-authored-by: Elliott Johnson <sejohnson@torchcloudconsulting.com>
* prettier
* unused
* fix flags
* tweak
* tweak
* unused
* fix
* no idea what a 'boundary micro task' is or why it was deemed necessary but evidently it isn't
* remove queue_boundary_micro_task
* oops
* note
* tidy up
* remove TODO
* make method private
* simplify
* flesh out await_reactivity_loss warning
* tweak
* update test
* fix
* null out from_async_derived in more places
* tidy up test
* failing test
* unused
* fix test
* fix
* simplify. no idea what the async_mode_flag stuff is about, but it appears unnecessary
* add async_derived_orphan error
* regenerate
* flesh out await_outside_boundary message
* add some JSDoc
* only update `$effect.pending()` if someone is listening, since it causes a double flush and makes debugging harder
* tweak logic to make it clearer why and when we commit a batch
* add a couple of comments
* false -> 0
* add comment
* unused
* silence warning
* add effect_pending_outside_reaction error
* Update packages/svelte/src/compiler/types/index.d.ts
Co-authored-by: Elliott Johnson <sejohnson@torchcloudconsulting.com>
* suspend batch, not boundary
* rename from_async_derived -> current_async_derived
* tweak
* remove TODO - this method is only called when pending snippet exists
* use error boundary for test - vitest does some weird error swallowing afaict
* flush less often
* restore -> activate
* remove TODO
* move batch-related code into batch.js
* make flush_queued_root_effects a method of batch
* make process_effects a method of batch
* make stuff private
* unused
* regenerate
* update test
* more JSDoc
* add more JSDoc
* branch and block effects do not also need to be render effects
* tidy up
* simplify
* unused
* move code where it belongs
* remove, for now
* fix
* only apply error adjustments when error escapes boundaries
* remove EFFECT_IS_UPDATING
* is_dirty is a better name than check_dirtiness
* duplicates are rare and harmless
* apparently we no longer need the merging logic? we can simplify and fix stuff by removing it
* tidy
* don't commit stale batches
* add skipped failing test
* partial merge
* WIP
* WIP
* WIP
* tweak
* tidy up
* dont update derived status when time-travelling
* tidy up
* tidy up
* tag async deriveds
* tweak
* bail out of secondary flushes
* re-run blocks on subsequent flushes
* add test
* fix
* add tests, one failing
* fix
* flesh out await_waterfall message
* tidy up
* dry out
* unused
* tweak
* tidy up
* TODO
* tweak
* tidy up
* remove TODO
* unused export
* add optimisation back
* revert unneeded changes
* revert
* update some tests
* more
* more
* move some code
* rename
* WIP
* unset context synchronously
* remove unused argument
* Apply suggestions from code review
Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
* add comment
* add comment
* use queue_micro_task in createSubscriber
* Update packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js
Co-authored-by: Elliott Johnson <sejohnson@torchcloudconsulting.com>
* prettier
---------
Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
Co-authored-by: Simon Holthausen <simon.holthausen@vercel.com>
Co-authored-by: Elliott Johnson <sejohnson@torchcloudconsulting.com>
pull/16364/head
parent
82f648157e
commit
0672e48223
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'svelte': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
feat: support `await` in components when using the `experimental.async` compiler option
|
@ -0,0 +1,30 @@
|
|||||||
|
/** @import { AwaitExpression } from 'estree' */
|
||||||
|
/** @import { Context } from '../types' */
|
||||||
|
import * as e from '../../../errors.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {AwaitExpression} node
|
||||||
|
* @param {Context} context
|
||||||
|
*/
|
||||||
|
export function AwaitExpression(node, context) {
|
||||||
|
let suspend = context.state.ast_type === 'instance' && context.state.function_depth === 1;
|
||||||
|
|
||||||
|
if (context.state.expression) {
|
||||||
|
context.state.expression.has_await = true;
|
||||||
|
suspend = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// disallow top-level `await` or `await` in template expressions
|
||||||
|
// unless a) in runes mode and b) opted into `experimental.async`
|
||||||
|
if (suspend) {
|
||||||
|
if (!context.state.options.experimental.async) {
|
||||||
|
e.experimental_async(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.state.analysis.runes) {
|
||||||
|
e.legacy_await_invalid(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context.next();
|
||||||
|
}
|
@ -0,0 +1,126 @@
|
|||||||
|
/** @import { AwaitExpression, Expression, Property, SpreadElement } from 'estree' */
|
||||||
|
/** @import { Context } from '../types' */
|
||||||
|
import { dev, is_ignored } from '../../../../state.js';
|
||||||
|
import * as b from '../../../../utils/builders.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {AwaitExpression} node
|
||||||
|
* @param {Context} context
|
||||||
|
*/
|
||||||
|
export function AwaitExpression(node, context) {
|
||||||
|
const argument = /** @type {Expression} */ (context.visit(node.argument));
|
||||||
|
|
||||||
|
const tla = context.state.is_instance && context.state.scope.function_depth === 1;
|
||||||
|
|
||||||
|
// preserve context for
|
||||||
|
// a) top-level await and
|
||||||
|
// b) awaits that precede other expressions in template or `$derived(...)`
|
||||||
|
if (tla || (is_reactive_expression(context) && !is_last_evaluated_expression(context, node))) {
|
||||||
|
return b.call(b.await(b.call('$.save', argument)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// in dev, note which values are read inside a reactive expression,
|
||||||
|
// but don't track them
|
||||||
|
else if (dev && !is_ignored(node, 'await_reactivity_loss')) {
|
||||||
|
return b.call(b.await(b.call('$.track_reactivity_loss', argument)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return argument === node.argument ? node : { ...node, argument };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Context} context
|
||||||
|
*/
|
||||||
|
function is_reactive_expression(context) {
|
||||||
|
if (context.state.in_derived) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let i = context.path.length;
|
||||||
|
|
||||||
|
while (i--) {
|
||||||
|
const parent = context.path[i];
|
||||||
|
|
||||||
|
if (
|
||||||
|
parent.type === 'ArrowFunctionExpression' ||
|
||||||
|
parent.type === 'FunctionExpression' ||
|
||||||
|
parent.type === 'FunctionDeclaration'
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error we could probably use a neater/more robust mechanism
|
||||||
|
if (parent.metadata) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Context} context
|
||||||
|
* @param {Expression | SpreadElement | Property} node
|
||||||
|
*/
|
||||||
|
function is_last_evaluated_expression(context, node) {
|
||||||
|
let i = context.path.length;
|
||||||
|
|
||||||
|
while (i--) {
|
||||||
|
const parent = /** @type {Expression | Property | SpreadElement} */ (context.path[i]);
|
||||||
|
|
||||||
|
// @ts-expect-error we could probably use a neater/more robust mechanism
|
||||||
|
if (parent.metadata) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (parent.type) {
|
||||||
|
case 'ArrayExpression':
|
||||||
|
if (node !== parent.elements.at(-1)) return false;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'AssignmentExpression':
|
||||||
|
case 'BinaryExpression':
|
||||||
|
case 'LogicalExpression':
|
||||||
|
if (node === parent.left) return false;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'CallExpression':
|
||||||
|
case 'NewExpression':
|
||||||
|
if (node !== parent.arguments.at(-1)) return false;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'ConditionalExpression':
|
||||||
|
if (node === parent.test) return false;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'MemberExpression':
|
||||||
|
if (parent.computed && node === parent.object) return false;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'ObjectExpression':
|
||||||
|
if (node !== parent.properties.at(-1)) return false;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Property':
|
||||||
|
if (node === parent.key) return false;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'SequenceExpression':
|
||||||
|
if (node !== parent.expressions.at(-1)) return false;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'TaggedTemplateExpression':
|
||||||
|
if (node !== parent.quasi.expressions.at(-1)) return false;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'TemplateLiteral':
|
||||||
|
if (node !== parent.expressions.at(-1)) return false;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
node = parent;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
/** @import { AwaitExpression } from 'estree' */
|
||||||
|
/** @import { Context } from '../types.js' */
|
||||||
|
import * as b from '../../../../utils/builders.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {AwaitExpression} node
|
||||||
|
* @param {Context} context
|
||||||
|
*/
|
||||||
|
export function AwaitExpression(node, context) {
|
||||||
|
// if `await` is inside a function, or inside `<script module>`,
|
||||||
|
// allow it, otherwise error
|
||||||
|
if (
|
||||||
|
context.state.scope.function_depth === 0 ||
|
||||||
|
context.path.some(
|
||||||
|
(node) =>
|
||||||
|
node.type === 'ArrowFunctionExpression' ||
|
||||||
|
node.type === 'FunctionDeclaration' ||
|
||||||
|
node.type === 'FunctionExpression'
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return context.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.call('$.await_outside_boundary');
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
/** @import { TemplateNode, Value } from '#client' */
|
||||||
|
import { flatten } from '../../reactivity/async.js';
|
||||||
|
import { get } from '../../runtime.js';
|
||||||
|
import { get_pending_boundary } from './boundary.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {TemplateNode} node
|
||||||
|
* @param {Array<() => Promise<any>>} expressions
|
||||||
|
* @param {(anchor: TemplateNode, ...deriveds: Value[]) => void} fn
|
||||||
|
*/
|
||||||
|
export function async(node, expressions, fn) {
|
||||||
|
var boundary = get_pending_boundary();
|
||||||
|
|
||||||
|
boundary.update_pending_count(1);
|
||||||
|
|
||||||
|
flatten([], expressions, (values) => {
|
||||||
|
try {
|
||||||
|
// get values eagerly to avoid creating blocks if they reject
|
||||||
|
for (const d of values) get(d);
|
||||||
|
|
||||||
|
fn(node, ...values);
|
||||||
|
} finally {
|
||||||
|
boundary.update_pending_count(-1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,127 @@
|
|||||||
|
/** @import { Effect, Value } from '#client' */
|
||||||
|
|
||||||
|
import { DESTROYED } from '#client/constants';
|
||||||
|
import { DEV } from 'esm-env';
|
||||||
|
import { component_context, is_runes, set_component_context } from '../context.js';
|
||||||
|
import { get_pending_boundary } from '../dom/blocks/boundary.js';
|
||||||
|
import { invoke_error_boundary } from '../error-handling.js';
|
||||||
|
import {
|
||||||
|
active_effect,
|
||||||
|
active_reaction,
|
||||||
|
set_active_effect,
|
||||||
|
set_active_reaction
|
||||||
|
} from '../runtime.js';
|
||||||
|
import { current_batch } from './batch.js';
|
||||||
|
import {
|
||||||
|
async_derived,
|
||||||
|
current_async_effect,
|
||||||
|
derived,
|
||||||
|
derived_safe_equal,
|
||||||
|
set_from_async_derived
|
||||||
|
} from './deriveds.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Array<() => any>} sync
|
||||||
|
* @param {Array<() => Promise<any>>} async
|
||||||
|
* @param {(values: Value[]) => any} fn
|
||||||
|
*/
|
||||||
|
export function flatten(sync, async, fn) {
|
||||||
|
const d = is_runes() ? derived : derived_safe_equal;
|
||||||
|
|
||||||
|
if (async.length === 0) {
|
||||||
|
fn(sync.map(d));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var batch = current_batch;
|
||||||
|
var parent = /** @type {Effect} */ (active_effect);
|
||||||
|
|
||||||
|
var restore = capture();
|
||||||
|
var boundary = get_pending_boundary();
|
||||||
|
|
||||||
|
Promise.all(async.map((expression) => async_derived(expression)))
|
||||||
|
.then((result) => {
|
||||||
|
batch?.activate();
|
||||||
|
|
||||||
|
restore();
|
||||||
|
|
||||||
|
try {
|
||||||
|
fn([...sync.map(d), ...result]);
|
||||||
|
} catch (error) {
|
||||||
|
// ignore errors in blocks that have already been destroyed
|
||||||
|
if ((parent.f & DESTROYED) === 0) {
|
||||||
|
invoke_error_boundary(error, parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
batch?.deactivate();
|
||||||
|
unset_context();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
boundary.error(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Captures the current effect context so that we can restore it after
|
||||||
|
* some asynchronous work has happened (so that e.g. `await a + b`
|
||||||
|
* causes `b` to be registered as a dependency).
|
||||||
|
*/
|
||||||
|
function capture() {
|
||||||
|
var previous_effect = active_effect;
|
||||||
|
var previous_reaction = active_reaction;
|
||||||
|
var previous_component_context = component_context;
|
||||||
|
|
||||||
|
return function restore() {
|
||||||
|
set_active_effect(previous_effect);
|
||||||
|
set_active_reaction(previous_reaction);
|
||||||
|
set_component_context(previous_component_context);
|
||||||
|
|
||||||
|
if (DEV) {
|
||||||
|
set_from_async_derived(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps an `await` expression in such a way that the effect context that was
|
||||||
|
* active before the expression evaluated can be reapplied afterwards —
|
||||||
|
* `await a + b` becomes `(await $.save(a))() + b`
|
||||||
|
* @template T
|
||||||
|
* @param {Promise<T>} promise
|
||||||
|
* @returns {Promise<() => T>}
|
||||||
|
*/
|
||||||
|
export async function save(promise) {
|
||||||
|
var restore = capture();
|
||||||
|
var value = await promise;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
restore();
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset `current_async_effect` after the `promise` resolves, so
|
||||||
|
* that we can emit `await_reactivity_loss` warnings
|
||||||
|
* @template T
|
||||||
|
* @param {Promise<T>} promise
|
||||||
|
* @returns {Promise<() => T>}
|
||||||
|
*/
|
||||||
|
export async function track_reactivity_loss(promise) {
|
||||||
|
var previous_async_effect = current_async_effect;
|
||||||
|
var value = await promise;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
set_from_async_derived(previous_async_effect);
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unset_context() {
|
||||||
|
set_active_effect(null);
|
||||||
|
set_active_reaction(null);
|
||||||
|
set_component_context(null);
|
||||||
|
if (DEV) set_from_async_derived(null);
|
||||||
|
}
|
@ -0,0 +1,603 @@
|
|||||||
|
/** @import { Derived, Effect, Source } from '#client' */
|
||||||
|
import {
|
||||||
|
BLOCK_EFFECT,
|
||||||
|
BRANCH_EFFECT,
|
||||||
|
CLEAN,
|
||||||
|
DESTROYED,
|
||||||
|
DIRTY,
|
||||||
|
EFFECT,
|
||||||
|
ASYNC,
|
||||||
|
INERT,
|
||||||
|
RENDER_EFFECT,
|
||||||
|
ROOT_EFFECT,
|
||||||
|
USER_EFFECT
|
||||||
|
} from '#client/constants';
|
||||||
|
import { async_mode_flag } from '../../flags/index.js';
|
||||||
|
import { deferred, define_property } from '../../shared/utils.js';
|
||||||
|
import { get_pending_boundary } from '../dom/blocks/boundary.js';
|
||||||
|
import {
|
||||||
|
active_effect,
|
||||||
|
is_dirty,
|
||||||
|
is_updating_effect,
|
||||||
|
set_is_updating_effect,
|
||||||
|
set_signal_status,
|
||||||
|
update_effect,
|
||||||
|
write_version
|
||||||
|
} from '../runtime.js';
|
||||||
|
import * as e from '../errors.js';
|
||||||
|
import { flush_tasks } from '../dom/task.js';
|
||||||
|
import { DEV } from 'esm-env';
|
||||||
|
import { invoke_error_boundary } from '../error-handling.js';
|
||||||
|
import { old_values } from './sources.js';
|
||||||
|
import { unlink_effect } from './effects.js';
|
||||||
|
import { unset_context } from './async.js';
|
||||||
|
|
||||||
|
/** @type {Set<Batch>} */
|
||||||
|
const batches = new Set();
|
||||||
|
|
||||||
|
/** @type {Batch | null} */
|
||||||
|
export let current_batch = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When time travelling, we re-evaluate deriveds based on the temporary
|
||||||
|
* values of their dependencies rather than their actual values, and cache
|
||||||
|
* the results in this map rather than on the deriveds themselves
|
||||||
|
* @type {Map<Derived, any> | null}
|
||||||
|
*/
|
||||||
|
export let batch_deriveds = null;
|
||||||
|
|
||||||
|
/** @type {Effect[]} Stack of effects, dev only */
|
||||||
|
export let dev_effect_stack = [];
|
||||||
|
|
||||||
|
/** @type {Effect[]} */
|
||||||
|
let queued_root_effects = [];
|
||||||
|
|
||||||
|
/** @type {Effect | null} */
|
||||||
|
let last_scheduled_effect = null;
|
||||||
|
|
||||||
|
let is_flushing = false;
|
||||||
|
|
||||||
|
export class Batch {
|
||||||
|
/**
|
||||||
|
* The current values of any sources that are updated in this batch
|
||||||
|
* They keys of this map are identical to `this.#previous`
|
||||||
|
* @type {Map<Source, any>}
|
||||||
|
*/
|
||||||
|
#current = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The values of any sources that are updated in this batch _before_ those updates took place.
|
||||||
|
* They keys of this map are identical to `this.#current`
|
||||||
|
* @type {Map<Source, any>}
|
||||||
|
*/
|
||||||
|
#previous = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* @type {Set<() => void>}
|
||||||
|
*/
|
||||||
|
#callbacks = new Set();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of async effects that are currently in flight
|
||||||
|
*/
|
||||||
|
#pending = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A deferred that resolves when the batch is committed, used with `settled()`
|
||||||
|
* TODO replace with Promise.withResolvers once supported widely enough
|
||||||
|
* @type {{ promise: Promise<void>, resolve: (value?: any) => void, reject: (reason: unknown) => void } | null}
|
||||||
|
*/
|
||||||
|
#deferred = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if an async effect inside this batch resolved and
|
||||||
|
* its parent branch was already deleted
|
||||||
|
*/
|
||||||
|
#neutered = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Async effects (created inside `async_derived`) encountered during processing.
|
||||||
|
* These run after the rest of the batch has updated, since they should
|
||||||
|
* always have the latest values
|
||||||
|
* @type {Effect[]}
|
||||||
|
*/
|
||||||
|
#async_effects = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The same as `#async_effects`, but for effects inside a newly-created
|
||||||
|
* `<svelte:boundary>` — these do not prevent the batch from committing
|
||||||
|
* @type {Effect[]}
|
||||||
|
*/
|
||||||
|
#boundary_async_effects = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template effects and `$effect.pre` effects, which run when
|
||||||
|
* a batch is committed
|
||||||
|
* @type {Effect[]}
|
||||||
|
*/
|
||||||
|
#render_effects = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The same as `#render_effects`, but for `$effect` (which runs after)
|
||||||
|
* @type {Effect[]}
|
||||||
|
*/
|
||||||
|
#effects = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Block effects, which may need to re-run on subsequent flushes
|
||||||
|
* in order to update internal sources (e.g. each block items)
|
||||||
|
* @type {Effect[]}
|
||||||
|
*/
|
||||||
|
#block_effects = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A set of branches that still exist, but will be destroyed when this batch
|
||||||
|
* is committed — we skip over these during `process`
|
||||||
|
* @type {Set<Effect>}
|
||||||
|
*/
|
||||||
|
skipped_effects = new Set();
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Effect[]} root_effects
|
||||||
|
*/
|
||||||
|
#process(root_effects) {
|
||||||
|
queued_root_effects = [];
|
||||||
|
|
||||||
|
/** @type {Map<Source, { v: unknown, wv: number }> | null} */
|
||||||
|
var current_values = null;
|
||||||
|
|
||||||
|
// if there are multiple batches, we are 'time travelling' —
|
||||||
|
// we need to undo the changes belonging to any batch
|
||||||
|
// other than the current one
|
||||||
|
if (batches.size > 1) {
|
||||||
|
current_values = new Map();
|
||||||
|
batch_deriveds = new Map();
|
||||||
|
|
||||||
|
for (const [source, current] of this.#current) {
|
||||||
|
current_values.set(source, { v: source.v, wv: source.wv });
|
||||||
|
source.v = current;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const batch of batches) {
|
||||||
|
if (batch === this) continue;
|
||||||
|
|
||||||
|
for (const [source, previous] of batch.#previous) {
|
||||||
|
if (!current_values.has(source)) {
|
||||||
|
current_values.set(source, { v: source.v, wv: source.wv });
|
||||||
|
source.v = previous;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const root of root_effects) {
|
||||||
|
this.#traverse_effect_tree(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we didn't start any new async work, and no async work
|
||||||
|
// is outstanding from a previous flush, commit
|
||||||
|
if (this.#async_effects.length === 0 && this.#pending === 0) {
|
||||||
|
var render_effects = this.#render_effects;
|
||||||
|
var effects = this.#effects;
|
||||||
|
|
||||||
|
this.#render_effects = [];
|
||||||
|
this.#effects = [];
|
||||||
|
this.#block_effects = [];
|
||||||
|
|
||||||
|
this.#commit();
|
||||||
|
|
||||||
|
flush_queued_effects(render_effects);
|
||||||
|
flush_queued_effects(effects);
|
||||||
|
|
||||||
|
this.#deferred?.resolve();
|
||||||
|
} else {
|
||||||
|
// otherwise mark effects clean so they get scheduled on the next run
|
||||||
|
for (const e of this.#render_effects) set_signal_status(e, CLEAN);
|
||||||
|
for (const e of this.#effects) set_signal_status(e, CLEAN);
|
||||||
|
for (const e of this.#block_effects) set_signal_status(e, CLEAN);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current_values) {
|
||||||
|
for (const [source, { v, wv }] of current_values) {
|
||||||
|
// reset the source to the current value (unless
|
||||||
|
// it got a newer value as a result of effects running)
|
||||||
|
if (source.wv <= wv) {
|
||||||
|
source.v = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
batch_deriveds = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const effect of this.#async_effects) {
|
||||||
|
update_effect(effect);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const effect of this.#boundary_async_effects) {
|
||||||
|
update_effect(effect);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#async_effects = [];
|
||||||
|
this.#boundary_async_effects = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Traverse the effect tree, executing effects or stashing
|
||||||
|
* them for later execution as appropriate
|
||||||
|
* @param {Effect} root
|
||||||
|
*/
|
||||||
|
#traverse_effect_tree(root) {
|
||||||
|
root.f ^= CLEAN;
|
||||||
|
|
||||||
|
var effect = root.first;
|
||||||
|
|
||||||
|
while (effect !== null) {
|
||||||
|
var flags = effect.f;
|
||||||
|
var is_branch = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) !== 0;
|
||||||
|
var is_skippable_branch = is_branch && (flags & CLEAN) !== 0;
|
||||||
|
|
||||||
|
var skip = is_skippable_branch || (flags & INERT) !== 0 || this.skipped_effects.has(effect);
|
||||||
|
|
||||||
|
if (!skip && effect.fn !== null) {
|
||||||
|
if (is_branch) {
|
||||||
|
effect.f ^= CLEAN;
|
||||||
|
} else if ((flags & EFFECT) !== 0) {
|
||||||
|
this.#effects.push(effect);
|
||||||
|
} else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) {
|
||||||
|
this.#render_effects.push(effect);
|
||||||
|
} else if (is_dirty(effect)) {
|
||||||
|
if ((flags & ASYNC) !== 0) {
|
||||||
|
var effects = effect.b?.pending ? this.#boundary_async_effects : this.#async_effects;
|
||||||
|
effects.push(effect);
|
||||||
|
} else {
|
||||||
|
if ((effect.f & BLOCK_EFFECT) !== 0) this.#block_effects.push(effect);
|
||||||
|
update_effect(effect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var child = effect.first;
|
||||||
|
|
||||||
|
if (child !== null) {
|
||||||
|
effect = child;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var parent = effect.parent;
|
||||||
|
effect = effect.next;
|
||||||
|
|
||||||
|
while (effect === null && parent !== null) {
|
||||||
|
effect = parent.next;
|
||||||
|
parent = parent.parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Associate a change to a given source with the current
|
||||||
|
* batch, noting its previous and current values
|
||||||
|
* @param {Source} source
|
||||||
|
* @param {any} value
|
||||||
|
*/
|
||||||
|
capture(source, value) {
|
||||||
|
if (!this.#previous.has(source)) {
|
||||||
|
this.#previous.set(source, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#current.set(source, source.v);
|
||||||
|
}
|
||||||
|
|
||||||
|
activate() {
|
||||||
|
current_batch = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
deactivate() {
|
||||||
|
current_batch = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
neuter() {
|
||||||
|
this.#neutered = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
flush() {
|
||||||
|
if (queued_root_effects.length > 0) {
|
||||||
|
this.flush_effects();
|
||||||
|
} else {
|
||||||
|
this.#commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current_batch !== this) {
|
||||||
|
// this can happen if a `flushSync` occurred during `this.flush_effects()`,
|
||||||
|
// which is permitted in legacy mode despite being a terrible idea
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.#pending === 0) {
|
||||||
|
batches.delete(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
current_batch = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
flush_effects() {
|
||||||
|
var was_updating_effect = is_updating_effect;
|
||||||
|
is_flushing = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
var flush_count = 0;
|
||||||
|
set_is_updating_effect(true);
|
||||||
|
|
||||||
|
while (queued_root_effects.length > 0) {
|
||||||
|
if (flush_count++ > 1000) {
|
||||||
|
infinite_loop_guard();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#process(queued_root_effects);
|
||||||
|
old_values.clear();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
is_flushing = false;
|
||||||
|
set_is_updating_effect(was_updating_effect);
|
||||||
|
|
||||||
|
last_scheduled_effect = null;
|
||||||
|
if (DEV) {
|
||||||
|
dev_effect_stack = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append and remove branches to/from the DOM
|
||||||
|
*/
|
||||||
|
#commit() {
|
||||||
|
if (!this.#neutered) {
|
||||||
|
for (const fn of this.#callbacks) {
|
||||||
|
fn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#callbacks.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
increment() {
|
||||||
|
this.#pending += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
decrement() {
|
||||||
|
this.#pending -= 1;
|
||||||
|
|
||||||
|
if (this.#pending === 0) {
|
||||||
|
for (const e of this.#render_effects) {
|
||||||
|
set_signal_status(e, DIRTY);
|
||||||
|
schedule_effect(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const e of this.#effects) {
|
||||||
|
set_signal_status(e, DIRTY);
|
||||||
|
schedule_effect(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const e of this.#block_effects) {
|
||||||
|
set_signal_status(e, DIRTY);
|
||||||
|
schedule_effect(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#render_effects = [];
|
||||||
|
this.#effects = [];
|
||||||
|
|
||||||
|
this.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {() => void} fn */
|
||||||
|
add_callback(fn) {
|
||||||
|
this.#callbacks.add(fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
settled() {
|
||||||
|
return (this.#deferred ??= deferred()).promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ensure() {
|
||||||
|
if (current_batch === null) {
|
||||||
|
const batch = (current_batch = new Batch());
|
||||||
|
batches.add(current_batch);
|
||||||
|
|
||||||
|
queueMicrotask(() => {
|
||||||
|
if (current_batch !== batch) {
|
||||||
|
// a flushSync happened in the meantime
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
batch.flush();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return current_batch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronously flush any pending updates.
|
||||||
|
* Returns void if no callback is provided, otherwise returns the result of calling the callback.
|
||||||
|
* @template [T=void]
|
||||||
|
* @param {(() => T) | undefined} [fn]
|
||||||
|
* @returns {T}
|
||||||
|
*/
|
||||||
|
export function flushSync(fn) {
|
||||||
|
if (async_mode_flag && active_effect !== null) {
|
||||||
|
e.flush_sync_in_effect();
|
||||||
|
}
|
||||||
|
|
||||||
|
var result;
|
||||||
|
|
||||||
|
const batch = Batch.ensure();
|
||||||
|
|
||||||
|
if (fn) {
|
||||||
|
batch.flush_effects();
|
||||||
|
|
||||||
|
result = fn();
|
||||||
|
}
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
flush_tasks();
|
||||||
|
|
||||||
|
if (queued_root_effects.length === 0) {
|
||||||
|
if (batch === current_batch) {
|
||||||
|
batch.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
// this would be reset in `batch.flush_effects()` but since we are early returning here,
|
||||||
|
// we need to reset it here as well in case the first time there's 0 queued root effects
|
||||||
|
last_scheduled_effect = null;
|
||||||
|
|
||||||
|
if (DEV) {
|
||||||
|
dev_effect_stack = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return /** @type {T} */ (result);
|
||||||
|
}
|
||||||
|
|
||||||
|
batch.flush_effects();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function log_effect_stack() {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(
|
||||||
|
'Last ten effects were: ',
|
||||||
|
dev_effect_stack.slice(-10).map((d) => d.fn)
|
||||||
|
);
|
||||||
|
dev_effect_stack = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function infinite_loop_guard() {
|
||||||
|
try {
|
||||||
|
e.effect_update_depth_exceeded();
|
||||||
|
} catch (error) {
|
||||||
|
if (DEV) {
|
||||||
|
// stack is garbage, ignore. Instead add a console.error message.
|
||||||
|
define_property(error, 'stack', {
|
||||||
|
value: ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Try and handle the error so it can be caught at a boundary, that's
|
||||||
|
// if there's an effect available from when it was last scheduled
|
||||||
|
if (last_scheduled_effect !== null) {
|
||||||
|
if (DEV) {
|
||||||
|
try {
|
||||||
|
invoke_error_boundary(error, last_scheduled_effect);
|
||||||
|
} catch (e) {
|
||||||
|
// Only log the effect stack if the error is re-thrown
|
||||||
|
log_effect_stack();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
invoke_error_boundary(error, last_scheduled_effect);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (DEV) {
|
||||||
|
log_effect_stack();
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Array<Effect>} effects
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function flush_queued_effects(effects) {
|
||||||
|
var length = effects.length;
|
||||||
|
if (length === 0) return;
|
||||||
|
|
||||||
|
for (var i = 0; i < length; i++) {
|
||||||
|
var effect = effects[i];
|
||||||
|
|
||||||
|
if ((effect.f & (DESTROYED | INERT)) === 0) {
|
||||||
|
if (is_dirty(effect)) {
|
||||||
|
var wv = write_version;
|
||||||
|
|
||||||
|
update_effect(effect);
|
||||||
|
|
||||||
|
// Effects with no dependencies or teardown do not get added to the effect tree.
|
||||||
|
// Deferred effects (e.g. `$effect(...)`) _are_ added to the tree because we
|
||||||
|
// don't know if we need to keep them until they are executed. Doing the check
|
||||||
|
// here (rather than in `update_effect`) allows us to skip the work for
|
||||||
|
// immediate effects.
|
||||||
|
if (effect.deps === null && effect.first === null && effect.nodes_start === null) {
|
||||||
|
if (effect.teardown === null) {
|
||||||
|
// remove this effect from the graph
|
||||||
|
unlink_effect(effect);
|
||||||
|
} else {
|
||||||
|
// keep the effect in the graph, but free up some memory
|
||||||
|
effect.fn = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if state is written in a user effect, abort and re-schedule, lest we run
|
||||||
|
// effects that should be removed as a result of the state change
|
||||||
|
if (write_version > wv && (effect.f & USER_EFFECT) !== 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (; i < length; i += 1) {
|
||||||
|
schedule_effect(effects[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Effect} signal
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function schedule_effect(signal) {
|
||||||
|
var effect = (last_scheduled_effect = signal);
|
||||||
|
|
||||||
|
while (effect.parent !== null) {
|
||||||
|
effect = effect.parent;
|
||||||
|
var flags = effect.f;
|
||||||
|
|
||||||
|
// if the effect is being scheduled because a parent (each/await/etc) block
|
||||||
|
// updated an internal source, bail out or we'll cause a second flush
|
||||||
|
if (is_flushing && effect === active_effect && (flags & BLOCK_EFFECT) !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((flags & (ROOT_EFFECT | BRANCH_EFFECT)) !== 0) {
|
||||||
|
if ((flags & CLEAN) === 0) return;
|
||||||
|
effect.f ^= CLEAN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
queued_root_effects.push(effect);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function suspend() {
|
||||||
|
var boundary = get_pending_boundary();
|
||||||
|
var batch = /** @type {Batch} */ (current_batch);
|
||||||
|
var pending = boundary.pending;
|
||||||
|
|
||||||
|
boundary.update_pending_count(1);
|
||||||
|
if (!pending) batch.increment();
|
||||||
|
|
||||||
|
return function unsuspend() {
|
||||||
|
boundary.update_pending_count(-1);
|
||||||
|
if (!pending) batch.decrement();
|
||||||
|
|
||||||
|
unset_context();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forcibly remove all current batches, to prevent cross-talk between tests
|
||||||
|
*/
|
||||||
|
export function clear() {
|
||||||
|
batches.clear();
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
import { enable_async_mode_flag } from './index.js';
|
||||||
|
|
||||||
|
enable_async_mode_flag();
|
@ -0,0 +1,23 @@
|
|||||||
|
import { settled, tick } from 'svelte';
|
||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
async test({ assert, target, logs }) {
|
||||||
|
const [reset, resolve] = target.querySelectorAll('button');
|
||||||
|
|
||||||
|
reset.click();
|
||||||
|
await settled();
|
||||||
|
assert.deepEqual(logs, ['aborted']);
|
||||||
|
|
||||||
|
resolve.click();
|
||||||
|
await tick();
|
||||||
|
assert.htmlEqual(
|
||||||
|
target.innerHTML,
|
||||||
|
`
|
||||||
|
<button>reset</button>
|
||||||
|
<button>resolve</button>
|
||||||
|
<h1>hello</h1>
|
||||||
|
`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,29 @@
|
|||||||
|
<script>
|
||||||
|
import { getAbortSignal } from 'svelte';
|
||||||
|
|
||||||
|
let deferred = $state(Promise.withResolvers());
|
||||||
|
|
||||||
|
function load(deferred) {
|
||||||
|
const signal = getAbortSignal();
|
||||||
|
|
||||||
|
return new Promise((fulfil, reject) => {
|
||||||
|
signal.onabort = (e) => {
|
||||||
|
console.log('aborted');
|
||||||
|
reject(e.currentTarget.reason);
|
||||||
|
};
|
||||||
|
|
||||||
|
deferred.promise.then(fulfil, reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button onclick={() => deferred = Promise.withResolvers()}>reset</button>
|
||||||
|
<button onclick={() => deferred.resolve('hello')}>resolve</button>
|
||||||
|
|
||||||
|
<svelte:boundary>
|
||||||
|
<h1>{await load(deferred)}</h1>
|
||||||
|
|
||||||
|
{#snippet pending()}
|
||||||
|
<p>pending</p>
|
||||||
|
{/snippet}
|
||||||
|
</svelte:boundary>
|
@ -0,0 +1,18 @@
|
|||||||
|
import { flushSync, tick } from 'svelte';
|
||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
html: `
|
||||||
|
<p>pending</p>
|
||||||
|
`,
|
||||||
|
|
||||||
|
async test({ assert, target }) {
|
||||||
|
await Promise.resolve();
|
||||||
|
await Promise.resolve();
|
||||||
|
await Promise.resolve();
|
||||||
|
await Promise.resolve();
|
||||||
|
flushSync();
|
||||||
|
|
||||||
|
assert.htmlEqual(target.innerHTML, '<p data-foo="bar">hello</p>');
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,7 @@
|
|||||||
|
<svelte:boundary>
|
||||||
|
<p data-foo={await 'bar'}>hello</p>
|
||||||
|
|
||||||
|
{#snippet pending()}
|
||||||
|
<p>pending</p>
|
||||||
|
{/snippet}
|
||||||
|
</svelte:boundary>
|
@ -0,0 +1,29 @@
|
|||||||
|
import { tick } from 'svelte';
|
||||||
|
import { ok, test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
html: `
|
||||||
|
<button>cool</button>
|
||||||
|
<button>neat</button>
|
||||||
|
<button>reset</button>
|
||||||
|
<p>pending</p>
|
||||||
|
`,
|
||||||
|
|
||||||
|
async test({ assert, target }) {
|
||||||
|
const [cool, neat, reset] = target.querySelectorAll('button');
|
||||||
|
|
||||||
|
cool.click();
|
||||||
|
await tick();
|
||||||
|
|
||||||
|
const p = target.querySelector('p');
|
||||||
|
ok(p);
|
||||||
|
assert.htmlEqual(p.outerHTML, '<p class="cool">hello</p>');
|
||||||
|
|
||||||
|
reset.click();
|
||||||
|
assert.htmlEqual(p.outerHTML, '<p class="cool">hello</p>');
|
||||||
|
|
||||||
|
neat.click();
|
||||||
|
await tick();
|
||||||
|
assert.htmlEqual(p.outerHTML, '<p class="neat">hello</p>');
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,15 @@
|
|||||||
|
<script>
|
||||||
|
let deferred = $state(Promise.withResolvers());
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button onclick={() => deferred.resolve('cool')}>cool</button>
|
||||||
|
<button onclick={() => deferred.resolve('neat')}>neat</button>
|
||||||
|
<button onclick={() => deferred = Promise.withResolvers()}>reset</button>
|
||||||
|
|
||||||
|
<svelte:boundary>
|
||||||
|
<p class={await deferred.promise}>hello</p>
|
||||||
|
|
||||||
|
{#snippet pending()}
|
||||||
|
<p>pending</p>
|
||||||
|
{/snippet}
|
||||||
|
</svelte:boundary>
|
@ -0,0 +1,27 @@
|
|||||||
|
import { tick } from 'svelte';
|
||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
async test({ assert, target }) {
|
||||||
|
const [increment, shift] = target.querySelectorAll('button');
|
||||||
|
|
||||||
|
increment.click();
|
||||||
|
await tick();
|
||||||
|
|
||||||
|
shift.click();
|
||||||
|
await tick();
|
||||||
|
|
||||||
|
shift.click();
|
||||||
|
await tick();
|
||||||
|
|
||||||
|
assert.htmlEqual(
|
||||||
|
target.innerHTML,
|
||||||
|
`
|
||||||
|
<button>increment</button>
|
||||||
|
<button>shift</button>
|
||||||
|
<p>false</p>
|
||||||
|
<p>1</p>
|
||||||
|
`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,28 @@
|
|||||||
|
<script>
|
||||||
|
let count = $state(0);
|
||||||
|
|
||||||
|
let deferreds = [];
|
||||||
|
|
||||||
|
function push() {
|
||||||
|
const deferred = Promise.withResolvers();
|
||||||
|
deferreds.push(deferred);
|
||||||
|
return deferred.promise;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button onclick={() => count += 1}>increment</button>
|
||||||
|
<button onclick={() => deferreds.shift()?.resolve(count)}>shift</button>
|
||||||
|
|
||||||
|
<svelte:boundary>
|
||||||
|
{#if count % 2 === 0}
|
||||||
|
<p>true</p>
|
||||||
|
<p>{await push()}</p>
|
||||||
|
{:else}
|
||||||
|
<p>false</p>
|
||||||
|
<p>{await push()}</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#snippet pending()}
|
||||||
|
<p>loading...</p>
|
||||||
|
{/snippet}
|
||||||
|
</svelte:boundary>
|
@ -0,0 +1,10 @@
|
|||||||
|
import { tick } from 'svelte';
|
||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
async test({ assert, target }) {
|
||||||
|
assert.htmlEqual(target.innerHTML, 'loading');
|
||||||
|
await tick();
|
||||||
|
assert.htmlEqual(target.innerHTML, 'nope');
|
||||||
|
}
|
||||||
|
});
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue