chore: refactor scheduling (#17805)

This simplifies the scheduling logic and will likely improve performance
in some cases. Previously, there was a global `queued_root_effects`
array, and we would cycle through the batch flushing logic as long as it
was non-empty. This was a very loosey-goosey approach that was
appropriate in the pre-async world, but has gradually become a source of
confusion.

Now, effects are scheduled within the context of a specific batch. The
lifecycle is more rigorous and debuggable. This opens the door to
explorations of alternative approaches, such as only scheduling effects
when we call `batch.flush()`, which _may_ be better than the eager
status quo.

The layout of the `Batch` class is extremely chaotic —
public/private/static fields/methods are all jumbled up together — and I
would like to get a grip of it. In the interests of minimising diff
noise that ought to be a follow-up rather than part of this PR.

### Before submitting the PR, please make sure you do the following

- [x] It's really useful if your PR references an issue where it is
discussed ahead of time. In many cases, features are absent for a
reason. For large changes, please create an RFC:
https://github.com/sveltejs/rfcs
- [x] Prefix your PR title with `feat:`, `fix:`, `chore:`, or `docs:`.
- [x] This message body should clearly illustrate what problems it
solves.
- [ ] Ideally, include a test that fails without this PR but passes with
it.
- [x] If this PR changes code within `packages/svelte/src`, add a
changeset (`npx changeset`).

### Tests and linting

- [x] Run the tests with `pnpm test` and lint the project with `pnpm
lint`

---------

Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
pull/17879/head
Rich Harris 4 months ago committed by GitHub
parent 2deebdea8f
commit 6fb7b4d265
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: simplify scheduling logic

@ -45,7 +45,14 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
var branches = new BranchManager(node);
block(() => {
var batch = /** @type {Batch} */ (current_batch);
// we null out `current_batch` because otherwise `save(...)` will incorrectly restore it —
// the batch will already have been committed by the time it resolves
batch.deactivate();
var input = get_input();
batch.activate();
var destroyed = false;
/** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */

@ -35,7 +35,7 @@ import { queue_micro_task } from '../task.js';
import * as e from '../../errors.js';
import * as w from '../../warnings.js';
import { DEV } from 'esm-env';
import { Batch, schedule_effect } from '../../reactivity/batch.js';
import { Batch, current_batch, schedule_effect } from '../../reactivity/batch.js';
import { internal_set, source } from '../../reactivity/sources.js';
import { tag } from '../../dev/tracing.js';
import { createSubscriber } from '../../../../reactivity/create-subscriber.js';
@ -218,6 +218,8 @@ export class Boundary {
this.is_pending = true;
this.#pending_effect = branch(() => pending(this.#anchor));
var batch = /** @type {Batch} */ (current_batch);
queue_micro_task(() => {
var fragment = (this.#offscreen_fragment = document.createDocumentFragment());
var anchor = create_text();
@ -236,12 +238,14 @@ export class Boundary {
this.#pending_effect = null;
});
this.#resolve();
this.#resolve(batch);
}
});
}
#render() {
var batch = /** @type {Batch} */ (current_batch);
try {
this.is_pending = this.has_pending_snippet();
this.#pending_count = 0;
@ -258,14 +262,17 @@ export class Boundary {
const pending = /** @type {(anchor: Node) => void} */ (this.#props.pending);
this.#pending_effect = branch(() => pending(this.#anchor));
} else {
this.#resolve();
this.#resolve(batch);
}
} catch (error) {
this.error(error);
}
}
#resolve() {
/**
* @param {Batch} batch
*/
#resolve(batch) {
this.is_pending = false;
// any effects that were previously deferred should be rescheduled —
@ -273,12 +280,12 @@ export class Boundary {
// same update that brought us here) the effects will be flushed
for (const e of this.#dirty_effects) {
set_signal_status(e, DIRTY);
schedule_effect(e);
batch.schedule(e);
}
for (const e of this.#maybe_dirty_effects) {
set_signal_status(e, MAYBE_DIRTY);
schedule_effect(e);
batch.schedule(e);
}
this.#dirty_effects.clear();
@ -335,11 +342,12 @@ export class Boundary {
* Updates the pending count associated with the currently visible pending snippet,
* if any, such that we can replace the snippet with content once work is done
* @param {1 | -1} d
* @param {Batch} batch
*/
#update_pending_count(d) {
#update_pending_count(d, batch) {
if (!this.has_pending_snippet()) {
if (this.parent) {
this.parent.#update_pending_count(d);
this.parent.#update_pending_count(d, batch);
}
// if there's no parent, we're in a scope with no pending snippet
@ -349,7 +357,7 @@ export class Boundary {
this.#pending_count += d;
if (this.#pending_count === 0) {
this.#resolve();
this.#resolve(batch);
if (this.#pending_effect) {
pause_effect(this.#pending_effect, () => {
@ -369,9 +377,10 @@ export class Boundary {
* and controls when the current `pending` snippet (if any) is removed.
* Do not call from inside the class
* @param {1 | -1} d
* @param {Batch} batch
*/
update_pending_count(d) {
this.#update_pending_count(d);
update_pending_count(d, batch) {
this.#update_pending_count(d, batch);
this.#local_pending_count += d;

@ -9,6 +9,7 @@ import { hydrating } from '../../hydration.js';
import { tick, untrack } from '../../../runtime.js';
import { is_runes } from '../../../context.js';
import { current_batch, previous_batch } from '../../../reactivity/batch.js';
import { async_mode_flag } from '../../../../flags/index.js';
/**
* @param {HTMLInputElement} input
@ -87,8 +88,9 @@ export function bind_value(input, get, set = get) {
var value = get();
if (input === document.activeElement) {
// we need both, because in non-async mode, render effects run before previous_batch is set
var batch = /** @type {Batch} */ (previous_batch ?? current_batch);
// In sync mode render effects are executed during tree traversal -> needs current_batch
// In async mode render effects are flushed once batch resolved, at which point current_batch is null -> needs previous_batch
var batch = /** @type {Batch} */ (async_mode_flag ? previous_batch : current_batch);
// Never rewrite the contents of a focused input. We can get here if, for example,
// an update is deferred because of async work depending on the input:

@ -4,6 +4,7 @@ import { is } from '../../../proxy.js';
import { is_array } from '../../../../shared/utils.js';
import * as w from '../../../warnings.js';
import { Batch, current_batch, previous_batch } from '../../../reactivity/batch.js';
import { async_mode_flag } from '../../../../flags/index.js';
/**
* Selects the correct option(s) (depending on whether this is a multiple select)
@ -115,8 +116,9 @@ export function bind_select_value(select, get, set = get) {
var value = get();
if (select === document.activeElement) {
// we need both, because in non-async mode, render effects run before previous_batch is set
var batch = /** @type {Batch} */ (previous_batch ?? current_batch);
// In sync mode render effects are executed during tree traversal -> needs current_batch
// In async mode render effects are flushed once batch resolved, at which point current_batch is null -> needs previous_batch
var batch = /** @type {Batch} */ (async_mode_flag ? previous_batch : current_batch);
// Don't update the <select> if it is focused. We can get here if, for example,
// an update is deferred because of async work depending on the select:

@ -74,11 +74,14 @@ export function flatten(blockers, sync, async, fn) {
return;
}
var decrement_pending = increment_pending();
// Full path: has async expressions
function run() {
Promise.all(async.map((expression) => async_derived(expression)))
.then((result) => finish([...sync.map(d), ...result]))
.catch((error) => invoke_error_boundary(error, parent));
.catch((error) => invoke_error_boundary(error, parent))
.finally(() => decrement_pending());
}
if (blocker_promise) {
@ -106,10 +109,10 @@ export function run_after_blockers(blockers, fn) {
* causes `b` to be registered as a dependency).
*/
export function capture() {
var previous_effect = active_effect;
var previous_effect = /** @type {Effect} */ (active_effect);
var previous_reaction = active_reaction;
var previous_component_context = component_context;
var previous_batch = current_batch;
var previous_batch = /** @type {Batch} */ (current_batch);
if (DEV) {
var previous_dev_stack = dev_stack;
@ -119,7 +122,13 @@ export function capture() {
set_active_effect(previous_effect);
set_active_reaction(previous_reaction);
set_component_context(previous_component_context);
if (activate_batch) previous_batch?.activate();
if (activate_batch && (previous_effect.f & DESTROYED) === 0) {
// TODO we only need optional chaining here because `{#await ...}` blocks
// are anomalous. Once we retire them we can get rid of it
previous_batch?.activate();
previous_batch?.apply();
}
if (DEV) {
set_from_async_derived(null);
@ -282,7 +291,7 @@ export function run(thunks) {
// wait one more tick, so that template effects are
// guaranteed to run before `$effect(...)`
.then(() => Promise.resolve())
.finally(decrement_pending);
.finally(() => decrement_pending());
return blockers;
}
@ -294,16 +303,19 @@ export function wait(blockers) {
return Promise.all(blockers.map((b) => b.promise));
}
/**
* @returns {(skip?: boolean) => void}
*/
export function increment_pending() {
var boundary = /** @type {Boundary} */ (/** @type {Effect} */ (active_effect).b);
var batch = /** @type {Batch} */ (current_batch);
var blocking = boundary.is_rendered();
boundary.update_pending_count(1);
boundary.update_pending_count(1, batch);
batch.increment(blocking);
return () => {
boundary.update_pending_count(-1);
batch.decrement(blocking);
return (skip = false) => {
boundary.update_pending_count(-1, batch);
batch.decrement(blocking, skip);
};
}

@ -46,8 +46,7 @@ const batches = new Set();
export let current_batch = null;
/**
* This is needed to avoid overwriting inputs in non-async mode
* TODO 6.0 remove this, as non-async mode will go away
* This is needed to avoid overwriting inputs
* @type {Batch | null}
*/
export let previous_batch = null;
@ -60,14 +59,11 @@ export let previous_batch = null;
*/
export let batch_values = null;
// TODO this should really be a property of `batch`
/** @type {Effect[]} */
let queued_root_effects = [];
/** @type {Effect | null} */
let last_scheduled_effect = null;
export let is_flushing_sync = false;
let is_processing = false;
/**
* During traversal, this is an array. Newly created effects are (if not immediately
@ -77,6 +73,18 @@ export let is_flushing_sync = false;
*/
export let collected_effects = null;
/**
* An array of effects that are marked during traversal as a result of a `set`
* (not `internal_set`) call. These will be added to the next batch and
* trigger another `batch.process()`
* @type {Effect[] | null}
* @deprecated when we get rid of legacy mode and stores, we can get rid of this
*/
export let legacy_updates = null;
var flush_count = 0;
var source_stacks = DEV ? new Set() : null;
let uid = 1;
export class Batch {
@ -127,6 +135,12 @@ export class Batch {
*/
#deferred = null;
/**
* The root effects that need to be flushed
* @type {Effect[]}
*/
#roots = [];
/**
* Deferred effects (which run after async work has completed) that are DIRTY
* @type {Set<Effect>}
@ -178,22 +192,23 @@ export class Batch {
for (var e of tracked.d) {
set_signal_status(e, DIRTY);
schedule_effect(e);
this.schedule(e);
}
for (e of tracked.m) {
set_signal_status(e, MAYBE_DIRTY);
schedule_effect(e);
this.schedule(e);
}
}
}
/**
*
* @param {Effect[]} root_effects
*/
process(root_effects) {
queued_root_effects = [];
#process() {
if (flush_count++ > 1000) {
infinite_loop_guard();
}
const roots = this.#roots;
this.#roots = [];
this.apply();
@ -203,16 +218,28 @@ export class Batch {
/** @type {Effect[]} */
var render_effects = [];
for (const root of root_effects) {
this.#traverse_effect_tree(root, effects, render_effects);
// Note: #traverse_effect_tree runs block effects eagerly, which can schedule effects,
// which means queued_root_effects now may be filled again.
/**
* @type {Effect[]}
* @deprecated when we get rid of legacy mode and stores, we can get rid of this
*/
var updates = (legacy_updates = []);
for (const root of roots) {
this.#traverse(root, effects, render_effects);
}
// any writes should take effect in a subsequent batch
current_batch = null;
// Helpful for debugging reactivity loss that has to do with branches being skipped:
// log_inconsistent_branches(root);
if (updates.length > 0) {
var batch = Batch.ensure();
for (const e of updates) {
batch.schedule(e);
}
}
collected_effects = null;
legacy_updates = null;
if (this.#is_deferred()) {
this.#defer_effects(render_effects);
@ -222,32 +249,39 @@ export class Batch {
reset_branch(e, t);
}
} else {
// If sources are written to, then work needs to happen in a separate batch, else prior sources would be mixed with
// newly updated sources, which could lead to infinite loops when effects run over and over again.
previous_batch = this;
current_batch = null;
// clear effects. Those that are still needed will be rescheduled through unskipping the skipped branches.
this.#dirty_effects.clear();
this.#maybe_dirty_effects.clear();
// append/remove branches
for (const fn of this.#commit_callbacks) fn(this);
this.#commit_callbacks.clear();
previous_batch = this;
flush_queued_effects(render_effects);
flush_queued_effects(effects);
previous_batch = null;
if (this.#pending === 0) {
this.#commit();
}
flush_queued_effects(render_effects);
flush_queued_effects(effects);
this.#deferred?.resolve();
}
// Clear effects. Those that are still needed will be rescheduled through unskipping the skipped branches.
this.#dirty_effects.clear();
this.#maybe_dirty_effects.clear();
var next_batch = /** @type {Batch | null} */ (/** @type {unknown} */ (current_batch));
previous_batch = null;
if (next_batch !== null) {
batches.add(next_batch);
this.#deferred?.resolve();
}
if (DEV) {
for (const source of this.current.keys()) {
/** @type {Set<Source>} */ (source_stacks).add(source);
}
}
batch_values = null;
next_batch.#process();
}
}
/**
@ -257,7 +291,7 @@ export class Batch {
* @param {Effect[]} effects
* @param {Effect[]} render_effects
*/
#traverse_effect_tree(root, effects, render_effects) {
#traverse(root, effects, render_effects) {
root.f ^= CLEAN;
var effect = root.first;
@ -339,32 +373,54 @@ export class Batch {
activate() {
current_batch = this;
this.apply();
}
deactivate() {
// If we're not the current batch, don't deactivate,
// else we could create zombie batches that are never flushed
if (current_batch !== this) return;
current_batch = null;
batch_values = null;
}
flush() {
if (queued_root_effects.length > 0) {
var source_stacks = DEV ? new Set() : null;
try {
is_processing = true;
current_batch = this;
flush_effects();
} else if (this.#pending === 0 && !this.is_fork) {
// append/remove branches
for (const fn of this.#commit_callbacks) fn(this);
this.#commit_callbacks.clear();
this.#commit();
this.#deferred?.resolve();
}
// we only reschedule previously-deferred effects if we expect
// to be able to run them after processing the batch
if (!this.#is_deferred()) {
for (const e of this.#dirty_effects) {
this.#maybe_dirty_effects.delete(e);
set_signal_status(e, DIRTY);
this.schedule(e);
}
this.deactivate();
for (const e of this.#maybe_dirty_effects) {
set_signal_status(e, MAYBE_DIRTY);
this.schedule(e);
}
}
this.#process();
} finally {
flush_count = 0;
last_scheduled_effect = null;
collected_effects = null;
legacy_updates = null;
is_processing = false;
current_batch = null;
batch_values = null;
old_values.clear();
if (DEV) {
for (const source of /** @type {Set<Source>} */ (source_stacks)) {
source.updated = null;
}
}
}
}
discard() {
@ -415,9 +471,7 @@ export class Batch {
// Re-run async/block effects that depend on distinct values changed in both batches
const others = [...batch.current.keys()].filter((s) => !this.current.has(s));
if (others.length > 0) {
// Avoid running queued root effects on the wrong branch
var prev_queued_root_effects = queued_root_effects;
queued_root_effects = [];
batch.activate();
/** @type {Set<Value>} */
const marked = new Set();
@ -427,20 +481,17 @@ export class Batch {
mark_effects(source, others, marked, checked);
}
if (queued_root_effects.length > 0) {
current_batch = batch;
if (batch.#roots.length > 0) {
batch.apply();
for (const root of queued_root_effects) {
batch.#traverse_effect_tree(root, [], []);
for (const root of batch.#roots) {
batch.#traverse(root, [], []);
}
// TODO do we need to do anything with the dummy effect arrays?
batch.deactivate();
}
queued_root_effects = prev_queued_root_effects;
batch.deactivate();
}
}
@ -462,46 +513,22 @@ export class Batch {
}
/**
*
* @param {boolean} blocking
* @param {boolean} skip - whether to skip updates (because this is triggered by a stale reaction)
*/
decrement(blocking) {
decrement(blocking, skip) {
this.#pending -= 1;
if (blocking) this.#blocking_pending -= 1;
if (this.#decrement_queued) return;
if (this.#decrement_queued || skip) return;
this.#decrement_queued = true;
queue_micro_task(() => {
this.#decrement_queued = false;
if (!this.#is_deferred()) {
// we only reschedule previously-deferred effects if we expect
// to be able to run them after processing the batch
this.revive();
} else if (queued_root_effects.length > 0) {
// if other effects are scheduled, process the batch _without_
// rescheduling the previously-deferred effects
this.flush();
}
this.flush();
});
}
revive() {
for (const e of this.#dirty_effects) {
this.#maybe_dirty_effects.delete(e);
set_signal_status(e, DIRTY);
schedule_effect(e);
}
for (const e of this.#maybe_dirty_effects) {
set_signal_status(e, MAYBE_DIRTY);
schedule_effect(e);
}
this.flush();
}
/** @param {(batch: Batch) => void} fn */
oncommit(fn) {
this.#commit_callbacks.add(fn);
@ -519,17 +546,20 @@ export class Batch {
static ensure() {
if (current_batch === null) {
const batch = (current_batch = new Batch());
batches.add(current_batch);
if (!is_flushing_sync) {
queue_micro_task(() => {
if (current_batch !== batch) {
// a flushSync happened in the meantime
return;
}
if (!is_processing) {
batches.add(current_batch);
if (!is_flushing_sync) {
queue_micro_task(() => {
if (current_batch !== batch) {
// a flushSync happened in the meantime
return;
}
batch.flush();
});
batch.flush();
});
}
}
}
@ -554,6 +584,63 @@ export class Batch {
}
}
}
/**
*
* @param {Effect} effect
*/
schedule(effect) {
last_scheduled_effect = effect;
// defer render effects inside a pending boundary
// TODO the `REACTION_RAN` check is only necessary because of legacy `$:` effects AFAICT — we can remove later
if (
effect.b?.is_pending &&
(effect.f & (EFFECT | RENDER_EFFECT | MANAGED_EFFECT)) !== 0 &&
(effect.f & REACTION_RAN) === 0
) {
effect.b.defer_effect(effect);
return;
}
var e = effect;
while (e.parent !== null) {
e = e.parent;
var flags = e.f;
// if the effect is being scheduled because a parent (each/await/etc) block
// updated an internal source, or because a branch is being unskipped,
// bail out or we'll cause a second flush
if (collected_effects !== null && e === active_effect) {
if (async_mode_flag) return;
// in sync mode, render effects run during traversal. in an extreme edge case
// — namely that we're setting a value inside a derived read during traversal —
// they can be made dirty after they have already been visited, in which
// case we shouldn't bail out. we also shouldn't bail out if we're
// updating a store inside a `$:`, since this might invalidate
// effects that were already visited
if (
(active_reaction === null || (active_reaction.f & DERIVED) === 0) &&
!legacy_is_updating_store
) {
return;
}
}
if ((flags & (ROOT_EFFECT | BRANCH_EFFECT)) !== 0) {
if ((flags & CLEAN) === 0) {
// branch is already dirty, bail
return;
}
e.f ^= CLEAN;
}
}
this.#roots.push(e);
}
}
/**
@ -571,8 +658,8 @@ export function flushSync(fn) {
var result;
if (fn) {
if (current_batch !== null) {
flush_effects();
if (current_batch !== null && !current_batch.is_fork) {
current_batch.flush();
}
result = fn();
@ -581,87 +668,42 @@ export function flushSync(fn) {
while (true) {
flush_tasks();
if (queued_root_effects.length === 0) {
current_batch?.flush();
// we need to check again, in case we just updated an `$effect.pending()`
if (queued_root_effects.length === 0) {
// this would be reset in `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;
return /** @type {T} */ (result);
}
if (current_batch === null) {
return /** @type {T} */ (result);
}
flush_effects();
current_batch.flush();
}
} finally {
is_flushing_sync = was_flushing_sync;
}
}
function flush_effects() {
var source_stacks = DEV ? new Set() : null;
try {
var flush_count = 0;
while (queued_root_effects.length > 0) {
var batch = Batch.ensure();
if (flush_count++ > 1000) {
if (DEV) {
var updates = new Map();
for (const source of batch.current.keys()) {
for (const [stack, update] of source.updated ?? []) {
var entry = updates.get(stack);
function infinite_loop_guard() {
if (DEV) {
var updates = new Map();
if (!entry) {
entry = { error: update.error, count: 0 };
updates.set(stack, entry);
}
for (const source of /** @type {Batch} */ (current_batch).current.keys()) {
for (const [stack, update] of source.updated ?? []) {
var entry = updates.get(stack);
entry.count += update.count;
}
}
for (const update of updates.values()) {
if (update.error) {
// eslint-disable-next-line no-console
console.error(update.error);
}
}
if (!entry) {
entry = { error: update.error, count: 0 };
updates.set(stack, entry);
}
infinite_loop_guard();
}
batch.process(queued_root_effects);
old_values.clear();
if (DEV) {
for (const source of batch.current.keys()) {
/** @type {Set<Source>} */ (source_stacks).add(source);
}
entry.count += update.count;
}
}
} finally {
queued_root_effects = [];
last_scheduled_effect = null;
collected_effects = null;
if (DEV) {
for (const source of /** @type {Set<Source>} */ (source_stacks)) {
source.updated = null;
for (const update of updates.values()) {
if (update.error) {
// eslint-disable-next-line no-console
console.error(update.error);
}
}
}
}
function infinite_loop_guard() {
try {
e.effect_update_depth_exceeded();
} catch (error) {
@ -831,60 +873,11 @@ function depends_on(reaction, sources, checked) {
}
/**
* @param {Effect} signal
* @param {Effect} effect
* @returns {void}
*/
export function schedule_effect(signal) {
var effect = (last_scheduled_effect = signal);
var boundary = effect.b;
// defer render effects inside a pending boundary
// TODO the `REACTION_RAN` check is only necessary because of legacy `$:` effects AFAICT — we can remove later
if (
boundary?.is_pending &&
(signal.f & (EFFECT | RENDER_EFFECT | MANAGED_EFFECT)) !== 0 &&
(signal.f & REACTION_RAN) === 0
) {
boundary.defer_effect(signal);
return;
}
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, or because a branch is being unskipped,
// bail out or we'll cause a second flush
if (collected_effects !== null && effect === active_effect) {
if (async_mode_flag) return;
// in sync mode, render effects run during traversal. in an extreme edge case
// — namely that we're setting a value inside a derived read during traversal —
// they can be made dirty after they have already been visited, in which
// case we shouldn't bail out. we also shouldn't bail out if we're
// updating a store inside a `$:`, since this might invalidate
// effects that were already visited
if (
(active_reaction === null || (active_reaction.f & DERIVED) === 0) &&
!legacy_is_updating_store
) {
return;
}
}
if ((flags & (ROOT_EFFECT | BRANCH_EFFECT)) !== 0) {
if ((flags & CLEAN) === 0) {
// branch is already dirty, bail
return;
}
effect.f ^= CLEAN;
}
}
queued_root_effects.push(effect);
export function schedule_effect(effect) {
/** @type {Batch} */ (current_batch).schedule(effect);
}
/** @type {Source<number>[]} */
@ -1061,7 +1054,7 @@ export function fork(fn) {
flush_eager_effects();
});
batch.revive();
batch.flush();
await settled;
},
discard: () => {

@ -10,7 +10,8 @@ import {
ASYNC,
WAS_MARKED,
DESTROYED,
CLEAN
CLEAN,
REACTION_RAN
} from '#client/constants';
import {
active_reaction,
@ -36,7 +37,6 @@ import {
import { eager_effects, internal_set, set_eager_effects, source } from './sources.js';
import { get_error } from '../../shared/dev.js';
import { async_mode_flag, tracing_mode_flag } from '../../flags/index.js';
import { Boundary } from '../dom/blocks/boundary.js';
import { component_context } from '../context.js';
import { UNINITIALIZED } from '../../../constants.js';
import { batch_values, current_batch } from './batch.js';
@ -125,6 +125,8 @@ export function async_derived(fn, label, location) {
async_effect(() => {
if (DEV) current_async_effect = active_effect;
var effect = /** @type {Effect} */ (active_effect);
/** @type {ReturnType<typeof deferred<V>>} */
var d = deferred();
promise = d.promise;
@ -143,7 +145,10 @@ export function async_derived(fn, label, location) {
var batch = /** @type {Batch} */ (current_batch);
if (should_suspend) {
// we only increment the batch's pending state for updates, not creation, otherwise
// we will decrement to zero before the work that depends on this promise (e.g. a
// template effect) has initialized, causing the batch to resolve prematurely
if (should_suspend && (effect.f & REACTION_RAN) !== 0) {
var decrement_pending = increment_pending();
if (/** @type {Boundary} */ (parent.b).is_rendered()) {
@ -166,17 +171,26 @@ export function async_derived(fn, label, location) {
* @param {unknown} error
*/
const handler = (value, error = undefined) => {
current_async_effect = null;
if (DEV) current_async_effect = null;
if (decrement_pending) {
// don't trigger an update if we're only here because
// the promise was superseded before it could resolve
var skip = error === STALE_REACTION;
decrement_pending(skip);
}
if (error === STALE_REACTION || (effect.f & DESTROYED) !== 0) {
return;
}
batch.activate();
if (error) {
if (error !== STALE_REACTION) {
signal.f |= ERROR_VALUE;
signal.f |= ERROR_VALUE;
// @ts-expect-error the error is the wrong type, but we don't care
internal_set(signal, error);
}
// @ts-expect-error the error is the wrong type, but we don't care
internal_set(signal, error);
} else {
if ((signal.f & ERROR_VALUE) !== 0) {
signal.f ^= ERROR_VALUE;
@ -203,10 +217,6 @@ export function async_derived(fn, label, location) {
}
}
if (decrement_pending) {
decrement_pending();
}
batch.deactivate();
};

@ -41,7 +41,7 @@ import { DEV } from 'esm-env';
import { define_property } from '../../shared/utils.js';
import { get_next_sibling } from '../dom/operations.js';
import { component_context, dev_current_component_function, dev_stack } from '../context.js';
import { Batch, collected_effects, schedule_effect } from './batch.js';
import { Batch, collected_effects } from './batch.js';
import { flatten, increment_pending } from './async.js';
import { without_reactive_context } from '../dom/elements/bindings/shared.js';
import { set_signal_status } from './status.js';
@ -129,7 +129,7 @@ function create_effect(type, fn) {
collected_effects.push(effect);
} else {
// schedule for later
schedule_effect(effect);
Batch.ensure().schedule(effect);
}
} else if (fn !== null) {
try {

@ -22,6 +22,7 @@ import { DESTROYED, LEGACY_PROPS, STATE_SYMBOL } from '#client/constants';
import { proxy } from '../proxy.js';
import { capture_store_binding } from './store.js';
import { legacy_mode_flag } from '../../flags/index.js';
import { effect, render_effect } from './effects.js';
/**
* @param {((value?: number) => number)} fn
@ -296,7 +297,7 @@ export function prop(props, key, flags, fallback) {
};
/** @type {((v: V) => void) | undefined} */
var setter;
let setter;
if (bindable) {
// Can be the case when someone does `mount(Component, props)` with `let props = $state({...})`
@ -308,6 +309,7 @@ export function prop(props, key, flags, fallback) {
(is_entry_props && key in props ? (v) => (props[key] = v) : undefined);
}
/** @type {V} */
var initial_value;
var is_store_sub = false;

@ -35,7 +35,13 @@ import { includes } from '../../shared/utils.js';
import { tag_proxy } from '../dev/tracing.js';
import { get_error } from '../../shared/dev.js';
import { component_context, is_runes } from '../context.js';
import { Batch, batch_values, eager_block_effects, schedule_effect } from './batch.js';
import {
Batch,
batch_values,
eager_block_effects,
schedule_effect,
legacy_updates
} from './batch.js';
import { proxy } from '../proxy.js';
import { execute_derived } from './deriveds.js';
import { set_signal_status, update_derived_status } from './status.js';
@ -162,16 +168,17 @@ export function set(source, value, should_proxy = false) {
tag_proxy(new_value, /** @type {string} */ (source.label));
}
return internal_set(source, new_value);
return internal_set(source, new_value, legacy_updates);
}
/**
* @template V
* @param {Source<V>} source
* @param {V} value
* @param {Effect[] | null} [updated_during_traversal]
* @returns {V}
*/
export function internal_set(source, value) {
export function internal_set(source, value, updated_during_traversal = null) {
if (!source.equals(value)) {
var old_value = source.v;
@ -231,7 +238,7 @@ export function internal_set(source, value) {
// For debugging, in case you want to know which reactions are being scheduled:
// log_reactions(source);
mark_reactions(source, DIRTY);
mark_reactions(source, DIRTY, updated_during_traversal);
// It's possible that the current reaction might not have up-to-date dependencies
// whilst it's actively running. So in the case of ensuring it registers the reaction
@ -317,9 +324,10 @@ export function increment(source) {
/**
* @param {Value} signal
* @param {number} status should be DIRTY or MAYBE_DIRTY
* @param {Effect[] | null} updated_during_traversal
* @returns {void}
*/
function mark_reactions(signal, status) {
function mark_reactions(signal, status, updated_during_traversal) {
var reactions = signal.reactions;
if (reactions === null) return;
@ -357,14 +365,20 @@ function mark_reactions(signal, status) {
reaction.f |= WAS_MARKED;
}
mark_reactions(derived, MAYBE_DIRTY);
mark_reactions(derived, MAYBE_DIRTY, updated_during_traversal);
}
} else if (not_dirty) {
var effect = /** @type {Effect} */ (reaction);
if ((flags & BLOCK_EFFECT) !== 0 && eager_block_effects !== null) {
eager_block_effects.add(/** @type {Effect} */ (reaction));
eager_block_effects.add(effect);
}
schedule_effect(/** @type {Effect} */ (reaction));
if (updated_during_traversal !== null) {
updated_during_traversal.push(effect);
} else {
schedule_effect(effect);
}
}
}
}

@ -1,4 +1,4 @@
import { flushSync, tick } from 'svelte';
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
@ -7,12 +7,7 @@ export default test({
`,
async test({ assert, target }) {
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
flushSync();
await tick();
assert.htmlEqual(target.innerHTML, '<p data-foo="bar">hello</p>');
}
});

Loading…
Cancel
Save