chore: move reactivity code around (#10696)

* move some code

* split computations.js into deriveds.js and effects.js

* move reactivity types into separate .d.ts file

* move some signal code

---------

Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/10697/head
Rich Harris 7 months ago committed by GitHub
parent 2d15c9de3f
commit aa29a853dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,6 +1,6 @@
import { createClassComponent } from '../../legacy/legacy-client.js'; import { createClassComponent } from '../../legacy/legacy-client.js';
import { destroy_signal } from './runtime.js'; import { destroy_signal } from './runtime.js';
import { render_effect } from './reactivity/computations.js'; import { render_effect } from './reactivity/effects.js';
import { open, close } from './render.js'; import { open, close } from './render.js';
import { define_property } from './utils.js'; import { define_property } from './utils.js';

@ -8,7 +8,7 @@ import {
flushSync, flushSync,
push_destroy_fn push_destroy_fn
} from '../../runtime.js'; } from '../../runtime.js';
import { render_effect } from '../../reactivity/computations.js'; import { render_effect } from '../../reactivity/effects.js';
import { trigger_transitions } from '../../transitions.js'; import { trigger_transitions } from '../../transitions.js';
import { AWAIT_BLOCK, UNINITIALIZED } from '../../constants.js'; import { AWAIT_BLOCK, UNINITIALIZED } from '../../constants.js';

@ -16,15 +16,9 @@ import {
} from '../../hydration.js'; } from '../../hydration.js';
import { clear_text_content, empty, map_get, map_set } from '../../operations.js'; import { clear_text_content, empty, map_get, map_set } from '../../operations.js';
import { insert, remove } from '../../reconciler.js'; import { insert, remove } from '../../reconciler.js';
import { import { current_block, destroy_signal, execute_effect, push_destroy_fn } from '../../runtime.js';
current_block, import { render_effect } from '../../reactivity/effects.js';
destroy_signal, import { source, mutable_source, set_signal_value } from '../../reactivity/sources.js';
execute_effect,
push_destroy_fn,
set_signal_value
} from '../../runtime.js';
import { render_effect } from '../../reactivity/computations.js';
import { source, mutable_source } from '../../reactivity/sources.js';
import { trigger_transitions } from '../../transitions.js'; import { trigger_transitions } from '../../transitions.js';
import { is_array } from '../../utils.js'; import { is_array } from '../../utils.js';
import { EACH_BLOCK, EACH_ITEM_BLOCK } from '../../constants.js'; import { EACH_BLOCK, EACH_ITEM_BLOCK } from '../../constants.js';

@ -7,7 +7,7 @@ import {
} from '../../hydration.js'; } from '../../hydration.js';
import { remove } from '../../reconciler.js'; import { remove } from '../../reconciler.js';
import { current_block, destroy_signal, execute_effect, push_destroy_fn } from '../../runtime.js'; import { current_block, destroy_signal, execute_effect, push_destroy_fn } from '../../runtime.js';
import { render_effect } from '../../reactivity/computations.js'; import { render_effect } from '../../reactivity/effects.js';
import { trigger_transitions } from '../../transitions.js'; import { trigger_transitions } from '../../transitions.js';
/** @returns {import('../../types.js').IfBlock} */ /** @returns {import('../../types.js').IfBlock} */

@ -2,7 +2,7 @@ import { UNINITIALIZED, KEY_BLOCK } from '../../constants.js';
import { hydrate_block_anchor } from '../../hydration.js'; import { hydrate_block_anchor } from '../../hydration.js';
import { remove } from '../../reconciler.js'; import { remove } from '../../reconciler.js';
import { current_block, destroy_signal, execute_effect, push_destroy_fn } from '../../runtime.js'; import { current_block, destroy_signal, execute_effect, push_destroy_fn } from '../../runtime.js';
import { render_effect } from '../../reactivity/computations.js'; import { render_effect } from '../../reactivity/effects.js';
import { trigger_transitions } from '../../transitions.js'; import { trigger_transitions } from '../../transitions.js';
import { safe_not_equal } from '../../reactivity/equality.js'; import { safe_not_equal } from '../../reactivity/equality.js';

@ -1,14 +1,12 @@
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { import {
get, get,
set,
updating_derived, updating_derived,
batch_inspect, batch_inspect,
current_component_context, current_component_context,
untrack, untrack
set_signal_value
} from './runtime.js'; } from './runtime.js';
import { effect_active } from './reactivity/computations.js'; import { effect_active } from './reactivity/effects.js';
import { import {
array_prototype, array_prototype,
define_property, define_property,
@ -20,7 +18,7 @@ import {
object_prototype object_prototype
} from './utils.js'; } from './utils.js';
import { add_owner, check_ownership, strip_owner } from './dev/ownership.js'; import { add_owner, check_ownership, strip_owner } from './dev/ownership.js';
import { mutable_source, source } from './reactivity/sources.js'; import { mutable_source, source, set, set_signal_value } from './reactivity/sources.js';
import { STATE_SYMBOL, UNINITIALIZED } from './constants.js'; import { STATE_SYMBOL, UNINITIALIZED } from './constants.js';
/** /**

@ -0,0 +1,36 @@
import { CLEAN, DERIVED, UNINITIALIZED, UNOWNED } from '../constants.js';
import { current_block, current_consumer, current_effect } from '../runtime.js';
import { create_computation_signal, push_reference } from './effects.js';
import { default_equals, safe_equal } from './equality.js';
/**
* @template V
* @param {() => V} fn
* @returns {import('../types.js').ComputationSignal<V>}
*/
/*#__NO_SIDE_EFFECTS__*/
export function derived(fn) {
const is_unowned = current_effect === null;
const flags = is_unowned ? DERIVED | UNOWNED : DERIVED;
const signal = /** @type {import('../types.js').ComputationSignal<V>} */ (
create_computation_signal(flags | CLEAN, UNINITIALIZED, current_block)
);
signal.i = fn;
signal.e = default_equals;
if (current_consumer !== null) {
push_reference(current_consumer, signal);
}
return signal;
}
/**
* @template V
* @param {() => V} fn
* @returns {import('../types.js').ComputationSignal<V>}
*/
/*#__NO_SIDE_EFFECTS__*/
export function derived_safe_equal(fn) {
const signal = derived(fn);
signal.e = safe_equal;
return signal;
}

@ -27,7 +27,7 @@ import {
* @param {V} value * @param {V} value
* @param {import('../types.js').Block | null} block * @param {import('../types.js').Block | null} block
*/ */
function create_computation_signal(flags, value, block) { export function create_computation_signal(flags, value, block) {
/** @type {import('../types.js').ComputationSignal<V>} */ /** @type {import('../types.js').ComputationSignal<V>} */
const signal = { const signal = {
b: block, b: block,
@ -224,35 +224,3 @@ export function render_effect(fn, block = current_block, managed = false, sync =
} }
return internal_create_effect(flags, /** @type {any} */ (fn), sync, block, true); return internal_create_effect(flags, /** @type {any} */ (fn), sync, block, true);
} }
/**
* @template V
* @param {() => V} fn
* @returns {import('../types.js').ComputationSignal<V>}
*/
/*#__NO_SIDE_EFFECTS__*/
export function derived(fn) {
const is_unowned = current_effect === null;
const flags = is_unowned ? DERIVED | UNOWNED : DERIVED;
const signal = /** @type {import('../types.js').ComputationSignal<V>} */ (
create_computation_signal(flags | CLEAN, UNINITIALIZED, current_block)
);
signal.i = fn;
signal.e = default_equals;
if (current_consumer !== null) {
push_reference(current_consumer, signal);
}
return signal;
}
/**
* @template V
* @param {() => V} fn
* @returns {import('../types.js').ComputationSignal<V>}
*/
/*#__NO_SIDE_EFFECTS__*/
export function derived_safe_equal(fn) {
const signal = derived(fn);
signal.e = safe_equal;
return signal;
}

@ -0,0 +1,21 @@
/**
* @param {((value?: number) => number)} fn
* @param {1 | -1} [d]
* @returns {number}
*/
export function update_prop(fn, d = 1) {
const value = fn();
fn(value + d);
return value;
}
/**
* @param {((value?: number) => number)} fn
* @param {1 | -1} [d]
* @returns {number}
*/
export function update_pre_prop(fn, d = 1) {
const value = fn() + d;
fn(value);
return value;
}

@ -1,7 +1,25 @@
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { current_component_context } from '../runtime.js'; import {
current_component_context,
current_consumer,
current_dependencies,
current_effect,
current_untracked_writes,
current_untracking,
flushSync,
get,
ignore_mutation_validation,
is_batching_effect,
is_runes,
mark_signal_consumers,
schedule_effect,
set_current_untracked_writes,
set_last_inspected_signal,
set_signal_status,
untrack
} from '../runtime.js';
import { default_equals, safe_equal } from './equality.js'; import { default_equals, safe_equal } from './equality.js';
import { CLEAN, SOURCE } from '../constants.js'; import { CLEAN, DERIVED, DIRTY, MANAGED, SOURCE } from '../constants.js';
/** /**
* @template V * @template V
@ -68,3 +86,109 @@ function create_source_signal(flags, value) {
w: 0 w: 0
}; };
} }
/**
* @template V
* @param {import('./types.js').Signal<V>} signal
* @param {V} value
* @returns {V}
*/
export function set(signal, value) {
set_signal_value(signal, value);
return value;
}
/**
* @template V
* @param {import('./types.js').Signal<V>} signal
* @param {V} value
* @returns {void}
*/
export function set_sync(signal, value) {
flushSync(() => set(signal, value));
}
/**
* @template V
* @param {import('./types.js').Signal<V>} source
* @param {V} value
*/
export function mutate(source, value) {
set_signal_value(
source,
untrack(() => get(source))
);
return value;
}
/**
* @template V
* @param {import('./types.js').Signal<V>} signal
* @param {V} value
* @returns {void}
*/
export function set_signal_value(signal, value) {
if (
!current_untracking &&
!ignore_mutation_validation &&
current_consumer !== null &&
is_runes(null) &&
(current_consumer.f & DERIVED) !== 0
) {
throw new Error(
'ERR_SVELTE_UNSAFE_MUTATION' +
(DEV
? ": Unsafe mutations during Svelte's render or derived phase are not permitted in runes mode. " +
'This can lead to unexpected errors and possibly cause infinite loops.\n\nIf this mutation is not meant ' +
'to be reactive do not use the "$state" rune for that declaration.'
: '')
);
}
if (
(signal.f & SOURCE) !== 0 &&
!(/** @type {import('#client').EqualsFunctions} */ (signal.e)(value, signal.v))
) {
signal.v = value;
// Increment write version so that unowned signals can properly track dirtyness
signal.w++;
// If the current signal is running for the first time, it won't have any
// consumers as we only allocate and assign the consumers after the signal
// has fully executed. So in the case of ensuring it registers the consumer
// properly for itself, we need to ensure the current effect actually gets
// scheduled. i.e:
//
// $effect(() => x++)
//
// We additionally want to skip this logic for when ignore_mutation_validation is
// true, as stores write to source signal on initialization.
if (
is_runes(null) &&
!ignore_mutation_validation &&
current_effect !== null &&
current_effect.c === null &&
(current_effect.f & CLEAN) !== 0 &&
(current_effect.f & MANAGED) === 0
) {
if (current_dependencies !== null && current_dependencies.includes(signal)) {
set_signal_status(current_effect, DIRTY);
schedule_effect(current_effect, false);
} else {
if (current_untracked_writes === null) {
set_current_untracked_writes([signal]);
} else {
current_untracked_writes.push(signal);
}
}
}
mark_signal_consumers(signal, DIRTY, true);
// @ts-expect-error
if (DEV && signal.inspect) {
if (is_batching_effect) {
set_last_inspected_signal(/** @type {import('./types.js').SignalDebug} */ (signal));
} else {
for (const fn of /** @type {import('./types.js').SignalDebug} */ (signal).inspect) fn();
}
}
}
}

@ -1,9 +1,9 @@
import { subscribe_to_store } from '../../../store/utils.js'; import { subscribe_to_store } from '../../../store/utils.js';
import { noop } from '../../common.js'; import { noop } from '../../common.js';
import { UNINITIALIZED } from '../constants.js'; import { UNINITIALIZED } from '../constants.js';
import { get, set, set_ignore_mutation_validation, untrack } from '../runtime.js'; import { get, set_ignore_mutation_validation, untrack } from '../runtime.js';
import { user_effect } from './computations.js'; import { user_effect } from './effects.js';
import { mutable_source } from './sources.js'; import { mutable_source, set } from './sources.js';
/** /**
* Gets the current value of a store. If the store isn't subscribed to yet, it will create a proxy * Gets the current value of a store. If the store isn't subscribed to yet, it will create a proxy

@ -0,0 +1,74 @@
import type { Block, ComponentContext, EqualsFunctions } from '#client';
import type { DERIVED, EFFECT, PRE_EFFECT, RENDER_EFFECT, SOURCE } from '../constants';
export type SignalFlags =
| typeof SOURCE
| typeof DERIVED
| typeof EFFECT
| typeof PRE_EFFECT
| typeof RENDER_EFFECT;
export type EffectType = typeof EFFECT | typeof PRE_EFFECT | typeof RENDER_EFFECT;
// We keep two shapes rather than a single monomorphic shape to improve the memory usage.
// Source signals don't need the same shape as they simply don't do as much as computations
// (effects and derived signals). Thus we can improve the memory profile at the slight cost
// of some runtime performance.
export type SourceSignal<V = unknown> = {
/** consumers: Signals that read from the current signal */
c: null | ComputationSignal[];
/** equals: For value equality */
e: null | EqualsFunctions;
/** flags: The types that the signal represent, as a bitwise value */
f: SignalFlags;
/** value: The latest value for this signal */
v: V;
// write version
w: number;
};
export type SourceSignalDebug = {
/** This is DEV only */
inspect: Set<Function>;
};
export type ComputationSignal<V = unknown> = {
/** block: The block associated with this effect/computed */
b: null | Block;
/** consumers: Signals that read from the current signal */
c: null | ComputationSignal[];
/** context: The associated component if this signal is an effect/computed */
x: null | ComponentContext;
/** dependencies: Signals that this signal reads from */
d: null | Signal<V>[];
/** destroy: Thing(s) that need destroying */
y: null | (() => void) | Array<() => void>;
/** equals: For value equality */
e: null | EqualsFunctions;
/** The types that the signal represent, as a bitwise value */
f: SignalFlags;
/** init: The function that we invoke for effects and computeds */
i:
| null
| (() => V)
| (() => void | (() => void))
| ((b: Block, s: Signal) => void | (() => void));
/** references: Anything that a signal owns */
r: null | ComputationSignal[];
/** value: The latest value for this signal, doubles as the teardown for effects */
v: V;
/** level: the depth from the root signal, used for ordering render/pre-effects topologically **/
l: number;
/** write version: used for unowned signals to track if their depdendencies are dirty or not **/
w: number;
};
export type Signal<V = unknown> = SourceSignal<V> | ComputationSignal<V>;
export type SignalDebug<V = unknown> = SourceSignalDebug & Signal<V>;
export type EffectSignal = ComputationSignal<null | (() => void)>;
export type MaybeSignal<T = unknown> = T | Signal<T>;
export type UnwrappedSignal<T> = T extends Signal<infer U> ? U : T;

@ -45,19 +45,18 @@ import {
pop, pop,
current_component_context, current_component_context,
get, get,
set,
is_signals_recorded, is_signals_recorded,
inspect_fn, inspect_fn,
deep_read_state deep_read_state
} from './runtime.js'; } from './runtime.js';
import { derived } from './reactivity/deriveds.js';
import { import {
render_effect, render_effect,
effect, effect,
managed_effect, managed_effect,
derived,
pre_effect, pre_effect,
user_effect user_effect
} from './reactivity/computations.js'; } from './reactivity/effects.js';
import { import {
current_hydration_fragment, current_hydration_fragment,
get_hydration_fragment, get_hydration_fragment,
@ -76,7 +75,7 @@ import {
} from './utils.js'; } from './utils.js';
import { run } from '../common.js'; import { run } from '../common.js';
import { bind_transition, trigger_transitions } from './transitions.js'; import { bind_transition, trigger_transitions } from './transitions.js';
import { mutable_source, source } from './reactivity/sources.js'; import { mutable_source, source, set } from './reactivity/sources.js';
import { safe_equal, safe_not_equal } from './reactivity/equality.js'; import { safe_equal, safe_not_equal } from './reactivity/equality.js';
/** @type {Set<string>} */ /** @type {Set<string>} */

@ -10,7 +10,7 @@ import {
object_prototype object_prototype
} from './utils.js'; } from './utils.js';
import { unstate } from './proxy.js'; import { unstate } from './proxy.js';
import { pre_effect } from './reactivity/computations.js'; import { pre_effect } from './reactivity/effects.js';
import { import {
EACH_BLOCK, EACH_BLOCK,
IF_BLOCK, IF_BLOCK,
@ -31,6 +31,7 @@ import {
} from './constants.js'; } from './constants.js';
import { flush_tasks } from './dom/task.js'; import { flush_tasks } from './dom/task.js';
import { add_owner } from './dev/ownership.js'; import { add_owner } from './dev/ownership.js';
import { mutate, set_signal_value } from './reactivity/sources.js';
const IS_EFFECT = EFFECT | PRE_EFFECT | RENDER_EFFECT; const IS_EFFECT = EFFECT | PRE_EFFECT | RENDER_EFFECT;
@ -64,20 +65,32 @@ export let current_consumer = null;
export let current_effect = null; export let current_effect = null;
/** @type {null | import('./types.js').Signal[]} */ /** @type {null | import('./types.js').Signal[]} */
let current_dependencies = null; export let current_dependencies = null;
let current_dependencies_index = 0; let current_dependencies_index = 0;
/** /**
* Tracks writes that the effect it's executed in doesn't listen to yet, * Tracks writes that the effect it's executed in doesn't listen to yet,
* so that the dependency can be added to the effect later on if it then reads it * so that the dependency can be added to the effect later on if it then reads it
* @type {null | import('./types.js').Signal[]} * @type {null | import('./types.js').Signal[]}
*/ */
let current_untracked_writes = null; export let current_untracked_writes = null;
/** @param {null | import('./types.js').Signal[]} value */
export function set_current_untracked_writes(value) {
current_untracked_writes = value;
}
/** @type {null | import('./types.js').SignalDebug} */ /** @type {null | import('./types.js').SignalDebug} */
let last_inspected_signal = null; export let last_inspected_signal = null;
/** @param {null | import('./types.js').SignalDebug} signal */
export function set_last_inspected_signal(signal) {
last_inspected_signal = signal;
}
/** If `true`, `get`ting the signal should not register it as a dependency */ /** If `true`, `get`ting the signal should not register it as a dependency */
export let current_untracking = false; export let current_untracking = false;
/** Exists to opt out of the mutation validation for stores which may be set for the first time during a derivation */ /** Exists to opt out of the mutation validation for stores which may be set for the first time during a derivation */
let ignore_mutation_validation = false; export let ignore_mutation_validation = false;
/** @param {boolean} value */ /** @param {boolean} value */
export function set_ignore_mutation_validation(value) { export function set_ignore_mutation_validation(value) {
ignore_mutation_validation = value; ignore_mutation_validation = value;
@ -110,7 +123,7 @@ export let updating_derived = false;
* @param {null | import('./types.js').ComponentContext} context * @param {null | import('./types.js').ComponentContext} context
* @returns {boolean} * @returns {boolean}
*/ */
function is_runes(context) { export function is_runes(context) {
const component_context = context || current_component_context; const component_context = context || current_component_context;
return component_context !== null && component_context.r; return component_context !== null && component_context.r;
} }
@ -767,27 +780,6 @@ export function get(signal) {
return signal.v; return signal.v;
} }
/**
* @template V
* @param {import('./types.js').Signal<V>} signal
* @param {V} value
* @returns {V}
*/
export function set(signal, value) {
set_signal_value(signal, value);
return value;
}
/**
* @template V
* @param {import('./types.js').Signal<V>} signal
* @param {V} value
* @returns {void}
*/
export function set_sync(signal, value) {
flushSync(() => set(signal, value));
}
/** /**
* Invokes a function and captures all signals that are read during the invocation, * Invokes a function and captures all signals that are read during the invocation,
* then invalidates them. * then invalidates them.
@ -816,19 +808,6 @@ export function invalidate_inner_signals(fn) {
} }
} }
/**
* @template V
* @param {import('./types.js').Signal<V>} source
* @param {V} value
*/
export function mutate(source, value) {
set_signal_value(
source,
untrack(() => get(source))
);
return value;
}
/** /**
* @param {import('./types.js').ComputationSignal} signal * @param {import('./types.js').ComputationSignal} signal
* @param {boolean} inert * @param {boolean} inert
@ -900,7 +879,7 @@ export function mark_subtree_inert(signal, inert, visited_blocks = new Set()) {
* @param {boolean} force_schedule * @param {boolean} force_schedule
* @returns {void} * @returns {void}
*/ */
function mark_signal_consumers(signal, to_status, force_schedule) { export function mark_signal_consumers(signal, to_status, force_schedule) {
const runes = is_runes(null); const runes = is_runes(null);
const consumers = signal.c; const consumers = signal.c;
if (consumers !== null) { if (consumers !== null) {
@ -932,78 +911,6 @@ function mark_signal_consumers(signal, to_status, force_schedule) {
} }
} }
/**
* @template V
* @param {import('./types.js').Signal<V>} signal
* @param {V} value
* @returns {void}
*/
export function set_signal_value(signal, value) {
if (
!current_untracking &&
!ignore_mutation_validation &&
current_consumer !== null &&
is_runes(null) &&
(current_consumer.f & DERIVED) !== 0
) {
throw new Error(
'ERR_SVELTE_UNSAFE_MUTATION' +
(DEV
? ": Unsafe mutations during Svelte's render or derived phase are not permitted in runes mode. " +
'This can lead to unexpected errors and possibly cause infinite loops.\n\nIf this mutation is not meant ' +
'to be reactive do not use the "$state" rune for that declaration.'
: '')
);
}
if (
(signal.f & SOURCE) !== 0 &&
!(/** @type {import('./types.js').EqualsFunctions} */ (signal.e)(value, signal.v))
) {
signal.v = value;
// Increment write version so that unowned signals can properly track dirtyness
signal.w++;
// If the current signal is running for the first time, it won't have any
// consumers as we only allocate and assign the consumers after the signal
// has fully executed. So in the case of ensuring it registers the consumer
// properly for itself, we need to ensure the current effect actually gets
// scheduled. i.e:
//
// $effect(() => x++)
//
// We additionally want to skip this logic for when ignore_mutation_validation is
// true, as stores write to source signal on initialization.
if (
is_runes(null) &&
!ignore_mutation_validation &&
current_effect !== null &&
current_effect.c === null &&
(current_effect.f & CLEAN) !== 0 &&
(current_effect.f & MANAGED) === 0
) {
if (current_dependencies !== null && current_dependencies.includes(signal)) {
set_signal_status(current_effect, DIRTY);
schedule_effect(current_effect, false);
} else {
if (current_untracked_writes === null) {
current_untracked_writes = [signal];
} else {
current_untracked_writes.push(signal);
}
}
}
mark_signal_consumers(signal, DIRTY, true);
// @ts-expect-error
if (DEV && signal.inspect) {
if (is_batching_effect) {
last_inspected_signal = /** @type {import('./types.js').SignalDebug} */ (signal);
} else {
for (const fn of /** @type {import('./types.js').SignalDebug} */ (signal).inspect) fn();
}
}
}
}
/** /**
* @template V * @template V
* @param {import('./types.js').ComputationSignal<V>} signal * @param {import('./types.js').ComputationSignal<V>} signal
@ -1071,7 +978,7 @@ const STATUS_MASK = ~(DIRTY | MAYBE_DIRTY | CLEAN);
* @param {number} status * @param {number} status
* @returns {void} * @returns {void}
*/ */
function set_signal_status(signal, status) { export function set_signal_status(signal, status) {
signal.f = (signal.f & STATUS_MASK) | status; signal.f = (signal.f & STATUS_MASK) | status;
} }
@ -1208,17 +1115,6 @@ export function update(signal, d = 1) {
return value; return value;
} }
/**
* @param {((value?: number) => number)} fn
* @param {1 | -1} [d]
* @returns {number}
*/
export function update_prop(fn, d = 1) {
const value = fn();
fn(value + d);
return value;
}
/** /**
* @param {import('./types.js').Signal<number>} signal * @param {import('./types.js').Signal<number>} signal
* @param {1 | -1} [d] * @param {1 | -1} [d]
@ -1230,17 +1126,6 @@ export function update_pre(signal, d = 1) {
return value; return value;
} }
/**
* @param {((value?: number) => number)} fn
* @param {1 | -1} [d]
* @returns {number}
*/
export function update_pre_prop(fn, d = 1) {
const value = fn() + d;
fn(value);
return value;
}
/** /**
* @param {Record<string, unknown>} obj * @param {Record<string, unknown>} obj
* @param {string[]} keys * @param {string[]} keys

@ -12,7 +12,7 @@ import {
import { destroy_each_item_block, get_first_element } from './dom/blocks/each.js'; import { destroy_each_item_block, get_first_element } from './dom/blocks/each.js';
import { schedule_raf_task } from './dom/task.js'; import { schedule_raf_task } from './dom/task.js';
import { append_child, empty } from './operations.js'; import { append_child, empty } from './operations.js';
import { effect, managed_effect, managed_pre_effect } from './reactivity/computations.js'; import { effect, managed_effect, managed_pre_effect } from './reactivity/effects.js';
import { import {
current_block, current_block,
current_effect, current_effect,

@ -1,9 +1,4 @@
import { import {
DERIVED,
EFFECT,
RENDER_EFFECT,
SOURCE,
PRE_EFFECT,
ROOT_BLOCK, ROOT_BLOCK,
EACH_BLOCK, EACH_BLOCK,
EACH_ITEM_BLOCK, EACH_ITEM_BLOCK,
@ -16,16 +11,7 @@ import {
SNIPPET_BLOCK, SNIPPET_BLOCK,
STATE_SYMBOL STATE_SYMBOL
} from './constants.js'; } from './constants.js';
import type { ComputationSignal, EffectSignal, Signal, SourceSignal } from './reactivity/types.js';
// Put all internal types in this file. Once we convert to JSDoc, we can make this a d.ts file
export type SignalFlags =
| typeof SOURCE
| typeof DERIVED
| typeof EFFECT
| typeof PRE_EFFECT
| typeof RENDER_EFFECT;
export type EffectType = typeof EFFECT | typeof PRE_EFFECT | typeof RENDER_EFFECT;
type EventCallback = (event: Event) => boolean; type EventCallback = (event: Event) => boolean;
export type EventCallbackMap = Record<string, EventCallback | EventCallback[]>; export type EventCallbackMap = Record<string, EventCallback | EventCallback[]>;
@ -67,70 +53,6 @@ export type ComponentContext = {
}; };
}; };
// We keep two shapes rather than a single monomorphic shape to improve the memory usage.
// Source signals don't need the same shape as they simply don't do as much as computations
// (effects and derived signals). Thus we can improve the memory profile at the slight cost
// of some runtime performance.
export type SourceSignal<V = unknown> = {
/** consumers: Signals that read from the current signal */
c: null | ComputationSignal[];
/** equals: For value equality */
e: null | EqualsFunctions;
/** flags: The types that the signal represent, as a bitwise value */
f: SignalFlags;
/** value: The latest value for this signal */
v: V;
// write version
w: number;
};
export type SourceSignalDebug = {
/** This is DEV only */
inspect: Set<Function>;
};
export type ComputationSignal<V = unknown> = {
/** block: The block associated with this effect/computed */
b: null | Block;
/** consumers: Signals that read from the current signal */
c: null | ComputationSignal[];
/** context: The associated component if this signal is an effect/computed */
x: null | ComponentContext;
/** dependencies: Signals that this signal reads from */
d: null | Signal<V>[];
/** destroy: Thing(s) that need destroying */
y: null | (() => void) | Array<() => void>;
/** equals: For value equality */
e: null | EqualsFunctions;
/** The types that the signal represent, as a bitwise value */
f: SignalFlags;
/** init: The function that we invoke for effects and computeds */
i:
| null
| (() => V)
| (() => void | (() => void))
| ((b: Block, s: Signal) => void | (() => void));
/** references: Anything that a signal owns */
r: null | ComputationSignal[];
/** value: The latest value for this signal, doubles as the teardown for effects */
v: V;
/** level: the depth from the root signal, used for ordering render/pre-effects topologically **/
l: number;
/** write version: used for unowned signals to track if their depdendencies are dirty or not **/
w: number;
};
export type Signal<V = unknown> = SourceSignal<V> | ComputationSignal<V>;
export type SignalDebug<V = unknown> = SourceSignalDebug & Signal<V>;
export type EffectSignal = ComputationSignal<null | (() => void)>;
export type MaybeSignal<T = unknown> = T | Signal<T>;
export type UnwrappedSignal<T> = T extends Signal<infer U> ? U : T;
export type EqualsFunctions<T = any> = (a: T, v: T) => boolean; export type EqualsFunctions<T = any> = (a: T, v: T) => boolean;
export type BlockType = export type BlockType =
@ -421,3 +343,5 @@ export interface ProxyMetadata<T = Record<string | symbol, any>> {
export type ProxyStateObject<T = Record<string | symbol, any>> = T & { export type ProxyStateObject<T = Record<string | symbol, any>> = T & {
[STATE_SYMBOL]: ProxyMetadata; [STATE_SYMBOL]: ProxyMetadata;
}; };
export * from './reactivity/types';

@ -1,16 +1,11 @@
export { export {
get, get,
set,
set_sync,
invalidate_inner_signals, invalidate_inner_signals,
flushSync, flushSync,
tick, tick,
untrack, untrack,
update, update,
update_prop,
update_pre, update_pre,
update_pre_prop,
mutate,
value_or_fallback, value_or_fallback,
exclude_from_object, exclude_from_object,
pop, pop,
@ -30,9 +25,11 @@ export { await_block as await } from './client/dom/blocks/await.js';
export { if_block as if } from './client/dom/blocks/if.js'; export { if_block as if } from './client/dom/blocks/if.js';
export { key_block as key } from './client/dom/blocks/key.js'; export { key_block as key } from './client/dom/blocks/key.js';
export * from './client/dom/blocks/each.js'; export * from './client/dom/blocks/each.js';
export * from './client/reactivity/computations.js'; export * from './client/reactivity/deriveds.js';
export * from './client/reactivity/effects.js';
export * from './client/reactivity/sources.js'; export * from './client/reactivity/sources.js';
export * from './client/reactivity/equality.js'; export * from './client/reactivity/equality.js';
export * from './client/reactivity/props.js';
export * from './client/reactivity/store.js'; export * from './client/reactivity/store.js';
export * from './client/render.js'; export * from './client/render.js';
export * from './client/validate.js'; export * from './client/validate.js';

@ -1,5 +1,5 @@
import { source } from '../internal/client/reactivity/sources.js'; import { source, set } from '../internal/client/reactivity/sources.js';
import { get, set } from '../internal/client/runtime.js'; import { get } from '../internal/client/runtime.js';
/** @type {Array<keyof Date>} */ /** @type {Array<keyof Date>} */
const read = [ const read = [

@ -1,12 +1,8 @@
import { describe, assert, it } from 'vitest'; import { describe, assert, it } from 'vitest';
import * as $ from '../../src/internal/client/runtime'; import * as $ from '../../src/internal/client/runtime';
import { import { derived } from '../../src/internal/client/reactivity/deriveds';
derived, import { effect, render_effect, user_effect } from '../../src/internal/client/reactivity/effects';
effect, import { source, set } from '../../src/internal/client/reactivity/sources';
render_effect,
user_effect
} from '../../src/internal/client/reactivity/computations';
import { source } from '../../src/internal/client/reactivity/sources';
import type { ComputationSignal } from '../../src/internal/client/types'; import type { ComputationSignal } from '../../src/internal/client/types';
import { proxy } from '../../src/internal/client/proxy'; import { proxy } from '../../src/internal/client/proxy';
@ -51,8 +47,8 @@ describe('signals', () => {
}); });
return () => { return () => {
$.flushSync(() => $.set(count, 1)); $.flushSync(() => set(count, 1));
$.flushSync(() => $.set(count, 2)); $.flushSync(() => set(count, 2));
assert.deepEqual(log, ['0:0', '1:2', '2:4']); assert.deepEqual(log, ['0:0', '1:2', '2:4']);
}; };
@ -72,8 +68,8 @@ describe('signals', () => {
}); });
return () => { return () => {
$.flushSync(() => $.set(count, 1)); $.flushSync(() => set(count, 1));
$.flushSync(() => $.set(count, 2)); $.flushSync(() => set(count, 2));
assert.deepEqual(log, ['A:0:0', 'B:0', 'A:1:2', 'B:2', 'A:2:4', 'B:4']); assert.deepEqual(log, ['A:0:0', 'B:0', 'A:1:2', 'B:2', 'A:2:4', 'B:4']);
}; };
@ -93,8 +89,8 @@ describe('signals', () => {
}); });
return () => { return () => {
$.flushSync(() => $.set(count, 1)); $.flushSync(() => set(count, 1));
$.flushSync(() => $.set(count, 2)); $.flushSync(() => set(count, 2));
assert.deepEqual(log, ['A:0', 'B:0:0', 'A:2', 'B:1:2', 'A:4', 'B:2:4']); assert.deepEqual(log, ['A:0', 'B:0:0', 'A:2', 'B:1:2', 'A:4', 'B:2:4']);
}; };
@ -111,8 +107,8 @@ describe('signals', () => {
}); });
return () => { return () => {
$.flushSync(() => $.set(count, 1)); $.flushSync(() => set(count, 1));
$.flushSync(() => $.set(count, 2)); $.flushSync(() => set(count, 2));
assert.deepEqual(log, [0, 2, 4]); assert.deepEqual(log, [0, 2, 4]);
}; };
@ -130,8 +126,8 @@ describe('signals', () => {
}); });
return () => { return () => {
$.flushSync(() => $.set(count, 1)); $.flushSync(() => set(count, 1));
$.flushSync(() => $.set(count, 2)); $.flushSync(() => set(count, 2));
assert.deepEqual(log, [0, 4, 8]); assert.deepEqual(log, [0, 4, 8]);
}; };
@ -167,12 +163,12 @@ describe('signals', () => {
let i = 2; let i = 2;
while (--i) { while (--i) {
res.length = 0; res.length = 0;
$.set(B, 1); set(B, 1);
$.set(A, 1 + i * 2); set(A, 1 + i * 2);
$.flushSync(); $.flushSync();
$.set(A, 2 + i * 2); set(A, 2 + i * 2);
$.set(B, 2); set(B, 2);
$.flushSync(); $.flushSync();
assert.equal(res.length, 4); assert.equal(res.length, 4);
@ -195,13 +191,13 @@ describe('signals', () => {
}); });
return () => { return () => {
$.flushSync(() => $.set(count, 1)); $.flushSync(() => set(count, 1));
// Ensure we're not leaking consumers // Ensure we're not leaking consumers
assert.deepEqual(count.c?.length, 1); assert.deepEqual(count.c?.length, 1);
$.flushSync(() => $.set(count, 2)); $.flushSync(() => set(count, 2));
// Ensure we're not leaking consumers // Ensure we're not leaking consumers
assert.deepEqual(count.c?.length, 1); assert.deepEqual(count.c?.length, 1);
$.flushSync(() => $.set(count, 3)); $.flushSync(() => set(count, 3));
// Ensure we're not leaking consumers // Ensure we're not leaking consumers
assert.deepEqual(count.c?.length, 1); assert.deepEqual(count.c?.length, 1);
assert.deepEqual(log, [0, 1, 2, 3]); assert.deepEqual(log, [0, 1, 2, 3]);
@ -224,11 +220,11 @@ describe('signals', () => {
$.get(c); $.get(c);
$.flushSync(() => $.set(a, 1)); $.flushSync(() => set(a, 1));
$.get(c); $.get(c);
$.flushSync(() => $.set(b, 1)); $.flushSync(() => set(b, 1));
$.get(c); $.get(c);
@ -257,11 +253,11 @@ describe('signals', () => {
}); });
return () => { return () => {
$.flushSync(() => $.set(count, 1)); $.flushSync(() => set(count, 1));
$.flushSync(() => $.set(count, 2)); $.flushSync(() => set(count, 2));
$.flushSync(() => $.set(count, 3)); $.flushSync(() => set(count, 3));
$.flushSync(() => $.set(count, 4)); $.flushSync(() => set(count, 4));
$.flushSync(() => $.set(count, 0)); $.flushSync(() => set(count, 0));
// Ensure we're not leaking consumers // Ensure we're not leaking consumers
assert.deepEqual(count.c?.length, 1); assert.deepEqual(count.c?.length, 1);
assert.deepEqual(log, [0, 2, 'limit', 0]); assert.deepEqual(log, [0, 2, 'limit', 0]);
@ -319,7 +315,7 @@ describe('signals', () => {
const value = source({ count: 0 }); const value = source({ count: 0 });
user_effect(() => { user_effect(() => {
$.set(value, { count: 0 }); set(value, { count: 0 });
$.get(value); $.get(value);
}); });

Loading…
Cancel
Save