diff --git a/packages/svelte/src/ambient.d.ts b/packages/svelte/src/ambient.d.ts index 4e98eb82eb..c285376212 100644 --- a/packages/svelte/src/ambient.d.ts +++ b/packages/svelte/src/ambient.d.ts @@ -255,6 +255,8 @@ declare namespace $effect { */ export function pre(fn: () => void | (() => void)): void; + export function yield(fn: () => void | (() => void)): void; + /** * The `$effect.tracking` rune is an advanced feature that tells you whether or not the code is running inside a tracking context, such as an effect or inside your template. * diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js index 6a301726b1..b166a3a139 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -95,6 +95,7 @@ export function CallExpression(node, context) { case '$effect': case '$effect.pre': + case '$effect.yield': if (parent.type !== 'ExpressionStatement') { e.effect_invalid_placement(node); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ExpressionStatement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ExpressionStatement.js index 0424e595be..efb60435d2 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ExpressionStatement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ExpressionStatement.js @@ -11,8 +11,13 @@ export function ExpressionStatement(node, context) { if (node.expression.type === 'CallExpression') { const rune = get_rune(node.expression, context.state.scope); - if (rune === '$effect' || rune === '$effect.pre') { - const callee = rune === '$effect' ? '$.user_effect' : '$.user_pre_effect'; + if (rune === '$effect' || rune === '$effect.pre' || rune === '$effect.yield') { + const callee = + rune === '$effect' + ? '$.user_effect' + : rune === '$effect.yield' + ? '$.user_yield_effect' + : '$.user_pre_effect'; const func = /** @type {Expression} */ (context.visit(node.expression.arguments[0])); const expr = b.call(callee, /** @type {Expression} */ (func)); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ExpressionStatement.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ExpressionStatement.js index 00d0dba5da..5e7275c346 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ExpressionStatement.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ExpressionStatement.js @@ -13,6 +13,7 @@ export function ExpressionStatement(node, context) { if ( rune === '$effect' || rune === '$effect.pre' || + rune === '$effect.yield' || rune === '$effect.root' || rune === '$inspect.trace' ) { diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index a4840ce4eb..fee9419893 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -5,21 +5,22 @@ export const BLOCK_EFFECT = 1 << 4; export const BRANCH_EFFECT = 1 << 5; export const ROOT_EFFECT = 1 << 6; export const BOUNDARY_EFFECT = 1 << 7; -export const UNOWNED = 1 << 8; -export const DISCONNECTED = 1 << 9; -export const CLEAN = 1 << 10; -export const DIRTY = 1 << 11; -export const MAYBE_DIRTY = 1 << 12; -export const INERT = 1 << 13; -export const DESTROYED = 1 << 14; -export const EFFECT_RAN = 1 << 15; +export const YIELD_EFFECT = 1 << 8; +export const UNOWNED = 1 << 9; +export const DISCONNECTED = 1 << 10; +export const CLEAN = 1 << 11; +export const DIRTY = 1 << 12; +export const MAYBE_DIRTY = 1 << 13; +export const INERT = 1 << 14; +export const DESTROYED = 1 << 15; +export const EFFECT_RAN = 1 << 16; /** 'Transparent' effects do not create a transition boundary */ -export const EFFECT_TRANSPARENT = 1 << 16; +export const EFFECT_TRANSPARENT = 1 << 17; /** Svelte 4 legacy mode props need to be handled with deriveds and be recognized elsewhere, hence the dedicated flag */ -export const LEGACY_DERIVED_PROP = 1 << 17; -export const INSPECT_EFFECT = 1 << 18; -export const HEAD_EFFECT = 1 << 19; -export const EFFECT_HAS_DERIVED = 1 << 20; +export const LEGACY_DERIVED_PROP = 1 << 18; +export const INSPECT_EFFECT = 1 << 19; +export const HEAD_EFFECT = 1 << 20; +export const EFFECT_HAS_DERIVED = 1 << 21; export const STATE_SYMBOL = Symbol('$state'); export const STATE_SYMBOL_METADATA = Symbol('$state metadata'); diff --git a/packages/svelte/src/internal/client/dom/task.js b/packages/svelte/src/internal/client/dom/task.js index acb5a5b117..e27bfbd285 100644 --- a/packages/svelte/src/internal/client/dom/task.js +++ b/packages/svelte/src/internal/client/dom/task.js @@ -1,18 +1,34 @@ import { run_all } from '../../shared/utils.js'; // Fallback for when requestIdleCallback is not available +// TODO: find a proper polyfill for Safari, this doesn't work export const request_idle_callback = typeof requestIdleCallback === 'undefined' ? (/** @type {() => void} */ cb) => setTimeout(cb, 1) : requestIdleCallback; +// Fallback for when scheduler.yield is not available +// TODO: find a proper polyfill for Safari, this doesn't work +export const scheduler_yield = + // @ts-ignore + typeof scheduler === 'undefined' + ? (/** @type {() => void} */ cb) => setTimeout(cb, 1) + : async (/** @type {() => void} */ fn) => { + // @ts-ignore + await scheduler.yield(); + fn(); + }; + let is_micro_task_queued = false; let is_idle_task_queued = false; +let is_yield_task_queued = false; /** @type {Array<() => void>} */ let current_queued_micro_tasks = []; /** @type {Array<() => void>} */ let current_queued_idle_tasks = []; +/** @type {Array<() => void>} */ +let current_queued_yield_tasks = []; function process_micro_tasks() { is_micro_task_queued = false; @@ -28,6 +44,13 @@ function process_idle_tasks() { run_all(tasks); } +function process_yield_tasks() { + is_yield_task_queued = false; + const tasks = current_queued_yield_tasks.slice(); + current_queued_yield_tasks = []; + run_all(tasks); +} + /** * @param {() => void} fn */ @@ -50,6 +73,17 @@ export function queue_idle_task(fn) { current_queued_idle_tasks.push(fn); } +/** + * @param {() => void} fn + */ +export function queue_yield_task(fn) { + if (!is_yield_task_queued) { + is_yield_task_queued = true; + scheduler_yield(process_yield_tasks); + } + current_queued_yield_tasks.push(fn); +} + /** * Synchronously run any queued tasks. */ diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index f22c33babc..8711248198 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -107,7 +107,8 @@ export { template_effect, effect, user_effect, - user_pre_effect + user_pre_effect, + user_yield_effect } from './reactivity/effects.js'; export { mutable_state, mutate, set, state } from './reactivity/sources.js'; export { diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index bf890627f7..73cad7e87f 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -35,7 +35,8 @@ import { INSPECT_EFFECT, HEAD_EFFECT, MAYBE_DIRTY, - EFFECT_HAS_DERIVED + EFFECT_HAS_DERIVED, + YIELD_EFFECT } from '../constants.js'; import { set } from './sources.js'; import * as e from '../errors.js'; @@ -45,7 +46,7 @@ import { get_next_sibling } from '../dom/operations.js'; import { destroy_derived } from './deriveds.js'; /** - * @param {'$effect' | '$effect.pre' | '$inspect'} rune + * @param {'$effect' | '$effect.pre' | '$effect.yield' | '$inspect'} rune */ export function validate_effect(rune) { if (active_effect === null && active_reaction === null) { @@ -212,8 +213,7 @@ export function user_effect(fn) { reaction: active_reaction }); } else { - var signal = effect(fn); - return signal; + return effect(fn); } } @@ -232,6 +232,22 @@ export function user_pre_effect(fn) { return render_effect(fn); } +/** + * Internal representation of `$effect.yield(...)` + * @param {() => void | (() => void)} fn + * @returns {Effect} + */ +export function user_yield_effect(fn) { + validate_effect('$effect.yield'); + if (DEV) { + define_property(fn, 'name', { + value: '$effect.yield' + }); + } + + return yield_effect(fn); +} + /** @param {() => void | (() => void)} fn */ export function inspect_effect(fn) { return create_effect(INSPECT_EFFECT, fn, true); @@ -281,6 +297,14 @@ export function effect(fn) { return create_effect(EFFECT, fn, false); } +/** + * @param {() => void | (() => void)} fn + * @returns {Effect} + */ +export function yield_effect(fn) { + return create_effect(YIELD_EFFECT, fn, false); +} + /** * Internal representation of `$: ..` * @param {() => any} deps diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 4aa3d17a15..a853c64ba7 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -25,9 +25,10 @@ import { ROOT_EFFECT, LEGACY_DERIVED_PROP, DISCONNECTED, - BOUNDARY_EFFECT + BOUNDARY_EFFECT, + YIELD_EFFECT } from './constants.js'; -import { flush_tasks } from './dom/task.js'; +import { flush_tasks, queue_yield_task } from './dom/task.js'; import { add_owner } from './dev/ownership.js'; import { internal_set, set, source } from './reactivity/sources.js'; import { destroy_derived, execute_derived, update_derived } from './reactivity/deriveds.js'; @@ -621,6 +622,37 @@ function flush_queued_root_effects(root_effects) { } } +/** + * @param {Effect} effect + * @returns {void} + */ +function flush_effect(effect) { + if ((effect.f & (DESTROYED | INERT)) === 0) { + try { + 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; + } + } + } + } catch (error) { + handle_error(error, effect, null, effect.ctx); + } + } +} + /** * @param {Array} effects * @returns {void} @@ -632,30 +664,7 @@ function flush_queued_effects(effects) { for (var i = 0; i < length; i++) { var effect = effects[i]; - if ((effect.f & (DESTROYED | INERT)) === 0) { - try { - 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; - } - } - } - } catch (error) { - handle_error(error, effect, null, effect.ctx); - } - } + flush_effect(effect); } } @@ -682,7 +691,11 @@ function process_deferred() { * @returns {void} */ export function schedule_effect(signal) { - if (scheduler_mode === FLUSH_MICROTASK) { + if ((signal.f & YIELD_EFFECT) !== 0) { + queue_yield_task(() => { + flush_effect(signal); + }); + } else if (scheduler_mode === FLUSH_MICROTASK) { if (!is_micro_task_queued) { is_micro_task_queued = true; queueMicrotask(process_deferred); diff --git a/packages/svelte/src/utils.js b/packages/svelte/src/utils.js index e42721b4f4..adffaae9f2 100644 --- a/packages/svelte/src/utils.js +++ b/packages/svelte/src/utils.js @@ -427,6 +427,7 @@ const RUNES = /** @type {const} */ ([ '$derived.by', '$effect', '$effect.pre', + '$effect.yield', '$effect.tracking', '$effect.root', '$inspect', diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 671f68bff7..a97fca27ef 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -2913,6 +2913,8 @@ declare namespace $effect { */ export function pre(fn: () => void | (() => void)): void; + export function yield(fn: () => void | (() => void)): void; + /** * The `$effect.tracking` rune is an advanced feature that tells you whether or not the code is running inside a tracking context, such as an effect or inside your template. *