@slimlib/store 1.6.1 → 2.0.0

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.
Files changed (57) hide show
  1. package/README.md +700 -129
  2. package/dist/index.mjs +1067 -1
  3. package/dist/index.mjs.map +1 -0
  4. package/package.json +9 -88
  5. package/src/computed.ts +232 -0
  6. package/src/core.ts +434 -0
  7. package/src/debug.ts +115 -0
  8. package/src/effect.ts +125 -0
  9. package/src/flags.ts +38 -0
  10. package/src/globals.ts +30 -0
  11. package/src/index.ts +9 -0
  12. package/src/internal-types.ts +45 -0
  13. package/src/scope.ts +85 -0
  14. package/src/signal.ts +55 -0
  15. package/src/state.ts +170 -0
  16. package/src/symbols.ts +9 -0
  17. package/src/types.ts +47 -0
  18. package/types/index.d.ts +129 -0
  19. package/types/index.d.ts.map +52 -0
  20. package/angular/package.json +0 -5
  21. package/core/package.json +0 -5
  22. package/dist/angular.cjs +0 -37
  23. package/dist/angular.d.ts +0 -23
  24. package/dist/angular.mjs +0 -33
  25. package/dist/angular.umd.js +0 -2
  26. package/dist/angular.umd.js.map +0 -1
  27. package/dist/core.cjs +0 -79
  28. package/dist/core.d.ts +0 -8
  29. package/dist/core.mjs +0 -76
  30. package/dist/index.cjs +0 -8
  31. package/dist/index.d.ts +0 -1
  32. package/dist/index.umd.js +0 -2
  33. package/dist/index.umd.js.map +0 -1
  34. package/dist/preact.cjs +0 -16
  35. package/dist/preact.d.ts +0 -3
  36. package/dist/preact.mjs +0 -13
  37. package/dist/preact.umd.js +0 -2
  38. package/dist/preact.umd.js.map +0 -1
  39. package/dist/react.cjs +0 -16
  40. package/dist/react.d.ts +0 -3
  41. package/dist/react.mjs +0 -13
  42. package/dist/react.umd.js +0 -2
  43. package/dist/react.umd.js.map +0 -1
  44. package/dist/rxjs.cjs +0 -18
  45. package/dist/rxjs.d.ts +0 -3
  46. package/dist/rxjs.mjs +0 -15
  47. package/dist/rxjs.umd.js +0 -2
  48. package/dist/rxjs.umd.js.map +0 -1
  49. package/dist/svelte.cjs +0 -7
  50. package/dist/svelte.d.ts +0 -1
  51. package/dist/svelte.mjs +0 -5
  52. package/dist/svelte.umd.js +0 -2
  53. package/dist/svelte.umd.js.map +0 -1
  54. package/preact/package.json +0 -5
  55. package/react/package.json +0 -5
  56. package/rxjs/package.json +0 -5
  57. package/svelte/package.json +0 -5
@@ -0,0 +1,232 @@
1
+ import {
2
+ checkComputedSources,
3
+ clearSources,
4
+ createSourceEntry,
5
+ currentComputing,
6
+ DepsSet,
7
+ globalVersion,
8
+ makeLive,
9
+ noopGetter,
10
+ runWithTracking,
11
+ setTracked,
12
+ tracked,
13
+ } from './core';
14
+ import { cycleMessage } from './debug';
15
+ import { Flag } from './flags';
16
+ import type { ReactiveNode, SourceEntry } from './internal-types';
17
+ import type { Computed } from './types';
18
+
19
+ /**
20
+ * Read function for computed nodes
21
+ */
22
+ export function computedRead<T>(self: ReactiveNode): T {
23
+ // ===== PULL PHASE: Register this computed as a dependency of the current consumer =====
24
+ // Track if someone is reading us
25
+ if (tracked && currentComputing !== undefined) {
26
+ // Inline tracking for computed dependencies
27
+ const consumerSources = currentComputing.$_sources;
28
+ const skipIndex = currentComputing.$_skipped;
29
+ const deps = self.$_deps;
30
+ const existing = consumerSources[skipIndex];
31
+ const noSource = existing === undefined;
32
+
33
+ if (noSource || existing.$_dependents !== deps) {
34
+ // Different dependency - clear old ones from this point and rebuild
35
+ if (!noSource) {
36
+ clearSources(currentComputing, skipIndex);
37
+ }
38
+
39
+ // Push source entry - version will be updated after source computes
40
+ // Uses shared createSourceEntry factory for V8 hidden class monomorphism
41
+ consumerSources.push(createSourceEntry(deps as DepsSet<ReactiveNode>, self, 0, undefined, undefined));
42
+
43
+ // Only register with source if we're live
44
+ if ((currentComputing.$_flags & (Flag.EFFECT | Flag.LIVE)) !== 0) {
45
+ (deps as DepsSet<ReactiveNode>).add(currentComputing);
46
+ // If source computed is not live, make it live
47
+ if ((self.$_flags & Flag.LIVE) === 0) {
48
+ makeLive(self);
49
+ }
50
+ }
51
+ }
52
+ // Mark that this node has computed sources (for version update loop optimization)
53
+ currentComputing.$_flags |= Flag.HAS_COMPUTED_SOURCE;
54
+ ++currentComputing.$_skipped;
55
+ }
56
+
57
+ let flags = self.$_flags;
58
+ const sourcesArray = self.$_sources;
59
+
60
+ // Cycle detection: if this computed is already being computed, we have a cycle
61
+ // This matches TC39 Signals proposal behavior: throw an error on cyclic reads
62
+ if ((flags & Flag.COMPUTING) !== 0) {
63
+ throw new Error(cycleMessage);
64
+ }
65
+
66
+ // ===== PULL PHASE: Check if cached value can be returned =====
67
+ // Combined check: node has cached result and doesn't need work
68
+ const hasCached = (flags & (Flag.HAS_VALUE | Flag.HAS_ERROR)) !== 0;
69
+
70
+ // biome-ignore lint/suspicious/noConfusingLabels: expected
71
+ checkCache: if ((flags & (Flag.DIRTY | Flag.CHECK)) === 0 && hasCached) {
72
+ // Fast-path: nothing has changed globally since last read
73
+ if (self.$_stamp === globalVersion) {
74
+ if ((flags & Flag.HAS_ERROR) !== 0) {
75
+ throw self.$_value;
76
+ }
77
+ return self.$_value as T;
78
+ }
79
+
80
+ // ===== PULL PHASE: Poll sources for non-live computeds =====
81
+ // Non-live nodes poll instead of receiving push notifications
82
+ if ((flags & Flag.LIVE) === 0) {
83
+ let sourceChanged = false;
84
+
85
+ // Disable tracking while polling sources to avoid unnecessary dependency tracking
86
+ const prevTracked = tracked;
87
+ // TODO: inline after it combined to a single scope?
88
+ setTracked(false);
89
+ for (let i = 0, len = sourcesArray.length; i < len; ++i) {
90
+ const source = sourcesArray[i] as SourceEntry;
91
+ const sourceNode = source.$_node; // Extract once at loop start
92
+ if (sourceNode === undefined) {
93
+ // State source - check if deps version changed
94
+ const currentDepsVersion = (source.$_dependents as DepsSet<ReactiveNode>).$_version as number;
95
+ if (source.$_version !== currentDepsVersion) {
96
+ // Deps version changed, check if actual value reverted (primitives only)
97
+ const storedValue = source.$_storedValue;
98
+ const storedType = typeof storedValue;
99
+ if (storedValue === null || (storedType !== 'object' && storedType !== 'function')) {
100
+ const currentValue = (source.$_getter as () => unknown)();
101
+ if (Object.is(currentValue, storedValue)) {
102
+ // Value reverted - update depsVersion and continue checking
103
+ source.$_version = currentDepsVersion;
104
+ continue;
105
+ }
106
+ }
107
+ // Value actually changed - mark DIRTY and skip remaining
108
+ sourceChanged = true;
109
+ break;
110
+ }
111
+ } else {
112
+ // Computed source - use sourceNode directly (already extracted above)
113
+ try {
114
+ computedRead(sourceNode);
115
+ } catch {
116
+ // Error counts as changed
117
+ sourceChanged = true;
118
+ break;
119
+ }
120
+ if (source.$_version !== sourceNode.$_version) {
121
+ sourceChanged = true;
122
+ break; // EXIT EARLY - don't process remaining sources
123
+ }
124
+ }
125
+ }
126
+ setTracked(prevTracked);
127
+
128
+ if (sourceChanged) {
129
+ // Source changed or threw - mark DIRTY and proceed to recompute
130
+ self.$_flags = flags |= Flag.DIRTY;
131
+ break checkCache;
132
+ }
133
+
134
+ // No sources changed - return cached value
135
+ self.$_stamp = globalVersion;
136
+ if ((flags & Flag.HAS_ERROR) !== 0) {
137
+ throw self.$_value;
138
+ }
139
+ return self.$_value as T;
140
+ }
141
+ }
142
+
143
+ // ===== PULL PHASE: Verify CHECK state for live computeds =====
144
+ // Live computeds receive CHECK via push - verify sources before recomputing
145
+ // Non-live computeds already verified above during polling
146
+ // Note: Check for Flag.HAS_VALUE OR Flag.HAS_ERROR since cached errors should also use this path
147
+ if ((flags & (Flag.DIRTY | Flag.CHECK | Flag.HAS_STATE_SOURCE)) === Flag.CHECK && hasCached) {
148
+ if (checkComputedSources(sourcesArray)) {
149
+ // Sources changed or errored - mark DIRTY and let getter run
150
+ flags = (flags & ~Flag.CHECK) | Flag.DIRTY;
151
+ } else {
152
+ // Sources unchanged, clear CHECK flag and return cached value
153
+ self.$_flags = flags & ~Flag.CHECK;
154
+ // No sources changed - return cached value
155
+ self.$_stamp = globalVersion;
156
+ if ((flags & Flag.HAS_ERROR) !== 0) {
157
+ throw self.$_value;
158
+ }
159
+ return self.$_value as T;
160
+ }
161
+ }
162
+
163
+ // ===== PULL PHASE: Recompute value by pulling from sources =====
164
+ // Recompute if dirty or check (sources actually changed)
165
+ if ((flags & (Flag.DIRTY | Flag.CHECK)) !== 0) {
166
+ const wasDirty = (flags & Flag.DIRTY) !== 0;
167
+
168
+ runWithTracking(self, () => {
169
+ try {
170
+ const newValue = (self.$_fn as () => T)();
171
+
172
+ // Check if value actually changed (common path: no error recovery)
173
+ const changed = (flags & Flag.HAS_VALUE) === 0 || !(self.$_equals as (a: T, b: T) => boolean)(self.$_value as T, newValue);
174
+
175
+ if (changed) {
176
+ self.$_value = newValue;
177
+ // Increment version to indicate value changed (for polling)
178
+ ++self.$_version;
179
+ self.$_flags = (self.$_flags | Flag.HAS_VALUE) & ~Flag.HAS_ERROR;
180
+ // ===== PUSH PHASE (during pull): Mark CHECK-only dependents as DIRTY =====
181
+ // When value changes during recomputation, upgrade dependent CHECK flags to DIRTY
182
+ for (const dep of self.$_deps) {
183
+ const depFlags = dep.$_flags;
184
+ if ((depFlags & (Flag.COMPUTING | Flag.DIRTY | Flag.CHECK)) === Flag.CHECK) {
185
+ dep.$_flags = depFlags | Flag.DIRTY;
186
+ }
187
+ }
188
+ } else if (wasDirty) {
189
+ self.$_flags |= Flag.HAS_VALUE;
190
+ }
191
+
192
+ // Update last seen global version
193
+ self.$_stamp = globalVersion;
194
+ } catch (e) {
195
+ // Per TC39 Signals proposal: cache the error and mark as clean with error flag
196
+ // The error will be rethrown on subsequent reads until a dependency changes
197
+ // Reuse valueSymbol for error storage since a computed can't have both value and error
198
+ // Increment version since the result changed (to error)
199
+ ++self.$_version;
200
+ self.$_value = e as T;
201
+ self.$_flags = (self.$_flags & ~Flag.HAS_VALUE) | Flag.HAS_ERROR;
202
+ self.$_stamp = globalVersion;
203
+ }
204
+ });
205
+ }
206
+
207
+ // Check if we have a cached error to rethrow (stored in valueSymbol)
208
+ if ((self.$_flags & Flag.HAS_ERROR) !== 0) {
209
+ throw self.$_value;
210
+ }
211
+
212
+ return self.$_value as T;
213
+ }
214
+
215
+ /**
216
+ * Creates a computed value that automatically tracks dependencies and caches results
217
+ */
218
+ export const computed = <T>(getter: () => T, equals: (a: T, b: T) => boolean = Object.is): Computed<T> => {
219
+ const node = {
220
+ $_sources: [],
221
+ $_deps: new DepsSet<ReactiveNode>(noopGetter),
222
+ $_flags: Flag.DIRTY,
223
+ $_skipped: 0,
224
+ $_version: 0,
225
+ $_value: undefined,
226
+ $_stamp: 0,
227
+ $_fn: getter,
228
+ $_equals: equals,
229
+ } as ReactiveNode;
230
+
231
+ return () => computedRead(node);
232
+ };
package/src/core.ts ADDED
@@ -0,0 +1,434 @@
1
+ /**
2
+ * Core reactive system implementation using push/pull algorithm:
3
+ *
4
+ * PUSH PHASE: When a source value changes (signal/state write):
5
+ * - Increment globalVersion
6
+ * - Eagerly propagate CHECK flags to all dependents
7
+ * - Schedule effects for execution
8
+ *
9
+ * PULL PHASE: When a computed/effect is read:
10
+ * - Check if recomputation is needed (via flags and source versions)
11
+ * - Lazily recompute by pulling values from sources
12
+ * - Update cached value and version
13
+ */
14
+
15
+ import { computedRead } from './computed';
16
+ import { Flag } from './flags';
17
+ import { scheduler } from './globals';
18
+ import { unwrap } from './symbols';
19
+ import type { ReactiveNode, SourceEntry } from './internal-types';
20
+
21
+ /**
22
+ * No-op getter used as default for DepsSet.$_getter to ensure all DepsSet
23
+ * instances have the same field representation (function, never undefined).
24
+ * This avoids V8 field representation deopts when some DepsSets have a getter
25
+ * and others don't.
26
+ */
27
+ /* v8 ignore next -- @preserve */
28
+ export const noopGetter = (): unknown => undefined;
29
+
30
+ /**
31
+ * DepsSet extends Set with $_version and $_getter properties for dependency
32
+ * tracking. Using a class ensures all instances share the same V8 hidden class,
33
+ * avoiding hidden class transitions and field constness changes.
34
+ *
35
+ * IMPORTANT: $_getter always defaults to a no-op function (never undefined)
36
+ * so that all DepsSet instances share the same V8 field representation.
37
+ * This prevents "dependent field representation changed" deopts.
38
+ */
39
+ export class DepsSet<T> extends Set<T> {
40
+ $_version = 0;
41
+ $_getter: () => unknown;
42
+ constructor(getter: () => unknown) {
43
+ super();
44
+ this.$_getter = getter;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Factory for creating source entries (unified allocation site).
50
+ *
51
+ * Both state and computed source entries MUST be created through this single
52
+ * factory to ensure they share the same V8 hidden class. Using separate
53
+ * object literals in different functions creates different allocation sites,
54
+ * causing V8 to assign different hidden classes and making all property
55
+ * accesses on source entries polymorphic.
56
+ *
57
+ * @param dependents - The DepsSet tracking dependents of this source
58
+ * @param node - The source ReactiveNode (undefined for state/signal sources)
59
+ * @param version - Initial version number
60
+ * @param getter - Value getter (undefined for computed sources)
61
+ * @param storedValue - Cached value (undefined for computed sources)
62
+ */
63
+ export const createSourceEntry = (
64
+ dependents: DepsSet<ReactiveNode>,
65
+ node: ReactiveNode | undefined,
66
+ version: number,
67
+ getter: undefined | (() => unknown),
68
+ storedValue: unknown
69
+ ): SourceEntry => ({
70
+ $_dependents: dependents,
71
+ $_node: node,
72
+ $_version: version,
73
+ $_getter: getter,
74
+ $_storedValue: storedValue,
75
+ });
76
+
77
+ let flushScheduled = false;
78
+
79
+ /**
80
+ * Global version counter - increments on every signal/state write
81
+ * Used for fast-path: if globalVersion hasn't changed since last read, skip all checks
82
+ */
83
+ export let globalVersion = 1;
84
+
85
+ export const batched: ReactiveNode[] = [];
86
+
87
+ let lastAddedId = 0;
88
+
89
+ let needsSort = false;
90
+
91
+ // Computation tracking state
92
+ export let currentComputing: ReactiveNode | undefined;
93
+ export let tracked = true;
94
+
95
+ /**
96
+ * Set the tracked state for dependency tracking (internal use only)
97
+ */
98
+ export const setTracked = (value: boolean) => {
99
+ tracked = value;
100
+ };
101
+
102
+ /**
103
+ * Add an effect to the batched set
104
+ * PUSH PHASE: Part of effect scheduling during dirty propagation
105
+ * Caller must check Flag.NEEDS_WORK before calling to avoid duplicates
106
+ */
107
+ export const batchedAdd = (node: ReactiveNode): void => {
108
+ const nodeId = node.$_stamp;
109
+ // Track if we're adding out of order
110
+ if (nodeId < lastAddedId) {
111
+ needsSort = true;
112
+ }
113
+ lastAddedId = nodeId;
114
+ batched.push(node);
115
+ };
116
+
117
+ /**
118
+ * Add a newly created effect to the batched set
119
+ * Used during effect creation - new effects always have the highest ID
120
+ * so we unconditionally update lastAddedId without checking order
121
+ */
122
+ export const batchedAddNew = (node: ReactiveNode, effectId: number): void => {
123
+ lastAddedId = effectId;
124
+ batched.push(node);
125
+ };
126
+
127
+ /**
128
+ * Unwraps a proxied value to get the underlying object
129
+ * (Utility - not specific to push/pull phases)
130
+ */
131
+ export const unwrapValue = <T>(value: T): T =>
132
+ (value !== null &&
133
+ (typeof value === 'object' || typeof value === 'function') &&
134
+ ((value as unknown as Record<symbol, unknown>)[unwrap] as T)) ||
135
+ value;
136
+
137
+ /**
138
+ * Make a computed live - register it with all its sources
139
+ * Called when a live consumer starts reading this computed
140
+ * PUSH PHASE: Enables push notifications to flow through this node
141
+ */
142
+ export const makeLive = (node: ReactiveNode): void => {
143
+ node.$_flags |= Flag.LIVE;
144
+ const nodeSources = node.$_sources;
145
+ for (let i = 0, len = nodeSources.length; i < len; ++i) {
146
+ const { $_dependents, $_node: sourceNode } = nodeSources[i] as SourceEntry;
147
+ $_dependents.add(node);
148
+ if (sourceNode !== undefined && (sourceNode.$_flags & (Flag.EFFECT | Flag.LIVE)) === 0) {
149
+ makeLive(sourceNode);
150
+ }
151
+ }
152
+ };
153
+
154
+ /**
155
+ * Make a computed non-live - unregister it from all its sources
156
+ * Called when all live consumers stop depending on this computed
157
+ * PUSH PHASE: Disables push notifications through this node
158
+ */
159
+ export const makeNonLive = (node: ReactiveNode): void => {
160
+ node.$_flags &= ~Flag.LIVE;
161
+ const nodeSources = node.$_sources;
162
+ for (let i = 0, len = nodeSources.length; i < len; ++i) {
163
+ const { $_dependents, $_node: sourceNode } = nodeSources[i] as SourceEntry;
164
+ $_dependents.delete(node);
165
+ // Check: has Flag.LIVE but not Flag.EFFECT (effects never become non-live)
166
+ if (
167
+ sourceNode !== undefined &&
168
+ (sourceNode.$_flags & (Flag.EFFECT | Flag.LIVE)) === Flag.LIVE &&
169
+ (sourceNode.$_deps as Set<ReactiveNode>).size === 0
170
+ ) {
171
+ makeNonLive(sourceNode);
172
+ }
173
+ }
174
+ };
175
+
176
+ /**
177
+ * Clear sources for a node starting from a specific index
178
+ * PULL PHASE: Cleanup during dependency tracking when sources change
179
+ */
180
+ export const clearSources = (node: ReactiveNode, fromIndex = 0): void => {
181
+ const isLive = (node.$_flags & (Flag.EFFECT | Flag.LIVE)) !== 0;
182
+ const nodeSources = node.$_sources;
183
+
184
+ for (let i = fromIndex, len = nodeSources.length; i < len; ++i) {
185
+ const { $_dependents, $_node: sourceNode } = nodeSources[i] as SourceEntry;
186
+
187
+ // Check if this deps is retained (exists in kept portion) - avoid removing shared deps
188
+ let retained = false;
189
+ for (let j = 0; j < fromIndex && !retained; ++j) {
190
+ retained = (nodeSources[j] as SourceEntry).$_dependents === $_dependents;
191
+ }
192
+
193
+ if (!retained) {
194
+ // Always remove from deps to prevent stale notifications
195
+ $_dependents.delete(node);
196
+ // If source is a computed and we're live, check if it became non-live
197
+ if (
198
+ isLive &&
199
+ sourceNode !== undefined &&
200
+ (sourceNode.$_flags & (Flag.EFFECT | Flag.LIVE)) === Flag.LIVE &&
201
+ (sourceNode.$_deps as Set<ReactiveNode>).size === 0
202
+ ) {
203
+ makeNonLive(sourceNode);
204
+ }
205
+ }
206
+ }
207
+ nodeSources.length = fromIndex;
208
+ };
209
+
210
+ /**
211
+ * Execute all pending effects immediately
212
+ * This function can be called to manually trigger all scheduled effects
213
+ * before the next microtask
214
+ * PULL PHASE: Executes batched effects, each effect pulls its dependencies
215
+ */
216
+ export const flushEffects = (): void => {
217
+ flushScheduled = false;
218
+ // Collect nodes, only sort if effects were added out of order
219
+ // Use a copy of the array for execution to allow re-scheduling
220
+ const nodes = batched.slice();
221
+ batched.length = 0;
222
+
223
+ if (needsSort) {
224
+ nodes.sort((a, b) => a.$_stamp - b.$_stamp);
225
+ }
226
+
227
+ lastAddedId = 0;
228
+ needsSort = false;
229
+
230
+ // Call $_fn on each node instead of calling nodes directly as functions
231
+ // This enables effect nodes to be plain objects (same hidden class as computed)
232
+ for (let i = 0, len = nodes.length; i < len; ++i) {
233
+ const node = nodes[i] as ReactiveNode;
234
+ try {
235
+ node.$_fn?.();
236
+ } catch (e) {
237
+ console.error(e);
238
+ }
239
+ }
240
+ };
241
+
242
+ /**
243
+ * Schedule flush via scheduler (default: microtask)
244
+ * PUSH PHASE: Schedules the transition from push to pull phase
245
+ */
246
+ export const scheduleFlush = (): void => {
247
+ if (!flushScheduled) {
248
+ flushScheduled = true;
249
+ scheduler(flushEffects);
250
+ }
251
+ };
252
+
253
+ /**
254
+ * Track a state/signal dependency between currentComputing and a deps Set
255
+ * Uses liveness tracking - only live consumers register with sources
256
+ * PULL PHASE: Records dependencies during computation for future invalidation
257
+ */
258
+ export const trackStateDependency = <T>(deps: DepsSet<ReactiveNode>, valueGetter: () => T, cachedValue: T): void => {
259
+ // Callers guarantee tracked && currentComputing are true
260
+
261
+ const sourcesArray = (currentComputing as ReactiveNode).$_sources;
262
+ const skipIndex = (currentComputing as ReactiveNode).$_skipped;
263
+ const existing = sourcesArray[skipIndex];
264
+ const noSource = existing === undefined;
265
+
266
+ if (noSource || existing.$_dependents !== deps) {
267
+ // Different dependency - clear old ones from this point and rebuild
268
+ if (!noSource) {
269
+ clearSources(currentComputing as ReactiveNode, skipIndex);
270
+ }
271
+
272
+ // Track deps version, value getter, and last seen value for polling
273
+ // Uses shared createSourceEntry factory for V8 hidden class monomorphism
274
+ sourcesArray.push(
275
+ createSourceEntry(deps, undefined, (deps as DepsSet<ReactiveNode>).$_version as number, valueGetter, cachedValue)
276
+ );
277
+
278
+ // Mark that this node has state/signal sources (for polling optimization)
279
+ (currentComputing as ReactiveNode).$_flags |= Flag.HAS_STATE_SOURCE;
280
+
281
+ // Only register with source if we're live
282
+ if (((currentComputing as ReactiveNode).$_flags & (Flag.EFFECT | Flag.LIVE)) !== 0) {
283
+ deps.add(currentComputing as ReactiveNode);
284
+ }
285
+ } else {
286
+ // Same state source - update depsVersion, getter, and storedValue for accurate polling
287
+ const entry = sourcesArray[skipIndex] as SourceEntry;
288
+ entry.$_version = (deps as DepsSet<ReactiveNode>).$_version as number;
289
+ entry.$_getter = valueGetter;
290
+ entry.$_storedValue = cachedValue;
291
+ // Re-set Flag.HAS_STATE_SOURCE (may have been cleared by runWithTracking)
292
+ (currentComputing as ReactiveNode).$_flags |= Flag.HAS_STATE_SOURCE;
293
+ }
294
+ ++(currentComputing as ReactiveNode).$_skipped;
295
+ };
296
+
297
+ /**
298
+ * Mark a node as needing check (eager propagation with equality cutoff)
299
+ * PUSH PHASE: Eagerly propagates CHECK flag up the dependency graph
300
+ */
301
+ export const markNeedsCheck = (node: ReactiveNode): void => {
302
+ const flags = node.$_flags;
303
+ // Fast path: skip if already computing, dirty, or marked CHECK
304
+ // (COMPUTING | DIRTY | CHECK) (bits 0, 1, 2)
305
+ if ((flags & (Flag.COMPUTING | Flag.DIRTY | Flag.CHECK)) !== 0) {
306
+ // Exception: computing effect that's not dirty yet needs to be marked dirty
307
+ // Uses combined mask: (flags & 13) === 12 means COMPUTING + EFFECT set, DIRTY not set
308
+ if ((flags & (Flag.COMPUTING | Flag.EFFECT | Flag.DIRTY)) === (Flag.COMPUTING | Flag.EFFECT)) {
309
+ node.$_flags = flags | Flag.DIRTY;
310
+ batchedAdd(node);
311
+ scheduleFlush();
312
+ }
313
+ return;
314
+ }
315
+ // Not skipped: set CHECK and propagate to dependents
316
+ node.$_flags = flags | Flag.CHECK;
317
+ if ((flags & Flag.EFFECT) !== 0) {
318
+ batchedAdd(node);
319
+ scheduleFlush();
320
+ }
321
+ for (const dep of node.$_deps) {
322
+ markNeedsCheck(dep);
323
+ }
324
+ };
325
+
326
+ /**
327
+ * Mark all dependents in a Set as needing check
328
+ * PUSH PHASE: Entry point for push propagation when a source value changes
329
+ */
330
+ export const markDependents = (deps: DepsSet<ReactiveNode>): void => {
331
+ ++globalVersion;
332
+ // Increment deps version for non-live computed polling
333
+ ++deps.$_version;
334
+ for (const dep of deps) {
335
+ markNeedsCheck(dep);
336
+ }
337
+ };
338
+
339
+ /**
340
+ * Run a getter function with dependency tracking
341
+ * Handles cycle detection, context setup, and source cleanup
342
+ * PULL PHASE: Core of pull - executes computation while tracking dependencies
343
+ */
344
+ export const runWithTracking = <T>(node: ReactiveNode, getter: () => T): T => {
345
+ // Clear (DIRTY | CHECK) and source flags (will be recalculated during tracking)
346
+ // Note: Even when called from checkComputedSources (which sets tracked=false), this works
347
+ // because runWithTracking sets tracked=true, so trackDependency will re-set the flag
348
+ node.$_flags = (node.$_flags & ~(Flag.DIRTY | Flag.CHECK | Flag.HAS_STATE_SOURCE | Flag.HAS_COMPUTED_SOURCE)) | Flag.COMPUTING;
349
+ node.$_skipped = 0;
350
+
351
+ const prev = currentComputing;
352
+ const prevTracked = tracked;
353
+ currentComputing = node;
354
+ tracked = true;
355
+
356
+ try {
357
+ return getter();
358
+ } finally {
359
+ currentComputing = prev;
360
+ tracked = prevTracked;
361
+ // biome-ignore lint/suspicious/noAssignInExpressions: optimization
362
+ const flags = (node.$_flags &= ~Flag.COMPUTING);
363
+ const nodeSources = node.$_sources;
364
+ const skipped = node.$_skipped;
365
+ const nodeSourcesLength = nodeSources.length;
366
+ // Only update versions if there are computed sources (state sources update inline)
367
+ if ((flags & Flag.HAS_COMPUTED_SOURCE) !== 0) {
368
+ const updateLen = Math.min(skipped, nodeSourcesLength);
369
+ for (let i = 0; i < updateLen; ++i) {
370
+ const entry = nodeSources[i] as SourceEntry;
371
+ const entryNode = entry.$_node;
372
+ if (entryNode !== undefined) {
373
+ entry.$_version = entryNode.$_version;
374
+ }
375
+ }
376
+ }
377
+ // Clean up any excess sources that weren't reused
378
+ if (nodeSourcesLength > skipped) {
379
+ clearSources(node, skipped);
380
+ }
381
+ }
382
+ };
383
+
384
+ /**
385
+ * Execute a callback without tracking any reactive dependencies
386
+ * Useful when reading signals/state without creating a dependency relationship
387
+ * PULL PHASE: Temporarily disables dependency tracking during pull
388
+ */
389
+ export const untracked = <T>(callback: () => T): T => {
390
+ const prevTracked = tracked;
391
+ tracked = false;
392
+ try {
393
+ return callback();
394
+ } finally {
395
+ tracked = prevTracked;
396
+ }
397
+ };
398
+
399
+ /**
400
+ * Check if any computed sources have changed or errored.
401
+ * Used by CHECK path optimization in computed and effect.
402
+ * PULL PHASE: Verifies if sources actually changed before recomputing (equality cutoff)
403
+ *
404
+ * Note: Callers must check HAS_STATE_SOURCE flag before calling this function.
405
+ * This function assumes all sources are computed (have $_node).
406
+ *
407
+ * @param sourcesArray - The sources to check (must all be computed sources)
408
+ * @returns true if sources changed, false if unchanged
409
+ */
410
+ export const checkComputedSources = (sourcesArray: SourceEntry[]): boolean => {
411
+ const prevTracked = tracked;
412
+ tracked = false;
413
+ const len = sourcesArray.length;
414
+ for (let i = 0; i < len; ++i) {
415
+ const sourceEntry = sourcesArray[i] as SourceEntry;
416
+ const sourceNode = sourceEntry.$_node as ReactiveNode;
417
+ // Access source to trigger its recomputation if needed
418
+ try {
419
+ computedRead(sourceNode);
420
+ } catch {
421
+ // Error counts as changed - caller will recompute and may handle differently
422
+ tracked = prevTracked;
423
+ return true;
424
+ }
425
+ // Check if source version changed (meaning its value changed)
426
+ // Early exit - runWithTracking will update all versions during recomputation
427
+ if (sourceEntry.$_version !== sourceNode.$_version) {
428
+ tracked = prevTracked;
429
+ return true;
430
+ }
431
+ }
432
+ tracked = prevTracked;
433
+ return false;
434
+ };