feat: add $effect.yield rune

yield
Dominic Gannaway 9 months ago
parent 36a437c2f9
commit f2aba081c0

@ -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.
*

@ -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);
}

@ -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));

@ -13,6 +13,7 @@ export function ExpressionStatement(node, context) {
if (
rune === '$effect' ||
rune === '$effect.pre' ||
rune === '$effect.yield' ||
rune === '$effect.root' ||
rune === '$inspect.trace'
) {

@ -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');

@ -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.
*/

@ -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 {

@ -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

@ -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<Effect>} 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);

@ -427,6 +427,7 @@ const RUNES = /** @type {const} */ ([
'$derived.by',
'$effect',
'$effect.pre',
'$effect.yield',
'$effect.tracking',
'$effect.root',
'$inspect',

@ -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.
*

Loading…
Cancel
Save