@xaendar/signals 0.0.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,229 +0,0 @@
1
- import { NoArgsVoidFunction } from '@xaendar/common';
2
- import { GLOBAL_STATE } from '../globals';
3
- import { PRIVATE, assertPrivateContext } from '../private-symbol';
4
- import { WatcherState } from '../types/watcher-state.type';
5
- import { Computed } from './computed';
6
- import { State } from './state';
7
-
8
- /**
9
- * A `Watcher` observes a set of Signals and fires a `notify` callback
10
- * synchronously when any of their (recursive) dependencies change.
11
- *
12
- * It is the low-level primitive on top of which frameworks implement
13
- * effects and scheduling. It does not hold a value and has no generic
14
- * type parameter.
15
- *
16
- * @see Signal algorithms — 'The `Signal.subtle.Watcher` class'
17
- */
18
- export class Watcher {
19
- /**
20
- * The current state of the Watcher.
21
- *
22
- * - `~waiting~` — newly created, or `notify` has already been called since
23
- * the last `watch` call. Not actively observing changes.
24
- * - `~watching~` — actively watching; no dependency has changed yet.
25
- * - `~pending~` — a dependency has changed but `notify` has not yet run.
26
- *
27
- * @internalSlot
28
- * @see Signal algorithms — 'Signal.subtle.Watcher State machine'
29
- */
30
- #state: WatcherState;
31
- /**
32
- * The ordered set of Signals this Watcher is currently watching.
33
- * May contain both `State` and `Computed` instances.
34
- *
35
- * @internalSlot
36
- * @see Signal algorithms — 'Signal.subtle.Watcher internal slots'
37
- */
38
- #signals: Set<State<unknown> | Computed<unknown>>;
39
- /**
40
- * Returns a snapshot of the current watched signals set for introspection.
41
- *
42
- * @param symbol - Private access symbol; rejects calls from outside the library.
43
- * @returns An array of `State` and `Computed` instances that this Watcher is watching.
44
- * @internal
45
- */
46
- public getSources(symbol: symbol): (State<unknown> | Computed<unknown>)[] {
47
- assertPrivateContext(symbol);
48
- return [...this.#signals];
49
- }
50
- /**
51
- * The callback invoked synchronously when a watched Signal (or one of its
52
- * recursive dependencies) changes for the first time since the last
53
- * `watch` call.
54
- *
55
- * Receives the Watcher itself as `this`. No Signals may be read or written
56
- * during its execution (`frozen` is `true` for its entire duration).
57
- *
58
- * @internalSlot
59
- * @see Signal algorithms — 'Signal.subtle.Watcher internal slots'
60
- */
61
- #notifyCallback: NoArgsVoidFunction;
62
-
63
- /**
64
- * Creates a new Watcher.
65
- *
66
- * The Watcher starts in the `~waiting~` state with an empty signals set.
67
- *
68
- * @param notifyCallback - Called synchronously (with `frozen = true`) the
69
- * first time a watched dependency changes after each `watch` call. No
70
- * Signals may be read or written inside this callback.
71
- *
72
- * @see Signal algorithms — 'Constructor: new Signal.subtle.Watcher(callback)'
73
- */
74
- constructor(notifyCallback: NoArgsVoidFunction) {
75
- this.#state = 'waiting';
76
- this.#signals = new Set;
77
- this.#notifyCallback = notifyCallback;
78
- }
79
-
80
- /**
81
- * Returns the subset of watched Signals that are `Computed` instances
82
- * currently in a `~dirty~` or `~checked~` state, meaning they may have a
83
- * stale value that has not yet been re-evaluated.
84
- *
85
- * Typically called inside the microtask scheduled by the `notify` callback
86
- * to know which Signals need to be pulled.
87
- *
88
- * @returns An array of `Computed` signals that are dirty or checked.
89
- *
90
- * @see Signal algorithms — 'Method: Signal.subtle.Watcher.prototype.getPending()'
91
- */
92
- public getPending(): Computed<unknown>[] {
93
- return [...this.#signals].filter((signal): signal is Computed<unknown> => {
94
- if (!(signal instanceof Computed)) {
95
- return false;
96
- }
97
-
98
- const state = signal.getState(PRIVATE);
99
- return state === 'dirty' || state === 'checked';
100
- });
101
- }
102
-
103
- /**
104
- * Adds the given Signals to the watched set and transitions the Watcher to
105
- * the `~watching~` state.
106
- *
107
- * For each newly-watched Signal, the Watcher is registered as a sink and —
108
- * if it is the first sink — the sink registration is propagated recursively
109
- * up through the Signal's sources, building the live dependency chain.
110
- *
111
- * The `watched` callback of each Signal (if any) is called with
112
- * `frozen = true`.
113
- *
114
- * @param signals - One or more `State` signals to start watching.
115
- * @throws If `frozen` is `true` at the time of the call.
116
- *
117
- * @see Signal algorithms — 'Method: Signal.subtle.Watcher.prototype.watch(...signals)'
118
- */
119
- public watch(...signals: (State | Computed)[]) {
120
- if (GLOBAL_STATE.frozen) {
121
- throw new Error('Cannot watch signals while frozen');
122
- }
123
-
124
- signals.forEach(signal => {
125
- if (this.#signals.has(signal)) {
126
- throw new Error('Cannot watch a signal that is already being watched');
127
- }
128
-
129
- this.#signals.add(signal)
130
- signal.addSink(this, PRIVATE);
131
- });
132
-
133
-
134
- if (this.#state === 'waiting') {
135
- this.#state = 'watching';
136
- }
137
- }
138
-
139
- /**
140
- * Removes the given Signals from the watched set.
141
- *
142
- * For each removed Signal, the Watcher is unregistered as a sink. If the
143
- * Signal's sink set becomes empty as a result, the removal is propagated
144
- * recursively up through its sources, tearing down the live dependency
145
- * chain and allowing garbage collection of unwatched nodes.
146
- *
147
- * The `unwatched` callback of each Signal (if any) is called with
148
- * `frozen = true`.
149
- *
150
- * If no Signals remain in the watched set, the Watcher transitions back to
151
- * the `~waiting~` state.
152
- *
153
- * @param signals - One or more `State` signals to stop watching.
154
- * @throws If `frozen` is `true` at the time of the call.
155
- * @throws If any of the given Signals is not currently being watched.
156
- *
157
- * @see Signal algorithms — 'Method: Signal.subtle.Watcher.prototype.unwatch(...signals)'
158
- */
159
- public unwatch(...signals: (State | Computed)[]) {
160
- if (GLOBAL_STATE.frozen) {
161
- throw new Error('Cannot unwatch signals while frozen');
162
- }
163
-
164
- signals.forEach(signal => {
165
- if (this.#signals.has(signal)) {
166
- throw new Error('Cannot unwatch a signal that is not being watched');
167
- }
168
-
169
- this.#signals.delete(signal)
170
- signal.removeSink(this, PRIVATE);
171
- });
172
-
173
- if (!this.#signals.size && this.#state === 'watching') {
174
- this.#state = 'waiting';
175
- }
176
- }
177
-
178
- /**
179
- * Get the current state of the Watcher.
180
- * @param symbol - The private symbol for prevent external calls.
181
- */
182
- public getState(symbol: symbol): WatcherState {
183
- assertPrivateContext(symbol);
184
- return this.#state;
185
- }
186
-
187
- /**
188
- * Set the current state of the Watcher.
189
- * @param newState - The new state to set.
190
- * @param symbol - The private symbol for prevent external calls.
191
- * @throws If the transition from `pending` to `watching` is attempted.
192
- */
193
- public setState(newState: WatcherState, symbol: symbol): void {
194
- assertPrivateContext(symbol);
195
-
196
- if (this.#state !== newState && this.#isValidTransition(this.#state, newState)) {
197
- this.#state = newState;
198
- }
199
- }
200
-
201
- /**
202
- * Invoce the notify callback when a watched dependency changes
203
- * @param symbol - The private symbol for prevent external calls.
204
- */
205
- public notify(symbol: symbol): void {
206
- assertPrivateContext(symbol);
207
-
208
- this.#state = 'pending';
209
-
210
- GLOBAL_STATE.frozen = true;
211
- try {
212
- this.#notifyCallback.call(this);
213
- } finally {
214
- this.#state = 'waiting';
215
- GLOBAL_STATE.frozen = false;
216
- }
217
- }
218
-
219
- #isValidTransition(from: WatcherState, to: WatcherState): boolean {
220
- switch (from) {
221
- case 'waiting':
222
- return to === 'watching';
223
- case 'watching':
224
- return to === 'pending' || to === 'waiting';
225
- case 'pending':
226
- return to === 'waiting';
227
- }
228
- }
229
- }
@@ -1,24 +0,0 @@
1
- /**
2
- * Symbol not available in public API,
3
- * used to call internal methods of `State` and `Computed` from `Watcher` without exposing them in the public API.
4
- *
5
- * @internal
6
- */
7
- export const PRIVATE = Symbol('signals-private');
8
-
9
- /**
10
- * Asserts that the provided symbol matches the internal {@link PRIVATE} symbol,
11
- * ensuring the caller has access to internal APIs.
12
- *
13
- * Throws if the symbol does not match, preventing external code from
14
- * invoking methods intended for internal use only.
15
- *
16
- * @param symbol - The symbol to validate against {@link PRIVATE}.
17
- * @throws {Error} If `symbol` does not match {@link PRIVATE}.
18
- * @internal
19
- */
20
- export function assertPrivateContext(symbol: symbol): void {
21
- if (symbol !== PRIVATE) {
22
- throw new Error('Invalid symbol');
23
- }
24
- }
package/src/lib/subtle.ts DELETED
@@ -1,79 +0,0 @@
1
- import { GLOBAL_STATE } from './globals';
2
- import { Computed } from './models/computed';
3
- import { State } from './models/state';
4
- import { Watcher } from './models/watcher';
5
- import { PRIVATE } from './private-symbol';
6
-
7
- /**
8
- * Executes a function without tracking any dependencies.
9
- * @param fn - The function to execute without tracking.
10
- * @returns The result of the function execution.
11
- */
12
- export function untrack<T>(fn: () => T): T {
13
- const prevComputing = GLOBAL_STATE.computing;
14
- GLOBAL_STATE.computing = null;
15
-
16
- try {
17
- return fn();
18
- } finally {
19
- GLOBAL_STATE.computing = prevComputing;
20
- }
21
- }
22
-
23
- /**
24
- * Returns the currently active `Computed` instance being evaluated, or `null`
25
- * @returns The currently active `Computed` instance, or `null` if none is being evaluated.
26
- */
27
- export function currentComputed(): Computed | null {
28
- return GLOBAL_STATE.computing;
29
- }
30
-
31
- /**
32
- * Returns the ordered list of all Signals which the given `Computed` or
33
- * `Watcher` referenced during its last evaluation.
34
- *
35
- * - For a `Computed`, these are the Signals read inside its callback.
36
- * - For a `Watcher`, these are the Signals it is currently watching.
37
- *
38
- * @param s - The `Computed` or `Watcher` to introspect.
39
- * @returns An array of `State` and `Computed` instances.
40
- */
41
- export function introspectSources(s: Computed | Watcher): (State | Computed)[] {
42
- return s.getSources(PRIVATE);
43
- }
44
-
45
- /**
46
- * Returns the direct dependents of the given Signal — Watchers that contain
47
- * it, plus any `Computed` Signals which read it during their last evaluation
48
- * (if that `Computed` is recursively watched).
49
- *
50
- * @param signal - The `State` or `Computed` Signal to introspect.
51
- * @returns An array of `Computed` and `Watcher` instances.
52
- */
53
- export function introspectSinks(signal: State | Computed): (Computed | Watcher)[] {
54
- return signal.getSinks(PRIVATE);
55
- }
56
-
57
- /**
58
- * Returns `true` if the given Signal is 'live' — i.e. it is watched by a
59
- * `Watcher`, or it is read by a `Computed` Signal which is (recursively)
60
- * live.
61
- *
62
- * @param signal - The `State` or `Computed` Signal to check.
63
- * @returns `true` if the Signal has at least one sink.
64
- */
65
- export function hasSinks(signal: State | Computed): boolean {
66
- return signal.getSinks(PRIVATE).length > 0;
67
- }
68
-
69
- /**
70
- * Returns `true` if the given node is 'reactive' — i.e. it depends on some
71
- * other Signal. A `Computed` where `hasSources` is `false` will always
72
- * return the same constant.
73
- *
74
- * @param signal - The `Computed` or `Watcher` to check.
75
- * @returns `true` if the node has at least one source.
76
- */
77
- export function hasSources(signal: Computed | Watcher): boolean {
78
- return signal.getSources(PRIVATE).length > 0;
79
- }
@@ -1,9 +0,0 @@
1
- /**
2
- * Rapresents the state of a computed signal.
3
- * The state can be one of the following:
4
- * - `dirty`: the computed signal is dirty and needs to be recomputed.
5
- * - `checked`: the computed signal is checked and needs to be recomputed.
6
- * - `computing`: the computed signal is currently being computed.
7
- * - `clean`: the computed signal is clean and does not need to be recomputed.
8
- */
9
- export type ComputedState = 'dirty' | 'checked' | 'computing' | 'clean';
@@ -1,42 +0,0 @@
1
- import { Computed } from '../models/computed';
2
-
3
- export type GlobalState = {
4
- /**
5
- * The innermost `Signal.Computed` (or effect Signal) currently being
6
- * re-evaluated as a side-effect of a `.get()` call.
7
- *
8
- * All Signal reads that occur while this is non-null will register
9
- * themselves as sources of this computed, enabling automatic dependency
10
- * tracking.
11
- *
12
- * Set to `null` when no computed is being evaluated (i.e. a 'root' read).
13
- *
14
- * @see Signal algorithms — 'Hidden global state'
15
- */
16
- computing: Computed | null;
17
- /**
18
- * Whether a callback is currently executing that requires the Signal graph
19
- * to remain unmodified for the duration of its execution.
20
- *
21
- * When `true`, any attempt to read or write a Signal will throw an exception.
22
- * This prevents the graph from being mutated while it is being traversed,
23
- * which would risk exposing inconsistent or half-processed state.
24
- *
25
- * Set to `true` immediately before invoking:
26
- * - The `notify` callback of a `Signal.subtle.Watcher` (during `State.prototype.set`)
27
- * - The `watched` callback (during `Watcher.prototype.watch`)
28
- * - The `unwatched` callback (during `Watcher.prototype.unwatch`)
29
- *
30
- * Restored to `false` in a `finally` block after each such callback returns
31
- * (or throws), guaranteeing the flag is never left permanently set.
32
- *
33
- * Note: `Signal.subtle.untrack` does NOT clear this flag — frozen is always
34
- * respected regardless of tracking context.
35
- *
36
- * @see Signal algorithms — 'Hidden global state'
37
- * @see Method: `Signal.State.prototype.set`
38
- * @see Method: `Signal.subtle.Watcher.prototype.watch`
39
- * @see Method: `Signal.subtle.Watcher.prototype.unwatch`
40
- */
41
- frozen: boolean;
42
- };
@@ -1,8 +0,0 @@
1
- import { Computed } from "../models/computed";
2
- import { State } from "../models/state";
3
-
4
- /**
5
- * A function that compares two values of type `T` and returns `true` if they are considered equal, or `false` otherwise.
6
- * This function is used to determine if a signal's value has changed and if dependent computations need to be re-evaluated.
7
- */
8
- export type SignalEqual<T> = (this: State<T> | Computed<T>, t: T, t2: T) => boolean;
@@ -1,19 +0,0 @@
1
- import { Computed } from '../models/computed';
2
- import { State } from '../models/state';
3
- import { SignalEqual } from './signal-equal.type';
4
-
5
- export type SignalOptions<T> = {
6
- /**
7
- * Custom comparison function between old and new value. Default: Object.is.
8
- * The signal is passed in as the this value for context.
9
- */
10
- equals?: SignalEqual<T>;
11
- /**
12
- * Callback called when isWatched becomes true, if it was previously false
13
- */
14
- watched?: (this: State<T> | Computed<T>) => void;
15
- /**
16
- * Callback called whenever isWatched becomes false, if it was previously true
17
- */
18
- unwatched?: (this: State<T> | Computed<T>) => void;
19
- }
@@ -1,7 +0,0 @@
1
- /**
2
- * Type of the state of a watcher.
3
- * - `waiting`: The watcher is waiting for its dependencies to change.
4
- * - `watching`: The watcher is currently watching its dependencies for changes.
5
- * - `pending`: The watcher has been notified of a change and is pending re-evaluation.
6
- */
7
- export type WatcherState = 'waiting' | 'watching' | 'pending';
package/src/public-api.ts DELETED
@@ -1 +0,0 @@
1
- export * from './lib/load-signals';
package/tsconfig.json DELETED
@@ -1,3 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- }
package/vite.config.ts DELETED
@@ -1,16 +0,0 @@
1
- import { readFileSync, writeFileSync } from 'node:fs';
2
- import { defineConfig } from 'vite';
3
- import getViteConfig from '../../vite-config';
4
-
5
- export default defineConfig(getViteConfig('@xaendar/signals', __dirname, {
6
- plugins: [
7
- {
8
- name: 'types',
9
- writeBundle() {
10
- const content = readFileSync('../packages/signals/src/globals.d.ts', 'utf-8');
11
- const dtsContent = readFileSync('../dist/@xaendar/signals/xaendar-signals.es.d.ts', 'utf-8');
12
- writeFileSync('../dist/@xaendar/signals/xaendar-signals.es.d.ts', `${content}\n\n${dtsContent}`);
13
- }
14
- }
15
- ]
16
- }));