@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
package/dist/index.mjs CHANGED
@@ -1 +1,1067 @@
1
- export { createStoreFactory, unwrapValue } from './core.mjs';
1
+ import { DEV } from 'esm-env';
2
+
3
+ /**
4
+ * Active scope for effect tracking
5
+ * When set, effects created will be tracked to this scope
6
+ * Can be set via setActiveScope() or automatically during scope() callbacks
7
+ */
8
+ let activeScope;
9
+ /**
10
+ * Set the active scope for effect tracking
11
+ * Effects created outside of a scope() callback will be tracked to this scope
12
+ * Pass undefined to clear the active scope
13
+ */
14
+ const setActiveScope = (scope) => {
15
+ activeScope = scope;
16
+ };
17
+ /**
18
+ * Scheduler function used to schedule effect execution
19
+ * Defaults to queueMicrotask, can be replaced with setScheduler
20
+ */
21
+ let scheduler = queueMicrotask;
22
+ /**
23
+ * Set a custom scheduler function for effect execution
24
+ */
25
+ const setScheduler = (newScheduler) => {
26
+ scheduler = newScheduler;
27
+ };
28
+
29
+ // Symbols for objects that ARE exposed to users
30
+ // These must remain symbols to avoid leaking internal implementation details
31
+ const [unwrap, propertyDepsSymbol, trackSymbol, childrenSymbol,
32
+ // biome-ignore lint/suspicious/noSparseArray: fine
33
+ ] = Array.from([, , , ,], Symbol);
34
+
35
+ /**
36
+ * Core reactive system implementation using push/pull algorithm:
37
+ *
38
+ * PUSH PHASE: When a source value changes (signal/state write):
39
+ * - Increment globalVersion
40
+ * - Eagerly propagate CHECK flags to all dependents
41
+ * - Schedule effects for execution
42
+ *
43
+ * PULL PHASE: When a computed/effect is read:
44
+ * - Check if recomputation is needed (via flags and source versions)
45
+ * - Lazily recompute by pulling values from sources
46
+ * - Update cached value and version
47
+ */
48
+ /**
49
+ * No-op getter used as default for DepsSet.$_getter to ensure all DepsSet
50
+ * instances have the same field representation (function, never undefined).
51
+ * This avoids V8 field representation deopts when some DepsSets have a getter
52
+ * and others don't.
53
+ */
54
+ /* v8 ignore next -- @preserve */
55
+ const noopGetter = () => undefined;
56
+ /**
57
+ * DepsSet extends Set with $_version and $_getter properties for dependency
58
+ * tracking. Using a class ensures all instances share the same V8 hidden class,
59
+ * avoiding hidden class transitions and field constness changes.
60
+ *
61
+ * IMPORTANT: $_getter always defaults to a no-op function (never undefined)
62
+ * so that all DepsSet instances share the same V8 field representation.
63
+ * This prevents "dependent field representation changed" deopts.
64
+ */
65
+ class DepsSet extends Set {
66
+ a = 0;
67
+ b;
68
+ constructor(getter) {
69
+ super();
70
+ this.b = getter;
71
+ }
72
+ }
73
+ /**
74
+ * Factory for creating source entries (unified allocation site).
75
+ *
76
+ * Both state and computed source entries MUST be created through this single
77
+ * factory to ensure they share the same V8 hidden class. Using separate
78
+ * object literals in different functions creates different allocation sites,
79
+ * causing V8 to assign different hidden classes and making all property
80
+ * accesses on source entries polymorphic.
81
+ *
82
+ * @param dependents - The DepsSet tracking dependents of this source
83
+ * @param node - The source ReactiveNode (undefined for state/signal sources)
84
+ * @param version - Initial version number
85
+ * @param getter - Value getter (undefined for computed sources)
86
+ * @param storedValue - Cached value (undefined for computed sources)
87
+ */
88
+ const createSourceEntry = (dependents, node, version, getter, storedValue) => ({
89
+ c: dependents,
90
+ d: node,
91
+ a: version,
92
+ b: getter,
93
+ e: storedValue,
94
+ });
95
+ let flushScheduled = false;
96
+ /**
97
+ * Global version counter - increments on every signal/state write
98
+ * Used for fast-path: if globalVersion hasn't changed since last read, skip all checks
99
+ */
100
+ let globalVersion = 1;
101
+ const batched = [];
102
+ let lastAddedId = 0;
103
+ let needsSort = false;
104
+ // Computation tracking state
105
+ let currentComputing;
106
+ let tracked = true;
107
+ /**
108
+ * Set the tracked state for dependency tracking (internal use only)
109
+ */
110
+ const setTracked = (value) => {
111
+ tracked = value;
112
+ };
113
+ /**
114
+ * Add an effect to the batched set
115
+ * PUSH PHASE: Part of effect scheduling during dirty propagation
116
+ * Caller must check Flag.NEEDS_WORK before calling to avoid duplicates
117
+ */
118
+ const batchedAdd = (node) => {
119
+ const nodeId = node.f;
120
+ // Track if we're adding out of order
121
+ if (nodeId < lastAddedId) {
122
+ needsSort = true;
123
+ }
124
+ lastAddedId = nodeId;
125
+ batched.push(node);
126
+ };
127
+ /**
128
+ * Add a newly created effect to the batched set
129
+ * Used during effect creation - new effects always have the highest ID
130
+ * so we unconditionally update lastAddedId without checking order
131
+ */
132
+ const batchedAddNew = (node, effectId) => {
133
+ lastAddedId = effectId;
134
+ batched.push(node);
135
+ };
136
+ /**
137
+ * Unwraps a proxied value to get the underlying object
138
+ * (Utility - not specific to push/pull phases)
139
+ */
140
+ const unwrapValue = (value) => (value !== null &&
141
+ (typeof value === 'object' || typeof value === 'function') &&
142
+ value[unwrap]) ||
143
+ value;
144
+ /**
145
+ * Make a computed live - register it with all its sources
146
+ * Called when a live consumer starts reading this computed
147
+ * PUSH PHASE: Enables push notifications to flow through this node
148
+ */
149
+ const makeLive = (node) => {
150
+ node.g |= 64 /* Flag.LIVE */;
151
+ const nodeSources = node.h;
152
+ for (let i = 0, len = nodeSources.length; i < len; ++i) {
153
+ const { c: c, d: sourceNode } = nodeSources[i];
154
+ c.add(node);
155
+ if (sourceNode !== undefined && (sourceNode.g & (8 /* Flag.EFFECT */ | 64 /* Flag.LIVE */)) === 0) {
156
+ makeLive(sourceNode);
157
+ }
158
+ }
159
+ };
160
+ /**
161
+ * Make a computed non-live - unregister it from all its sources
162
+ * Called when all live consumers stop depending on this computed
163
+ * PUSH PHASE: Disables push notifications through this node
164
+ */
165
+ const makeNonLive = (node) => {
166
+ node.g &= -65 /* Flag.LIVE */;
167
+ const nodeSources = node.h;
168
+ for (let i = 0, len = nodeSources.length; i < len; ++i) {
169
+ const { c: c, d: sourceNode } = nodeSources[i];
170
+ c.delete(node);
171
+ // Check: has Flag.LIVE but not Flag.EFFECT (effects never become non-live)
172
+ if (sourceNode !== undefined &&
173
+ (sourceNode.g & (8 /* Flag.EFFECT */ | 64 /* Flag.LIVE */)) === 64 /* Flag.LIVE */ &&
174
+ sourceNode.i.size === 0) {
175
+ makeNonLive(sourceNode);
176
+ }
177
+ }
178
+ };
179
+ /**
180
+ * Clear sources for a node starting from a specific index
181
+ * PULL PHASE: Cleanup during dependency tracking when sources change
182
+ */
183
+ const clearSources = (node, fromIndex = 0) => {
184
+ const isLive = (node.g & (8 /* Flag.EFFECT */ | 64 /* Flag.LIVE */)) !== 0;
185
+ const nodeSources = node.h;
186
+ for (let i = fromIndex, len = nodeSources.length; i < len; ++i) {
187
+ const { c: c, d: sourceNode } = nodeSources[i];
188
+ // Check if this deps is retained (exists in kept portion) - avoid removing shared deps
189
+ let retained = false;
190
+ for (let j = 0; j < fromIndex && !retained; ++j) {
191
+ retained = nodeSources[j].c === c;
192
+ }
193
+ if (!retained) {
194
+ // Always remove from deps to prevent stale notifications
195
+ c.delete(node);
196
+ // If source is a computed and we're live, check if it became non-live
197
+ if (isLive &&
198
+ sourceNode !== undefined &&
199
+ (sourceNode.g & (8 /* Flag.EFFECT */ | 64 /* Flag.LIVE */)) === 64 /* Flag.LIVE */ &&
200
+ sourceNode.i.size === 0) {
201
+ makeNonLive(sourceNode);
202
+ }
203
+ }
204
+ }
205
+ nodeSources.length = fromIndex;
206
+ };
207
+ /**
208
+ * Execute all pending effects immediately
209
+ * This function can be called to manually trigger all scheduled effects
210
+ * before the next microtask
211
+ * PULL PHASE: Executes batched effects, each effect pulls its dependencies
212
+ */
213
+ const flushEffects = () => {
214
+ flushScheduled = false;
215
+ // Collect nodes, only sort if effects were added out of order
216
+ // Use a copy of the array for execution to allow re-scheduling
217
+ const nodes = batched.slice();
218
+ batched.length = 0;
219
+ if (needsSort) {
220
+ nodes.sort((a, b) => a.f - b.f);
221
+ }
222
+ lastAddedId = 0;
223
+ needsSort = false;
224
+ // Call $_fn on each node instead of calling nodes directly as functions
225
+ // This enables effect nodes to be plain objects (same hidden class as computed)
226
+ for (let i = 0, len = nodes.length; i < len; ++i) {
227
+ const node = nodes[i];
228
+ try {
229
+ node.j?.();
230
+ }
231
+ catch (e) {
232
+ console.error(e);
233
+ }
234
+ }
235
+ };
236
+ /**
237
+ * Schedule flush via scheduler (default: microtask)
238
+ * PUSH PHASE: Schedules the transition from push to pull phase
239
+ */
240
+ const scheduleFlush = () => {
241
+ if (!flushScheduled) {
242
+ flushScheduled = true;
243
+ scheduler(flushEffects);
244
+ }
245
+ };
246
+ /**
247
+ * Track a state/signal dependency between currentComputing and a deps Set
248
+ * Uses liveness tracking - only live consumers register with sources
249
+ * PULL PHASE: Records dependencies during computation for future invalidation
250
+ */
251
+ const trackStateDependency = (deps, valueGetter, cachedValue) => {
252
+ // Callers guarantee tracked && currentComputing are true
253
+ const sourcesArray = currentComputing.h;
254
+ const skipIndex = currentComputing.k;
255
+ const existing = sourcesArray[skipIndex];
256
+ const noSource = existing === undefined;
257
+ if (noSource || existing.c !== deps) {
258
+ // Different dependency - clear old ones from this point and rebuild
259
+ if (!noSource) {
260
+ clearSources(currentComputing, skipIndex);
261
+ }
262
+ // Track deps version, value getter, and last seen value for polling
263
+ // Uses shared createSourceEntry factory for V8 hidden class monomorphism
264
+ sourcesArray.push(createSourceEntry(deps, undefined, deps.a, valueGetter, cachedValue));
265
+ // Mark that this node has state/signal sources (for polling optimization)
266
+ currentComputing.g |= 128 /* Flag.HAS_STATE_SOURCE */;
267
+ // Only register with source if we're live
268
+ if ((currentComputing.g & (8 /* Flag.EFFECT */ | 64 /* Flag.LIVE */)) !== 0) {
269
+ deps.add(currentComputing);
270
+ }
271
+ }
272
+ else {
273
+ // Same state source - update depsVersion, getter, and storedValue for accurate polling
274
+ const entry = sourcesArray[skipIndex];
275
+ entry.a = deps.a;
276
+ entry.b = valueGetter;
277
+ entry.e = cachedValue;
278
+ // Re-set Flag.HAS_STATE_SOURCE (may have been cleared by runWithTracking)
279
+ currentComputing.g |= 128 /* Flag.HAS_STATE_SOURCE */;
280
+ }
281
+ ++currentComputing.k;
282
+ };
283
+ /**
284
+ * Mark a node as needing check (eager propagation with equality cutoff)
285
+ * PUSH PHASE: Eagerly propagates CHECK flag up the dependency graph
286
+ */
287
+ const markNeedsCheck = (node) => {
288
+ const flags = node.g;
289
+ // Fast path: skip if already computing, dirty, or marked CHECK
290
+ // (COMPUTING | DIRTY | CHECK) (bits 0, 1, 2)
291
+ if ((flags & (4 /* Flag.COMPUTING */ | 1 /* Flag.DIRTY */ | 2 /* Flag.CHECK */)) !== 0) {
292
+ // Exception: computing effect that's not dirty yet needs to be marked dirty
293
+ // Uses combined mask: (flags & 13) === 12 means COMPUTING + EFFECT set, DIRTY not set
294
+ if ((flags & (4 /* Flag.COMPUTING */ | 8 /* Flag.EFFECT */ | 1 /* Flag.DIRTY */)) === (4 /* Flag.COMPUTING */ | 8 /* Flag.EFFECT */)) {
295
+ node.g = flags | 1 /* Flag.DIRTY */;
296
+ batchedAdd(node);
297
+ scheduleFlush();
298
+ }
299
+ return;
300
+ }
301
+ // Not skipped: set CHECK and propagate to dependents
302
+ node.g = flags | 2 /* Flag.CHECK */;
303
+ if ((flags & 8 /* Flag.EFFECT */) !== 0) {
304
+ batchedAdd(node);
305
+ scheduleFlush();
306
+ }
307
+ for (const dep of node.i) {
308
+ markNeedsCheck(dep);
309
+ }
310
+ };
311
+ /**
312
+ * Mark all dependents in a Set as needing check
313
+ * PUSH PHASE: Entry point for push propagation when a source value changes
314
+ */
315
+ const markDependents = (deps) => {
316
+ ++globalVersion;
317
+ // Increment deps version for non-live computed polling
318
+ ++deps.a;
319
+ for (const dep of deps) {
320
+ markNeedsCheck(dep);
321
+ }
322
+ };
323
+ /**
324
+ * Run a getter function with dependency tracking
325
+ * Handles cycle detection, context setup, and source cleanup
326
+ * PULL PHASE: Core of pull - executes computation while tracking dependencies
327
+ */
328
+ const runWithTracking = (node, getter) => {
329
+ // Clear (DIRTY | CHECK) and source flags (will be recalculated during tracking)
330
+ // Note: Even when called from checkComputedSources (which sets tracked=false), this works
331
+ // because runWithTracking sets tracked=true, so trackDependency will re-set the flag
332
+ node.g = (node.g & -388) | 4 /* Flag.COMPUTING */;
333
+ node.k = 0;
334
+ const prev = currentComputing;
335
+ const prevTracked = tracked;
336
+ currentComputing = node;
337
+ tracked = true;
338
+ try {
339
+ return getter();
340
+ }
341
+ finally {
342
+ currentComputing = prev;
343
+ tracked = prevTracked;
344
+ // biome-ignore lint/suspicious/noAssignInExpressions: optimization
345
+ const flags = (node.g &= -5 /* Flag.COMPUTING */);
346
+ const nodeSources = node.h;
347
+ const skipped = node.k;
348
+ const nodeSourcesLength = nodeSources.length;
349
+ // Only update versions if there are computed sources (state sources update inline)
350
+ if ((flags & 256 /* Flag.HAS_COMPUTED_SOURCE */) !== 0) {
351
+ const updateLen = Math.min(skipped, nodeSourcesLength);
352
+ for (let i = 0; i < updateLen; ++i) {
353
+ const entry = nodeSources[i];
354
+ const entryNode = entry.d;
355
+ if (entryNode !== undefined) {
356
+ entry.a = entryNode.a;
357
+ }
358
+ }
359
+ }
360
+ // Clean up any excess sources that weren't reused
361
+ if (nodeSourcesLength > skipped) {
362
+ clearSources(node, skipped);
363
+ }
364
+ }
365
+ };
366
+ /**
367
+ * Execute a callback without tracking any reactive dependencies
368
+ * Useful when reading signals/state without creating a dependency relationship
369
+ * PULL PHASE: Temporarily disables dependency tracking during pull
370
+ */
371
+ const untracked = (callback) => {
372
+ const prevTracked = tracked;
373
+ tracked = false;
374
+ try {
375
+ return callback();
376
+ }
377
+ finally {
378
+ tracked = prevTracked;
379
+ }
380
+ };
381
+ /**
382
+ * Check if any computed sources have changed or errored.
383
+ * Used by CHECK path optimization in computed and effect.
384
+ * PULL PHASE: Verifies if sources actually changed before recomputing (equality cutoff)
385
+ *
386
+ * Note: Callers must check HAS_STATE_SOURCE flag before calling this function.
387
+ * This function assumes all sources are computed (have $_node).
388
+ *
389
+ * @param sourcesArray - The sources to check (must all be computed sources)
390
+ * @returns true if sources changed, false if unchanged
391
+ */
392
+ const checkComputedSources = (sourcesArray) => {
393
+ const prevTracked = tracked;
394
+ tracked = false;
395
+ const len = sourcesArray.length;
396
+ for (let i = 0; i < len; ++i) {
397
+ const sourceEntry = sourcesArray[i];
398
+ const sourceNode = sourceEntry.d;
399
+ // Access source to trigger its recomputation if needed
400
+ try {
401
+ computedRead(sourceNode);
402
+ }
403
+ catch {
404
+ // Error counts as changed - caller will recompute and may handle differently
405
+ tracked = prevTracked;
406
+ return true;
407
+ }
408
+ // Check if source version changed (meaning its value changed)
409
+ // Early exit - runWithTracking will update all versions during recomputation
410
+ if (sourceEntry.a !== sourceNode.a) {
411
+ tracked = prevTracked;
412
+ return true;
413
+ }
414
+ }
415
+ tracked = prevTracked;
416
+ return false;
417
+ };
418
+
419
+ /**
420
+ * Debug configuration flag: Warn when writing to signals/state inside a computed
421
+ */
422
+ const WARN_ON_WRITE_IN_COMPUTED = 1 << 0;
423
+ /**
424
+ * Debug configuration flag: Suppress warning when effects are disposed by GC instead of explicitly
425
+ */
426
+ const SUPPRESS_EFFECT_GC_WARNING = 1 << 1;
427
+ /**
428
+ * Debug configuration flag: Warn when effects are created without an active scope
429
+ * This is an allowed pattern, but teams may choose to enforce scope usage for better effect lifecycle management
430
+ */
431
+ const WARN_ON_UNTRACKED_EFFECT = 1 << 2;
432
+ /**
433
+ * Current debug configuration bitfield
434
+ */
435
+ let debugConfigFlags = 0;
436
+ /**
437
+ * Configure debug behavior using a bitfield of flags
438
+ */
439
+ const debugConfig = (flags) => {
440
+ debugConfigFlags = flags | 0;
441
+ };
442
+ /**
443
+ * Safely call each function in an iterable, logging any errors to console
444
+ */
445
+ const safeForEach = (fns) => {
446
+ for (let i = 0, len = fns.length; i < len; ++i) {
447
+ const fn = fns[i];
448
+ try {
449
+ fn?.();
450
+ }
451
+ catch (e) {
452
+ console.error(e);
453
+ }
454
+ }
455
+ };
456
+ /**
457
+ * Warn if writing inside a computed (not an effect)
458
+ * Only runs in DEV mode and when configured
459
+ */
460
+ const warnIfWriteInComputed = (context) => {
461
+ if (DEV && (debugConfigFlags & WARN_ON_WRITE_IN_COMPUTED) !== 0 && currentComputing && (currentComputing.g & 8 /* Flag.EFFECT */) === 0) {
462
+ console.warn(`[@slimlib/store] Writing to ${context} inside a computed is not recommended. The computed will not automatically re-run when this value changes, which may lead to stale values.`);
463
+ }
464
+ };
465
+ /**
466
+ * FinalizationRegistry for detecting effects that are GC'd without being properly disposed.
467
+ * Only created in DEV mode.
468
+ */
469
+ const effectRegistry = DEV
470
+ ? new FinalizationRegistry((stackTrace) => {
471
+ if ((debugConfigFlags & SUPPRESS_EFFECT_GC_WARNING) === 0) {
472
+ console.warn(`[@slimlib/store] Effect was garbage collected without being disposed. This may indicate a memory leak. Effects should be disposed by calling the returned dispose function or by using a scope that is properly disposed.\n\nEffect was created at:\n${stackTrace}`);
473
+ }
474
+ })
475
+ : null;
476
+ /**
477
+ * Register an effect for GC tracking.
478
+ * Returns a token that must be passed to unregisterEffect when the effect is properly disposed.
479
+ * Only active in DEV mode; returns undefined in production.
480
+ */
481
+ const registerEffect = DEV
482
+ ? () => {
483
+ const token = {};
484
+ // Capture stack trace at effect creation for better debugging
485
+ // Remove the first few lines (Error + registerEffect call) to get to the actual effect() call
486
+ const relevantStack = String(new Error().stack).split('\n').slice(3).join('\n');
487
+ effectRegistry.register(token, relevantStack, token);
488
+ return token;
489
+ }
490
+ : () => undefined;
491
+ /**
492
+ * Unregister an effect from GC tracking (called when effect is properly disposed).
493
+ * Only active in DEV mode.
494
+ */
495
+ const unregisterEffect = DEV
496
+ ? (token) => {
497
+ effectRegistry?.unregister(token);
498
+ }
499
+ : () => { };
500
+ /**
501
+ * Warn if an effect is created without an active scope.
502
+ * Only runs in DEV mode and when WARN_ON_UNTRACKED_EFFECT is enabled.
503
+ */
504
+ const warnIfNoActiveScope = DEV
505
+ ? (activeScope) => {
506
+ if ((debugConfigFlags & WARN_ON_UNTRACKED_EFFECT) !== 0 && !activeScope) {
507
+ console.warn(`[@slimlib/store] Effect created without an active scope. Consider using scope() or setActiveScope() to track effects for proper lifecycle management.`);
508
+ }
509
+ }
510
+ : () => { };
511
+ const cycleMessage = 'Detected cycle in computations.';
512
+
513
+ /**
514
+ * Read function for computed nodes
515
+ */
516
+ function computedRead(self) {
517
+ // ===== PULL PHASE: Register this computed as a dependency of the current consumer =====
518
+ // Track if someone is reading us
519
+ if (tracked && currentComputing !== undefined) {
520
+ // Inline tracking for computed dependencies
521
+ const consumerSources = currentComputing.h;
522
+ const skipIndex = currentComputing.k;
523
+ const deps = self.i;
524
+ const existing = consumerSources[skipIndex];
525
+ const noSource = existing === undefined;
526
+ if (noSource || existing.c !== deps) {
527
+ // Different dependency - clear old ones from this point and rebuild
528
+ if (!noSource) {
529
+ clearSources(currentComputing, skipIndex);
530
+ }
531
+ // Push source entry - version will be updated after source computes
532
+ // Uses shared createSourceEntry factory for V8 hidden class monomorphism
533
+ consumerSources.push(createSourceEntry(deps, self, 0, undefined, undefined));
534
+ // Only register with source if we're live
535
+ if ((currentComputing.g & (8 /* Flag.EFFECT */ | 64 /* Flag.LIVE */)) !== 0) {
536
+ deps.add(currentComputing);
537
+ // If source computed is not live, make it live
538
+ if ((self.g & 64 /* Flag.LIVE */) === 0) {
539
+ makeLive(self);
540
+ }
541
+ }
542
+ }
543
+ // Mark that this node has computed sources (for version update loop optimization)
544
+ currentComputing.g |= 256 /* Flag.HAS_COMPUTED_SOURCE */;
545
+ ++currentComputing.k;
546
+ }
547
+ let flags = self.g;
548
+ const sourcesArray = self.h;
549
+ // Cycle detection: if this computed is already being computed, we have a cycle
550
+ // This matches TC39 Signals proposal behavior: throw an error on cyclic reads
551
+ if ((flags & 4 /* Flag.COMPUTING */) !== 0) {
552
+ throw new Error(cycleMessage);
553
+ }
554
+ // ===== PULL PHASE: Check if cached value can be returned =====
555
+ // Combined check: node has cached result and doesn't need work
556
+ const hasCached = (flags & (16 /* Flag.HAS_VALUE */ | 32 /* Flag.HAS_ERROR */)) !== 0;
557
+ // biome-ignore lint/suspicious/noConfusingLabels: expected
558
+ checkCache: if ((flags & (1 /* Flag.DIRTY */ | 2 /* Flag.CHECK */)) === 0 && hasCached) {
559
+ // Fast-path: nothing has changed globally since last read
560
+ if (self.f === globalVersion) {
561
+ if ((flags & 32 /* Flag.HAS_ERROR */) !== 0) {
562
+ throw self.l;
563
+ }
564
+ return self.l;
565
+ }
566
+ // ===== PULL PHASE: Poll sources for non-live computeds =====
567
+ // Non-live nodes poll instead of receiving push notifications
568
+ if ((flags & 64 /* Flag.LIVE */) === 0) {
569
+ let sourceChanged = false;
570
+ // Disable tracking while polling sources to avoid unnecessary dependency tracking
571
+ const prevTracked = tracked;
572
+ // TODO: inline after it combined to a single scope?
573
+ setTracked(false);
574
+ for (let i = 0, len = sourcesArray.length; i < len; ++i) {
575
+ const source = sourcesArray[i];
576
+ const sourceNode = source.d; // Extract once at loop start
577
+ if (sourceNode === undefined) {
578
+ // State source - check if deps version changed
579
+ const currentDepsVersion = source.c.a;
580
+ if (source.a !== currentDepsVersion) {
581
+ // Deps version changed, check if actual value reverted (primitives only)
582
+ const storedValue = source.e;
583
+ const storedType = typeof storedValue;
584
+ if (storedValue === null || (storedType !== 'object' && storedType !== 'function')) {
585
+ const currentValue = source.b();
586
+ if (Object.is(currentValue, storedValue)) {
587
+ // Value reverted - update depsVersion and continue checking
588
+ source.a = currentDepsVersion;
589
+ continue;
590
+ }
591
+ }
592
+ // Value actually changed - mark DIRTY and skip remaining
593
+ sourceChanged = true;
594
+ break;
595
+ }
596
+ }
597
+ else {
598
+ // Computed source - use sourceNode directly (already extracted above)
599
+ try {
600
+ computedRead(sourceNode);
601
+ }
602
+ catch {
603
+ // Error counts as changed
604
+ sourceChanged = true;
605
+ break;
606
+ }
607
+ if (source.a !== sourceNode.a) {
608
+ sourceChanged = true;
609
+ break; // EXIT EARLY - don't process remaining sources
610
+ }
611
+ }
612
+ }
613
+ setTracked(prevTracked);
614
+ if (sourceChanged) {
615
+ // Source changed or threw - mark DIRTY and proceed to recompute
616
+ self.g = flags |= 1 /* Flag.DIRTY */;
617
+ break checkCache;
618
+ }
619
+ // No sources changed - return cached value
620
+ self.f = globalVersion;
621
+ if ((flags & 32 /* Flag.HAS_ERROR */) !== 0) {
622
+ throw self.l;
623
+ }
624
+ return self.l;
625
+ }
626
+ }
627
+ // ===== PULL PHASE: Verify CHECK state for live computeds =====
628
+ // Live computeds receive CHECK via push - verify sources before recomputing
629
+ // Non-live computeds already verified above during polling
630
+ // Note: Check for Flag.HAS_VALUE OR Flag.HAS_ERROR since cached errors should also use this path
631
+ if ((flags & (1 /* Flag.DIRTY */ | 2 /* Flag.CHECK */ | 128 /* Flag.HAS_STATE_SOURCE */)) === 2 /* Flag.CHECK */ && hasCached) {
632
+ if (checkComputedSources(sourcesArray)) {
633
+ // Sources changed or errored - mark DIRTY and let getter run
634
+ flags = (flags & -3 /* Flag.CHECK */) | 1 /* Flag.DIRTY */;
635
+ }
636
+ else {
637
+ // Sources unchanged, clear CHECK flag and return cached value
638
+ self.g = flags & -3 /* Flag.CHECK */;
639
+ // No sources changed - return cached value
640
+ self.f = globalVersion;
641
+ if ((flags & 32 /* Flag.HAS_ERROR */) !== 0) {
642
+ throw self.l;
643
+ }
644
+ return self.l;
645
+ }
646
+ }
647
+ // ===== PULL PHASE: Recompute value by pulling from sources =====
648
+ // Recompute if dirty or check (sources actually changed)
649
+ if ((flags & (1 /* Flag.DIRTY */ | 2 /* Flag.CHECK */)) !== 0) {
650
+ const wasDirty = (flags & 1 /* Flag.DIRTY */) !== 0;
651
+ runWithTracking(self, () => {
652
+ try {
653
+ const newValue = self.j();
654
+ // Check if value actually changed (common path: no error recovery)
655
+ const changed = (flags & 16 /* Flag.HAS_VALUE */) === 0 || !self.m(self.l, newValue);
656
+ if (changed) {
657
+ self.l = newValue;
658
+ // Increment version to indicate value changed (for polling)
659
+ ++self.a;
660
+ self.g = (self.g | 16 /* Flag.HAS_VALUE */) & ~32 /* Flag.HAS_ERROR */;
661
+ // ===== PUSH PHASE (during pull): Mark CHECK-only dependents as DIRTY =====
662
+ // When value changes during recomputation, upgrade dependent CHECK flags to DIRTY
663
+ for (const dep of self.i) {
664
+ const depFlags = dep.g;
665
+ if ((depFlags & (4 /* Flag.COMPUTING */ | 1 /* Flag.DIRTY */ | 2 /* Flag.CHECK */)) === 2 /* Flag.CHECK */) {
666
+ dep.g = depFlags | 1 /* Flag.DIRTY */;
667
+ }
668
+ }
669
+ }
670
+ else if (wasDirty) {
671
+ self.g |= 16 /* Flag.HAS_VALUE */;
672
+ }
673
+ // Update last seen global version
674
+ self.f = globalVersion;
675
+ }
676
+ catch (e) {
677
+ // Per TC39 Signals proposal: cache the error and mark as clean with error flag
678
+ // The error will be rethrown on subsequent reads until a dependency changes
679
+ // Reuse valueSymbol for error storage since a computed can't have both value and error
680
+ // Increment version since the result changed (to error)
681
+ ++self.a;
682
+ self.l = e;
683
+ self.g = (self.g & ~16 /* Flag.HAS_VALUE */) | 32 /* Flag.HAS_ERROR */;
684
+ self.f = globalVersion;
685
+ }
686
+ });
687
+ }
688
+ // Check if we have a cached error to rethrow (stored in valueSymbol)
689
+ if ((self.g & 32 /* Flag.HAS_ERROR */) !== 0) {
690
+ throw self.l;
691
+ }
692
+ return self.l;
693
+ }
694
+ /**
695
+ * Creates a computed value that automatically tracks dependencies and caches results
696
+ */
697
+ const computed = (getter, equals = Object.is) => {
698
+ const node = {
699
+ h: [],
700
+ i: new DepsSet(noopGetter),
701
+ g: 1 /* Flag.DIRTY */,
702
+ k: 0,
703
+ a: 0,
704
+ l: undefined,
705
+ f: 0,
706
+ j: getter,
707
+ m: equals,
708
+ };
709
+ return () => computedRead(node);
710
+ };
711
+
712
+ /**
713
+ * Effect creation counter - increments on every effect creation
714
+ * Used to maintain effect execution order by creation time
715
+ */
716
+ let effectCreationCounter = 0;
717
+ /**
718
+ * Creates a reactive effect that runs when dependencies change
719
+ */
720
+ // biome-ignore lint/suspicious/noConfusingVoidType: void is semantically correct here - callback may return nothing or a cleanup function
721
+ const effect = (callback) => {
722
+ let disposed = false;
723
+ // Register effect for GC tracking (only in DEV mode)
724
+ const gcToken = registerEffect();
725
+ // Warn if effect is created without an active scope (only in DEV mode when enabled)
726
+ warnIfNoActiveScope(activeScope);
727
+ // Declare node first so the runner closure can capture it.
728
+ // The variable will be assigned before the runner is ever called.
729
+ let node;
730
+ // Define the runner function BEFORE creating the node so that $_fn
731
+ // is a function from the start (Fix #1: avoids hidden class transition
732
+ // from undefined → function on the $_fn field).
733
+ const runner = () => {
734
+ // Skip if effect was disposed (may still be in batched queue from before disposal)
735
+ if (disposed) {
736
+ return;
737
+ }
738
+ // Cycle detection: if this node is already being computed, we have a cycle
739
+ const flags = node.g;
740
+ if ((flags & 4 /* Flag.COMPUTING */) !== 0) {
741
+ throw new Error(cycleMessage);
742
+ }
743
+ // ----------------------------------------------------------------
744
+ // PULL PHASE: Verify if sources actually changed before running
745
+ // ----------------------------------------------------------------
746
+ // Bail-out optimization: if only CHECK flag is set (not DIRTY),
747
+ // verify that computed sources actually changed before running
748
+ if ((flags & (1 /* Flag.DIRTY */ | 2 /* Flag.CHECK */ | 128 /* Flag.HAS_STATE_SOURCE */)) === 2 /* Flag.CHECK */) {
749
+ // PULL: Read computed sources to check if they changed
750
+ // If false, sources didn't change - clear CHECK flag and skip
751
+ // If true, sources changed or errored - proceed to run
752
+ if (!checkComputedSources(node.h)) {
753
+ node.g = flags & -3 /* Flag.CHECK */;
754
+ return;
755
+ }
756
+ }
757
+ // ----------------------------------------------------------------
758
+ // PULL PHASE: Execute effect and track dependencies
759
+ // ----------------------------------------------------------------
760
+ runWithTracking(node, () => {
761
+ // Run previous cleanup if it exists (stored in $_value)
762
+ if (typeof node.l === 'function') {
763
+ node.l();
764
+ }
765
+ // Run the callback and store new cleanup in $_value
766
+ // (callback will PULL values from signals/state/computed)
767
+ node.l = callback();
768
+ });
769
+ };
770
+ // Create effect node as a plain object with IDENTICAL initial field types
771
+ // as computed nodes to ensure V8 hidden class monomorphism (Fix #2):
772
+ // $_deps: new DepsSet() (Set object, same as computed — never used for effects)
773
+ // $_fn: runner (function, same as computed's getter)
774
+ // $_equals: Object.is (function, same as computed's equality comparator)
775
+ //
776
+ // $_value: stores cleanup function returned by the effect callback
777
+ // $_stamp: creation order counter for effect scheduling
778
+ node = {
779
+ h: [],
780
+ i: new DepsSet(noopGetter),
781
+ g: 1 /* Flag.DIRTY */ | 8 /* Flag.EFFECT */,
782
+ k: 0,
783
+ a: 0,
784
+ l: undefined,
785
+ f: ++effectCreationCounter,
786
+ j: runner,
787
+ m: Object.is,
788
+ };
789
+ const effectId = node.f;
790
+ const dispose = () => {
791
+ // Mark as disposed to prevent running if still in batched queue
792
+ disposed = true;
793
+ // Unregister from GC tracking (only in DEV mode)
794
+ unregisterEffect(gcToken);
795
+ // Run cleanup if it exists (stored in $_value)
796
+ if (typeof node.l === 'function') {
797
+ node.l();
798
+ }
799
+ clearSources(node);
800
+ };
801
+ // Track to appropriate scope
802
+ if (activeScope) {
803
+ activeScope[trackSymbol](dispose);
804
+ }
805
+ // ----------------------------------------------------------------
806
+ // Initial scheduling (triggers first PULL when flush runs)
807
+ // ----------------------------------------------------------------
808
+ // Trigger first run via batched queue
809
+ // node is already dirty
810
+ // and effect is for sure with the latest id so we directly adding without the sort
811
+ batchedAddNew(node, effectId);
812
+ scheduleFlush();
813
+ return dispose;
814
+ };
815
+
816
+ /**
817
+ * Creates a reactive scope for tracking effects
818
+ * Effects created within a scope callback are automatically tracked and disposed together
819
+ */
820
+ const scope = (callback, parent = activeScope) => {
821
+ const effects = [];
822
+ const children = [];
823
+ const cleanups = [];
824
+ let disposed = false;
825
+ let myIndex = -1;
826
+ /**
827
+ * Register a cleanup function to run when scope is disposed
828
+ */
829
+ const onDispose = cleanup => {
830
+ if (disposed) {
831
+ return;
832
+ }
833
+ cleanups.push(cleanup);
834
+ };
835
+ const ctx = ((cb) => {
836
+ if (!cb) {
837
+ // Dispose - return early if already disposed (idempotent)
838
+ if (disposed) {
839
+ return;
840
+ }
841
+ // Dispose
842
+ disposed = true;
843
+ // Dispose children first (depth-first)
844
+ safeForEach(children);
845
+ // Stop all effects
846
+ safeForEach(effects);
847
+ effects.length = 0;
848
+ // Run cleanup handlers
849
+ safeForEach(cleanups);
850
+ // Remove from parent
851
+ if (parent) {
852
+ parent[childrenSymbol][myIndex] = undefined;
853
+ }
854
+ return;
855
+ }
856
+ // Extend scope - silently ignore if disposed
857
+ if (disposed) {
858
+ return ctx;
859
+ }
860
+ // Run callback in this scope's context
861
+ const prev = activeScope;
862
+ setActiveScope(ctx);
863
+ try {
864
+ cb(onDispose);
865
+ }
866
+ finally {
867
+ setActiveScope(prev);
868
+ }
869
+ return ctx;
870
+ });
871
+ // Internal symbols for effect tracking and child management
872
+ ctx[trackSymbol] = (dispose) => effects.push(dispose);
873
+ ctx[childrenSymbol] = children;
874
+ // Register with parent
875
+ if (parent) {
876
+ myIndex = parent[childrenSymbol].push(ctx) - 1;
877
+ }
878
+ // Run initial callback if provided
879
+ if (callback) {
880
+ ctx(callback);
881
+ }
882
+ return ctx;
883
+ };
884
+
885
+ /**
886
+ * Create a simple signal
887
+ */
888
+ function signal(initialValue) {
889
+ let value = initialValue;
890
+ let deps;
891
+ /**
892
+ * Read the signal value and track dependency
893
+ */
894
+ const read = () => {
895
+ // === PULL PHASE ===
896
+ // When a computed/effect reads this signal, we register the dependency
897
+ // Fast path: if not tracked or no current computing, skip tracking
898
+ if (tracked && currentComputing !== undefined) {
899
+ // Pass value getter for polling optimization (value revert detection)
900
+ // biome-ignore lint/suspicious/noAssignInExpressions: optimization
901
+ trackStateDependency((deps ??= new DepsSet(read)), read, value);
902
+ }
903
+ return value;
904
+ // === END PULL PHASE ===
905
+ };
906
+ /**
907
+ * Set a new value and notify dependents
908
+ */
909
+ read.set = (newValue) => {
910
+ // === PUSH PHASE ===
911
+ // When the signal value changes, we eagerly propagate dirty/check flags
912
+ // to all dependents via markDependents
913
+ warnIfWriteInComputed('signal');
914
+ if (!Object.is(value, newValue)) {
915
+ value = newValue;
916
+ if (deps !== undefined) {
917
+ markDependents(deps); // Push: notify all dependents
918
+ }
919
+ }
920
+ // === END PUSH PHASE ===
921
+ };
922
+ return read;
923
+ }
924
+
925
+ /**
926
+ * Creates a reactive state object
927
+ */
928
+ function state(object = {}) {
929
+ // State uses a proxy to intercept reads and writes
930
+ // - Reads trigger PULL phase (trackDependency registers the consumer)
931
+ // - Writes trigger PUSH phase (markDependents propagates dirty flags)
932
+ const proxiesCache = new WeakMap();
933
+ /**
934
+ * PUSH PHASE: Notify all dependents that a property changed
935
+ * This propagates dirty/check flags to all live consumers
936
+ */
937
+ const notifyPropertyDependents = (target, property) => {
938
+ const propsMap = target[propertyDepsSymbol];
939
+ if (!propsMap)
940
+ return;
941
+ const deps = propsMap.get(property);
942
+ if (deps !== undefined) {
943
+ markDependents(deps);
944
+ }
945
+ };
946
+ const createProxy = (object) => {
947
+ if (proxiesCache.has(object)) {
948
+ return proxiesCache.get(object);
949
+ }
950
+ let methodCache;
951
+ const proxy = new Proxy(object, {
952
+ // PUSH PHASE: Setting a property notifies all dependents
953
+ set(target, p, newValue) {
954
+ warnIfWriteInComputed('state');
955
+ const realValue = unwrapValue(newValue);
956
+ // Use direct property access instead of Reflect for performance
957
+ if (!Object.is(target[p], realValue)) {
958
+ target[p] = realValue;
959
+ // PUSH: Propagate dirty flags to dependents
960
+ notifyPropertyDependents(target, p);
961
+ // Clear method cache entry if it was a method
962
+ methodCache?.delete(p);
963
+ }
964
+ return true;
965
+ },
966
+ // PULL PHASE: Reading a property registers the dependency
967
+ get(target, p) {
968
+ if (p === unwrap)
969
+ return target;
970
+ // Use direct property access instead of Reflect for performance
971
+ const propValue = target[p];
972
+ // PULL: Track dependency if we're inside an effect/computed
973
+ if (tracked && currentComputing !== undefined) {
974
+ // Get or create the Map for this target (stored as non-enumerable property)
975
+ let propsMap = target[propertyDepsSymbol];
976
+ if (propsMap === undefined) {
977
+ propsMap = new Map();
978
+ Object.defineProperty(target, propertyDepsSymbol, { value: propsMap });
979
+ }
980
+ // Get or create the Set for this property
981
+ let deps = propsMap.get(p);
982
+ if (deps === undefined) {
983
+ // Create DepsSet with getter eagerly to avoid V8 field constness deopts
984
+ const propertyGetter = () => target[p];
985
+ // biome-ignore lint/suspicious/noAssignInExpressions: optimization
986
+ propsMap.set(p, (deps = new DepsSet(propertyGetter)));
987
+ }
988
+ // PULL: Bidirectional linking with optimization
989
+ // Pass value getter for polling optimization (value revert detection)
990
+ // Capture target and property for later value retrieval
991
+ trackStateDependency(deps, deps.b, propValue);
992
+ }
993
+ // Fast path for primitives (most common case)
994
+ const propertyType = typeof propValue;
995
+ if (propValue === null || (propertyType !== 'object' && propertyType !== 'function')) {
996
+ return propValue;
997
+ }
998
+ // Functions are wrapped to apply with correct `this` (target, not proxy)
999
+ // After function call, notify dependents (function may have mutated internal state)
1000
+ // Functions are wrapped to trigger PUSH after mutation
1001
+ if (propertyType === 'function') {
1002
+ // Check cache first to avoid creating new function on every access
1003
+ if (methodCache === undefined) {
1004
+ methodCache = new Map();
1005
+ }
1006
+ let cached = methodCache.get(p);
1007
+ if (cached === undefined) {
1008
+ // Capture method reference at cache time to avoid re-reading on each call
1009
+ const method = propValue;
1010
+ cached = (...args) => {
1011
+ // Unwrap in-place - args is already a new array from rest params
1012
+ for (let i = 0, len = args.length; i < len; ++i) {
1013
+ args[i] = unwrapValue(args[i]);
1014
+ }
1015
+ const result = method.apply(target, args);
1016
+ // PUSH PHASE: Notify after function call (function may have mutated state)
1017
+ // Only notify if we're NOT currently inside an effect/computed execution
1018
+ // to avoid infinite loops when reading during effect
1019
+ if (currentComputing === undefined) {
1020
+ const propsMap = target[propertyDepsSymbol];
1021
+ if (propsMap === undefined)
1022
+ return result;
1023
+ for (const deps of propsMap.values()) {
1024
+ // PUSH: Propagate dirty flags to all property dependents
1025
+ markDependents(deps);
1026
+ }
1027
+ }
1028
+ return result;
1029
+ };
1030
+ methodCache.set(p, cached);
1031
+ }
1032
+ return cached;
1033
+ }
1034
+ // Object - create nested proxy
1035
+ return createProxy(propValue);
1036
+ },
1037
+ // PUSH PHASE: Defining a property notifies dependents
1038
+ defineProperty(target, property, attributes) {
1039
+ warnIfWriteInComputed('state');
1040
+ const result = Reflect.defineProperty(target, property, attributes);
1041
+ if (result) {
1042
+ // PUSH: Propagate dirty flags to dependents
1043
+ notifyPropertyDependents(target, property);
1044
+ }
1045
+ return result;
1046
+ },
1047
+ // PUSH PHASE: Deleting a property notifies dependents
1048
+ deleteProperty(target, p) {
1049
+ warnIfWriteInComputed('state');
1050
+ const result = Reflect.deleteProperty(target, p);
1051
+ if (result) {
1052
+ // PUSH: Propagate dirty flags to dependents
1053
+ notifyPropertyDependents(target, p);
1054
+ // Clear method cache entry if it was a method
1055
+ methodCache?.delete(p);
1056
+ }
1057
+ return result;
1058
+ },
1059
+ });
1060
+ proxiesCache.set(object, proxy);
1061
+ return proxy;
1062
+ };
1063
+ return createProxy(object);
1064
+ }
1065
+
1066
+ export { SUPPRESS_EFFECT_GC_WARNING, WARN_ON_UNTRACKED_EFFECT, WARN_ON_WRITE_IN_COMPUTED, activeScope, computed, debugConfig, effect, flushEffects, scope, setActiveScope, setScheduler, signal, state, untracked, unwrapValue };
1067
+ //# sourceMappingURL=index.mjs.map