move batch-related code into batch.js

pull/15844/head
Rich Harris 3 months ago
parent c83374c3ca
commit ec21b741c2

@ -238,7 +238,7 @@ function init_update_callbacks(context) {
return (l.u ??= { a: [], b: [], m: [] }); return (l.u ??= { a: [], b: [], m: [] });
} }
export { flushSync } from './internal/client/runtime.js'; export { flushSync } from './internal/client/reactivity/batch.js';
export { getContext, getAllContexts, hasContext, setContext } from './internal/client/context.js'; export { getContext, getAllContexts, hasContext, setContext } from './internal/client/context.js';
export { hydrate, mount, unmount } from './internal/client/render.js'; export { hydrate, mount, unmount } from './internal/client/render.js';
export { tick, untrack, settled } from './internal/client/runtime.js'; export { tick, untrack, settled } from './internal/client/runtime.js';

@ -3,7 +3,7 @@ import { DEV } from 'esm-env';
import { is_promise } from '../../../shared/utils.js'; import { is_promise } from '../../../shared/utils.js';
import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js'; import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js';
import { internal_set, mutable_source, source } from '../../reactivity/sources.js'; import { internal_set, mutable_source, source } from '../../reactivity/sources.js';
import { flushSync, set_active_effect, set_active_reaction } from '../../runtime.js'; import { set_active_effect, set_active_reaction } from '../../runtime.js';
import { import {
hydrate_next, hydrate_next,
hydrate_node, hydrate_node,
@ -22,6 +22,7 @@ import {
set_dev_current_component_function, set_dev_current_component_function,
set_dev_stack set_dev_stack
} from '../../context.js'; } from '../../context.js';
import { flushSync } from '../../reactivity/batch.js';
const PENDING = 0; const PENDING = 0;
const THEN = 1; const THEN = 1;

@ -98,7 +98,7 @@ export {
props_id, props_id,
with_script with_script
} from './dom/template.js'; } from './dom/template.js';
export { suspend } from './reactivity/batch.js'; export { flushSync as flush, suspend } from './reactivity/batch.js';
export { export {
async_derived, async_derived,
user_derived as derived, user_derived as derived,
@ -142,7 +142,6 @@ export {
get, get,
safe_get, safe_get,
invalidate_inner_signals, invalidate_inner_signals,
flushSync as flush,
tick, tick,
untrack, untrack,
exclude_from_object, exclude_from_object,

@ -1,17 +1,36 @@
/** @import { Derived, Effect, Source } from '#client' */ /** @import { Derived, Effect, Source } from '#client' */
import { CLEAN, DIRTY } from '#client/constants'; import {
import { deferred } from '../../shared/utils.js'; BLOCK_EFFECT,
BRANCH_EFFECT,
CLEAN,
DESTROYED,
DIRTY,
EFFECT,
EFFECT_ASYNC,
INERT,
RENDER_EFFECT,
ROOT_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 { get_pending_boundary } from '../dom/blocks/boundary.js';
import { import {
flush_queued_effects, active_effect,
flush_queued_root_effects, check_dirtiness,
process_effects, dev_effect_stack,
is_updating_effect,
queued_root_effects, queued_root_effects,
schedule_effect, set_is_updating_effect,
set_queued_root_effects, set_queued_root_effects,
set_signal_status, set_signal_status,
update_effect update_effect
} from '../runtime.js'; } 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';
/** @type {Set<Batch>} */ /** @type {Set<Batch>} */
const batches = new Set(); const batches = new Set();
@ -22,6 +41,9 @@ export let current_batch = null;
/** @type {Map<Derived, any> | null} */ /** @type {Map<Derived, any> | null} */
export let batch_deriveds = null; export let batch_deriveds = null;
/** @type {Effect | null} */
let last_scheduled_effect = null;
/** TODO handy for debugging, but we should probably eventually delete it */ /** TODO handy for debugging, but we should probably eventually delete it */
let uid = 1; let uid = 1;
@ -329,6 +351,242 @@ export class 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) {
flush_queued_root_effects();
result = fn();
}
while (true) {
flush_tasks();
if (queued_root_effects.length === 0) {
if (batch === current_batch) {
batch.flush();
}
// this would be reset in `flush_queued_root_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.length = 0;
}
return /** @type {T} */ (result);
}
flush_queued_root_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.length = 0;
}
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;
}
}
}
export function flush_queued_root_effects() {
var was_updating_effect = is_updating_effect;
var batch = /** @type {Batch} */ (current_batch);
try {
var flush_count = 0;
set_is_updating_effect(true);
while (queued_root_effects.length > 0) {
if (flush_count++ > 1000) {
infinite_loop_guard();
}
batch.process(queued_root_effects);
old_values.clear();
}
} finally {
set_is_updating_effect(was_updating_effect);
last_scheduled_effect = null;
if (DEV) {
dev_effect_stack.length = 0;
}
}
}
/**
* @param {Array<Effect>} effects
* @returns {void}
*/
export 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 (check_dirtiness(effect)) {
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;
}
}
}
}
}
}
/**
* @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 ((flags & (ROOT_EFFECT | BRANCH_EFFECT)) !== 0) {
if ((flags & CLEAN) === 0) return;
effect.f ^= CLEAN;
}
}
queued_root_effects.push(effect);
}
/**
*
* This function both runs render effects and collects user effects in topological order
* from the starting effect passed in. Effects will be collected when they match the filtered
* bitwise flag passed in only. The collected effects array will be populated with all the user
* effects to be flushed.
*
* @param {Batch} batch
* @param {Effect} root
*/
export function process_effects(batch, 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 || batch.skipped_effects.has(effect);
if (!skip && effect.fn !== null) {
if ((flags & EFFECT_ASYNC) !== 0) {
const boundary = effect.b;
if (check_dirtiness(effect)) {
var effects = boundary?.pending ? batch.boundary_async_effects : batch.async_effects;
effects.push(effect);
}
} else if ((flags & BLOCK_EFFECT) !== 0) {
if (check_dirtiness(effect)) {
update_effect(effect);
}
} else if (is_branch) {
effect.f ^= CLEAN;
} else if ((flags & RENDER_EFFECT) !== 0) {
// we need to branch here because in legacy mode we run render effects
// before running block effects
if (async_mode_flag) {
batch.render_effects.push(effect);
} else {
if (check_dirtiness(effect)) {
update_effect(effect);
}
}
} else if ((flags & EFFECT) !== 0) {
batch.effects.push(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;
}
}
}
export function suspend() { export function suspend() {
var boundary = get_pending_boundary(); var boundary = get_pending_boundary();
var batch = /** @type {Batch} */ (current_batch); var batch = /** @type {Batch} */ (current_batch);

@ -7,7 +7,6 @@ import {
get, get,
is_destroying_effect, is_destroying_effect,
remove_reactions, remove_reactions,
schedule_effect,
set_active_reaction, set_active_reaction,
set_is_destroying_effect, set_is_destroying_effect,
set_signal_status, set_signal_status,
@ -40,7 +39,7 @@ import { DEV } from 'esm-env';
import { define_property } from '../../shared/utils.js'; import { define_property } from '../../shared/utils.js';
import { get_next_sibling } from '../dom/operations.js'; import { get_next_sibling } from '../dom/operations.js';
import { component_context, dev_current_component_function, dev_stack } from '../context.js'; import { component_context, dev_current_component_function, dev_stack } from '../context.js';
import { Batch } from './batch.js'; import { Batch, schedule_effect } from './batch.js';
import { flatten } from './async.js'; import { flatten } from './async.js';
/** /**

@ -5,7 +5,6 @@ import {
active_effect, active_effect,
untracked_writes, untracked_writes,
get, get,
schedule_effect,
set_untracked_writes, set_untracked_writes,
set_signal_status, set_signal_status,
untrack, untrack,
@ -34,7 +33,7 @@ import * as e from '../errors.js';
import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js'; import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js';
import { get_stack, tag_proxy } from '../dev/tracing.js'; import { get_stack, tag_proxy } from '../dev/tracing.js';
import { component_context, is_runes } from '../context.js'; import { component_context, is_runes } from '../context.js';
import { Batch } from './batch.js'; import { Batch, schedule_effect } from './batch.js';
import { proxy } from '../proxy.js'; import { proxy } from '../proxy.js';
import { execute_derived } from './deriveds.js'; import { execute_derived } from './deriveds.js';

@ -1,21 +1,18 @@
/** @import { Derived, Effect, Reaction, Signal, Source, Value } from '#client' */ /** @import { Derived, Effect, Reaction, Signal, Source, Value } from '#client' */
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { define_property, get_descriptors, get_prototype_of, index_of } from '../shared/utils.js'; import { get_descriptors, get_prototype_of, index_of } from '../shared/utils.js';
import { import {
destroy_block_effect_children, destroy_block_effect_children,
destroy_effect_children, destroy_effect_children,
execute_effect_teardown, execute_effect_teardown
unlink_effect
} from './reactivity/effects.js'; } from './reactivity/effects.js';
import { import {
EFFECT,
DIRTY, DIRTY,
MAYBE_DIRTY, MAYBE_DIRTY,
CLEAN, CLEAN,
DERIVED, DERIVED,
UNOWNED, UNOWNED,
DESTROYED, DESTROYED,
INERT,
BRANCH_EFFECT, BRANCH_EFFECT,
STATE_SYMBOL, STATE_SYMBOL,
BLOCK_EFFECT, BLOCK_EFFECT,
@ -23,12 +20,9 @@ import {
DISCONNECTED, DISCONNECTED,
REACTION_IS_UPDATING, REACTION_IS_UPDATING,
EFFECT_IS_UPDATING, EFFECT_IS_UPDATING,
EFFECT_ASYNC,
RENDER_EFFECT,
STALE_REACTION, STALE_REACTION,
ERROR_VALUE ERROR_VALUE
} from './constants.js'; } from './constants.js';
import { flush_tasks } from './dom/task.js';
import { internal_set, old_values } from './reactivity/sources.js'; import { internal_set, old_values } from './reactivity/sources.js';
import { import {
destroy_derived_effects, destroy_derived_effects,
@ -37,7 +31,6 @@ import {
recent_async_deriveds, recent_async_deriveds,
update_derived update_derived
} from './reactivity/deriveds.js'; } from './reactivity/deriveds.js';
import * as e from './errors.js';
import { async_mode_flag, tracing_mode_flag } from '../flags/index.js'; import { async_mode_flag, tracing_mode_flag } from '../flags/index.js';
import { tracing_expressions, get_stack } from './dev/tracing.js'; import { tracing_expressions, get_stack } from './dev/tracing.js';
import { import {
@ -50,13 +43,15 @@ import {
set_dev_stack set_dev_stack
} from './context.js'; } from './context.js';
import * as w from './warnings.js'; import * as w from './warnings.js';
import { current_batch, Batch, batch_deriveds } from './reactivity/batch.js'; import { Batch, batch_deriveds, flushSync, schedule_effect } from './reactivity/batch.js';
import { handle_error, invoke_error_boundary } from './error-handling.js'; import { handle_error } from './error-handling.js';
/** @type {Effect | null} */ export let is_updating_effect = false;
let last_scheduled_effect = null;
let is_updating_effect = false; /** @param {boolean} value */
export function set_is_updating_effect(value) {
is_updating_effect = value;
}
export let is_destroying_effect = false; export let is_destroying_effect = false;
@ -65,8 +60,6 @@ export function set_is_destroying_effect(value) {
is_destroying_effect = value; is_destroying_effect = value;
} }
// Handle effect queues
/** @type {Effect[]} */ /** @type {Effect[]} */
export let queued_root_effects = []; export let queued_root_effects = [];
@ -76,8 +69,7 @@ export function set_queued_root_effects(v) {
} }
/** @type {Effect[]} Stack of effects, dev only */ /** @type {Effect[]} Stack of effects, dev only */
let dev_effect_stack = []; export let dev_effect_stack = [];
// Handle signal reactivity tree dependencies and reactions
/** @type {null | Reaction} */ /** @type {null | Reaction} */
export let active_reaction = null; export let active_reaction = null;
@ -522,242 +514,6 @@ export function update_effect(effect) {
} }
} }
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;
}
}
}
export function flush_queued_root_effects() {
var was_updating_effect = is_updating_effect;
var batch = /** @type {Batch} */ (current_batch);
try {
var flush_count = 0;
is_updating_effect = true;
while (queued_root_effects.length > 0) {
if (flush_count++ > 1000) {
infinite_loop_guard();
}
batch.process(queued_root_effects);
old_values.clear();
}
} finally {
is_updating_effect = was_updating_effect;
last_scheduled_effect = null;
if (DEV) {
dev_effect_stack = [];
}
}
}
/**
* @param {Array<Effect>} effects
* @returns {void}
*/
export 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 (check_dirtiness(effect)) {
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;
}
}
}
}
}
}
/**
* @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 ((flags & (ROOT_EFFECT | BRANCH_EFFECT)) !== 0) {
if ((flags & CLEAN) === 0) return;
effect.f ^= CLEAN;
}
}
queued_root_effects.push(effect);
}
/**
*
* This function both runs render effects and collects user effects in topological order
* from the starting effect passed in. Effects will be collected when they match the filtered
* bitwise flag passed in only. The collected effects array will be populated with all the user
* effects to be flushed.
*
* @param {Batch} batch
* @param {Effect} root
*/
export function process_effects(batch, 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 || batch.skipped_effects.has(effect);
if (!skip && effect.fn !== null) {
if ((flags & EFFECT_ASYNC) !== 0) {
const boundary = effect.b;
if (check_dirtiness(effect)) {
var effects = boundary?.pending ? batch.boundary_async_effects : batch.async_effects;
effects.push(effect);
}
} else if ((flags & BLOCK_EFFECT) !== 0) {
if (check_dirtiness(effect)) {
update_effect(effect);
}
} else if (is_branch) {
effect.f ^= CLEAN;
} else if ((flags & RENDER_EFFECT) !== 0) {
// we need to branch here because in legacy mode we run render effects
// before running block effects
if (async_mode_flag) {
batch.render_effects.push(effect);
} else {
if (check_dirtiness(effect)) {
update_effect(effect);
}
}
} else if ((flags & EFFECT) !== 0) {
batch.effects.push(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;
}
}
}
/**
* 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) {
flush_queued_root_effects();
result = fn();
}
while (true) {
flush_tasks();
if (queued_root_effects.length === 0) {
if (batch === current_batch) {
batch.flush();
}
// this would be reset in `flush_queued_root_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);
}
flush_queued_root_effects();
}
}
/** /**
* Returns a promise that resolves once any pending state changes have been applied. * Returns a promise that resolves once any pending state changes have been applied.
* @returns {Promise<void>} * @returns {Promise<void>}
@ -768,6 +524,7 @@ export async function tick() {
} }
await Promise.resolve(); await Promise.resolve();
// By calling flushSync we guarantee that any pending state changes are applied after one tick. // By calling flushSync we guarantee that any pending state changes are applied after one tick.
// TODO look into whether we can make flushing subsequent updates synchronously in the future. // TODO look into whether we can make flushing subsequent updates synchronously in the future.
flushSync(); flushSync();

@ -3,7 +3,8 @@ import { DIRTY, LEGACY_PROPS, MAYBE_DIRTY } from '../internal/client/constants.j
import { user_pre_effect } from '../internal/client/reactivity/effects.js'; import { user_pre_effect } from '../internal/client/reactivity/effects.js';
import { mutable_source, set } from '../internal/client/reactivity/sources.js'; import { mutable_source, set } from '../internal/client/reactivity/sources.js';
import { hydrate, mount, unmount } from '../internal/client/render.js'; import { hydrate, mount, unmount } from '../internal/client/render.js';
import { active_effect, flushSync, get, set_signal_status } from '../internal/client/runtime.js'; import { active_effect, get, set_signal_status } from '../internal/client/runtime.js';
import { flushSync } from '../internal/client/reactivity/batch.js';
import { lifecycle_outside_component } from '../internal/shared/errors.js'; import { lifecycle_outside_component } from '../internal/shared/errors.js';
import { define_property, is_array } from '../internal/shared/utils.js'; import { define_property, is_array } from '../internal/shared/utils.js';
import * as w from '../internal/client/warnings.js'; import * as w from '../internal/client/warnings.js';

@ -11,6 +11,7 @@ import {
} from 'svelte/store'; } from 'svelte/store';
import { source, set } from '../../src/internal/client/reactivity/sources'; import { source, set } from '../../src/internal/client/reactivity/sources';
import * as $ from '../../src/internal/client/runtime'; import * as $ from '../../src/internal/client/runtime';
import { flushSync } from '../../src/internal/client/reactivity/batch';
import { effect_root, render_effect } from 'svelte/internal/client'; import { effect_root, render_effect } from 'svelte/internal/client';
describe('writable', () => { describe('writable', () => {
@ -602,7 +603,7 @@ describe('toStore', () => {
assert.deepEqual(log, [0]); assert.deepEqual(log, [0]);
set(count, 1); set(count, 1);
$.flushSync(); flushSync();
assert.deepEqual(log, [0, 1]); assert.deepEqual(log, [0, 1]);
unsubscribe(); unsubscribe();
@ -625,7 +626,7 @@ describe('toStore', () => {
assert.deepEqual(log, [0]); assert.deepEqual(log, [0]);
set(count, 1); set(count, 1);
$.flushSync(); flushSync();
assert.deepEqual(log, [0, 1]); assert.deepEqual(log, [0, 1]);
store.set(2); store.set(2);
@ -654,11 +655,11 @@ describe('fromStore', () => {
assert.deepEqual(log, [0]); assert.deepEqual(log, [0]);
store.set(1); store.set(1);
$.flushSync(); flushSync();
assert.deepEqual(log, [0, 1]); assert.deepEqual(log, [0, 1]);
count.current = 2; count.current = 2;
$.flushSync(); flushSync();
assert.deepEqual(log, [0, 1, 2]); assert.deepEqual(log, [0, 1, 2]);
assert.equal(get(store), 2); assert.equal(get(store), 2);

Loading…
Cancel
Save