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