mutts 1.0.5 → 1.0.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.
Files changed (66) hide show
  1. package/README.md +1 -0
  2. package/dist/chunks/{_tslib-Mzh1rNsX.esm.js → _tslib-MCKDzsSq.esm.js} +2 -2
  3. package/dist/chunks/_tslib-MCKDzsSq.esm.js.map +1 -0
  4. package/dist/chunks/decorator-BGILvPtN.esm.js +627 -0
  5. package/dist/chunks/decorator-BGILvPtN.esm.js.map +1 -0
  6. package/dist/chunks/decorator-BQ2eBTCj.js +651 -0
  7. package/dist/chunks/decorator-BQ2eBTCj.js.map +1 -0
  8. package/dist/chunks/{index-Cvxdw6Ax.js → index-CDCOjzTy.js} +396 -500
  9. package/dist/chunks/index-CDCOjzTy.js.map +1 -0
  10. package/dist/chunks/{index-qiWwozOc.esm.js → index-DiP0RXoZ.esm.js} +301 -403
  11. package/dist/chunks/index-DiP0RXoZ.esm.js.map +1 -0
  12. package/dist/decorator.d.ts +3 -3
  13. package/dist/decorator.esm.js +1 -1
  14. package/dist/decorator.js +1 -1
  15. package/dist/destroyable.esm.js +4 -4
  16. package/dist/destroyable.esm.js.map +1 -1
  17. package/dist/destroyable.js +4 -4
  18. package/dist/destroyable.js.map +1 -1
  19. package/dist/devtools/panel.js.map +1 -1
  20. package/dist/eventful.esm.js +1 -1
  21. package/dist/index.esm.js +48 -3
  22. package/dist/index.esm.js.map +1 -1
  23. package/dist/index.js +48 -4
  24. package/dist/index.js.map +1 -1
  25. package/dist/mutts.umd.js +1 -1
  26. package/dist/mutts.umd.js.map +1 -1
  27. package/dist/mutts.umd.min.js +1 -1
  28. package/dist/mutts.umd.min.js.map +1 -1
  29. package/dist/reactive.d.ts +25 -0
  30. package/dist/reactive.esm.js +3 -3
  31. package/dist/reactive.js +4 -4
  32. package/dist/std-decorators.d.ts +1 -1
  33. package/dist/std-decorators.esm.js +10 -10
  34. package/dist/std-decorators.esm.js.map +1 -1
  35. package/dist/std-decorators.js +10 -10
  36. package/dist/std-decorators.js.map +1 -1
  37. package/docs/ai/manual.md +14 -95
  38. package/docs/reactive/advanced.md +6 -107
  39. package/docs/reactive/debugging.md +158 -0
  40. package/docs/reactive.md +6 -5
  41. package/package.json +16 -66
  42. package/src/decorator.ts +11 -9
  43. package/src/destroyable.ts +3 -3
  44. package/src/index.ts +46 -0
  45. package/src/reactive/change.ts +1 -1
  46. package/src/reactive/debug.ts +1 -1
  47. package/src/reactive/deep-touch.ts +1 -1
  48. package/src/reactive/deep-watch.ts +1 -1
  49. package/src/reactive/effect-context.ts +2 -2
  50. package/src/reactive/effects.ts +44 -16
  51. package/src/reactive/index.ts +1 -1
  52. package/src/reactive/interface.ts +9 -8
  53. package/src/reactive/memoize.ts +77 -31
  54. package/src/reactive/proxy.ts +4 -4
  55. package/src/reactive/registry.ts +67 -0
  56. package/src/reactive/tracking.ts +12 -41
  57. package/src/reactive/types.ts +37 -0
  58. package/src/std-decorators.ts +9 -9
  59. package/src/utils.ts +141 -0
  60. package/dist/chunks/_tslib-Mzh1rNsX.esm.js.map +0 -1
  61. package/dist/chunks/decorator-DLvrD0UF.js +0 -265
  62. package/dist/chunks/decorator-DLvrD0UF.js.map +0 -1
  63. package/dist/chunks/decorator-DqiszP7i.esm.js +0 -253
  64. package/dist/chunks/decorator-DqiszP7i.esm.js.map +0 -1
  65. package/dist/chunks/index-Cvxdw6Ax.js.map +0 -1
  66. package/dist/chunks/index-qiWwozOc.esm.js.map +0 -1
@@ -1,6 +1,6 @@
1
- import { i as isConstructor, R as ReflectGet, d as decorator, b as ReflectSet, c as isOwnAccessor, r as renamed } from './decorator-DqiszP7i.esm.js';
1
+ import { i as isConstructor, a as ReflectGet, g as rootFunction, o as options, h as allProps, R as ReactiveError, j as ReactiveErrorCode, k as cleanup$1, s as stopped, d as decorator, n as nonReactiveMark, p as nativeReactive, q as prototypeForwarding, u as unreactiveProperties, b as ReflectSet, f as isOwnAccessor, r as renamed, e as deepCompare, t as projectionInfo } from './decorator-BGILvPtN.esm.js';
2
2
  import { Indexable, setAt, getAt, ArrayReadForward, forwardArray } from '../indexable.esm.js';
3
- import { a as __setFunctionName, b as __esDecorate, c as __runInitializers, d as __classPrivateFieldSet, _ as __classPrivateFieldGet } from './_tslib-Mzh1rNsX.esm.js';
3
+ import { _ as __setFunctionName, a as __esDecorate, b as __runInitializers, c as __classPrivateFieldSet, d as __classPrivateFieldGet } from './_tslib-MCKDzsSq.esm.js';
4
4
 
5
5
  /// <reference lib="esnext.collection" />
6
6
  var _a, _b;
@@ -332,323 +332,6 @@ function mixin(mixinFunction, unwrapFunction) {
332
332
  });
333
333
  }
334
334
 
335
- // biome-ignore-all lint/suspicious/noConfusingVoidType: Type 'void' is not assignable to type 'ScopedCallback | undefined'.
336
- // Argument of type '() => void' is not assignable to parameter of type '(dep: DependencyFunction) => ScopedCallback | undefined'.
337
- // Track native reactivity
338
- const nativeReactive = Symbol('native-reactive');
339
- /**
340
- * Symbol to mark individual objects as non-reactive
341
- */
342
- const nonReactiveMark = Symbol('non-reactive');
343
- /**
344
- * Symbol to mark class properties as non-reactive
345
- */
346
- const unreactiveProperties = Symbol('unreactive-properties');
347
- /**
348
- * Symbol for prototype forwarding in reactive objects
349
- */
350
- const prototypeForwarding = Symbol('prototype-forwarding');
351
- /**
352
- * Symbol representing all properties in reactive tracking
353
- */
354
- const allProps = Symbol('all-props');
355
- /**
356
- * Symbol for accessing projection information on reactive objects
357
- */
358
- const projectionInfo = Symbol('projection-info');
359
- // Symbol to mark functions with their root function
360
- const rootFunction = Symbol('root-function');
361
- /**
362
- * Structured error codes for machine-readable diagnosis
363
- */
364
- var ReactiveErrorCode;
365
- (function (ReactiveErrorCode) {
366
- ReactiveErrorCode["CycleDetected"] = "CYCLE_DETECTED";
367
- ReactiveErrorCode["MaxDepthExceeded"] = "MAX_DEPTH_EXCEEDED";
368
- ReactiveErrorCode["MaxReactionExceeded"] = "MAX_REACTION_EXCEEDED";
369
- ReactiveErrorCode["WriteInComputed"] = "WRITE_IN_COMPUTED";
370
- ReactiveErrorCode["TrackingError"] = "TRACKING_ERROR";
371
- })(ReactiveErrorCode || (ReactiveErrorCode = {}));
372
- /**
373
- * Error class for reactive system errors
374
- */
375
- class ReactiveError extends Error {
376
- constructor(message, debugInfo) {
377
- super(message);
378
- this.debugInfo = debugInfo;
379
- this.name = 'ReactiveError';
380
- }
381
- }
382
- // biome-ignore-start lint/correctness/noUnusedFunctionParameters: Interface declaration with empty defaults
383
- /**
384
- * Global options for the reactive system
385
- */
386
- const options = {
387
- /**
388
- * Debug purpose: called when an effect is entered
389
- * @param effect - The effect that is entered
390
- */
391
- enter: (_effect) => { },
392
- /**
393
- * Debug purpose: called when an effect is left
394
- * @param effect - The effect that is left
395
- */
396
- leave: (_effect) => { },
397
- /**
398
- * Debug purpose: called when an effect is chained
399
- * @param target - The effect that is being triggered
400
- * @param caller - The effect that is calling the target
401
- */
402
- chain: (_targets, _caller) => { },
403
- /**
404
- * Debug purpose: called when an effect chain is started
405
- * @param target - The effect that is being triggered
406
- */
407
- beginChain: (_targets) => { },
408
- /**
409
- * Debug purpose: called when an effect chain is ended
410
- */
411
- endChain: () => { },
412
- garbageCollected: (_fn) => { },
413
- /**
414
- * Debug purpose: called when an object is touched
415
- * @param obj - The object that is touched
416
- * @param evolution - The type of change
417
- * @param props - The properties that changed
418
- * @param deps - The dependencies that changed
419
- */
420
- touched: (_obj, _evolution, _props, _deps) => { },
421
- /**
422
- * Debug purpose: called when an effect is skipped because it's already running
423
- * @param effect - The effect that is already running
424
- * @param runningChain - The array of effects from the detected one to the currently running one
425
- */
426
- skipRunningEffect: (_effect, _runningChain) => { },
427
- /**
428
- * Debug purpose: maximum effect chain (like call stack max depth)
429
- * Used to prevent infinite loops
430
- * @default 100
431
- */
432
- maxEffectChain: 100,
433
- /**
434
- * Maximum number of times an effect can be triggered by the same cause in a single batch
435
- * Used to detect aggressive re-computation or infinite loops
436
- * @default 10
437
- */
438
- maxTriggerPerBatch: 10,
439
- /**
440
- * Debug purpose: maximum effect reaction (like call stack max depth)
441
- * Used to prevent infinite loops
442
- * @default 'throw'
443
- */
444
- maxEffectReaction: 'throw',
445
- /**
446
- * How to handle cycles detected in effect batches
447
- * - 'throw': Throw an error with cycle information (default, recommended for development)
448
- * - 'warn': Log a warning and break the cycle by executing one effect
449
- * - 'break': Silently break the cycle by executing one effect (recommended for production)
450
- * - 'strict': Prevent cycle creation by checking graph before execution (throws error)
451
- * @default 'throw'
452
- */
453
- cycleHandling: 'throw',
454
- /**
455
- * Maximum depth for deep watching traversal
456
- * Used to prevent infinite recursion in circular references
457
- * @default 100
458
- */
459
- maxDeepWatchDepth: 100,
460
- /**
461
- * Only react on instance members modification (not inherited properties)
462
- * For instance, do not track class methods
463
- * @default true
464
- */
465
- instanceMembers: true,
466
- /**
467
- * Ignore accessors (getters and setters) and only track direct properties
468
- * @default true
469
- */
470
- ignoreAccessors: true,
471
- /**
472
- * Enable recursive touching when objects with the same prototype are replaced
473
- * When enabled, replacing an object with another of the same prototype triggers
474
- * recursive diffing instead of notifying parent effects
475
- * @default true
476
- */
477
- recursiveTouching: true,
478
- /**
479
- * Default async execution mode for effects that return Promises
480
- * - 'cancel': Cancel previous async execution when dependencies change (default, enables async zone)
481
- * - 'queue': Queue next execution to run after current completes (enables async zone)
482
- * - 'ignore': Ignore new executions while async work is running (enables async zone)
483
- * - false: Disable async zone and async mode handling (effects run concurrently)
484
- *
485
- * **When truthy:** Enables async zone (Promise.prototype wrapping) for automatic context
486
- * preservation in Promise callbacks. Warning: This modifies Promise.prototype globally.
487
- * Only enable if no other library modifies Promise.prototype.
488
- *
489
- * **When false:** Async zone is disabled. Use `tracked()` manually in Promise callbacks.
490
- *
491
- * Can be overridden per-effect via EffectOptions
492
- * @default 'cancel'
493
- */
494
- asyncMode: 'cancel',
495
- // biome-ignore lint/suspicious/noConsole: This is the whole point here
496
- warn: (...args) => console.warn(...args),
497
- /**
498
- * Configuration for the introspection system
499
- */
500
- introspection: {
501
- /**
502
- * Whether to keep a history of mutations for debugging
503
- * @default false
504
- */
505
- enableHistory: false,
506
- /**
507
- * Number of mutations to keep in history
508
- * @default 50
509
- */
510
- historySize: 50,
511
- },
512
- /**
513
- * Configuration for zone hooks - control which async APIs are hooked
514
- * Each option controls whether the corresponding async API is wrapped to preserve effect context
515
- * Only applies when asyncMode is enabled (truthy)
516
- */
517
- zones: {
518
- /**
519
- * Hook setTimeout to preserve effect context
520
- * @default true
521
- */
522
- setTimeout: true,
523
- /**
524
- * Hook setInterval to preserve effect context
525
- * @default true
526
- */
527
- setInterval: true,
528
- /**
529
- * Hook requestAnimationFrame (runs in untracked context when hooked)
530
- * @default true
531
- */
532
- requestAnimationFrame: true,
533
- /**
534
- * Hook queueMicrotask to preserve effect context
535
- * @default true
536
- */
537
- queueMicrotask: true,
538
- },
539
- };
540
-
541
- /**
542
- * Effect context stack for nested tracking (front = active, next = parent)
543
- */
544
- const stack = [];
545
- function captureEffectStack() {
546
- return stack.slice();
547
- }
548
- function isRunning(effect) {
549
- const rootEffect = getRoot(effect);
550
- // Check if the effect is directly in the stack
551
- const rootIndex = stack.indexOf(rootEffect);
552
- if (rootIndex !== -1) {
553
- return stack.slice(0, rootIndex + 1).reverse();
554
- }
555
- // Check if any effect in the stack is a descendant of this effect
556
- // (i.e., walk up the parent chain from each stack effect to see if we reach this effect)
557
- for (let i = 0; i < stack.length; i++) {
558
- const stackEffect = stack[i];
559
- let current = stackEffect;
560
- const visited = new WeakSet();
561
- const ancestorChain = [];
562
- // TODO: That's perhaps a lot of computations for an `assert`
563
- // Walk up the parent chain to find if this effect is an ancestor
564
- while (current && !visited.has(current)) {
565
- visited.add(current);
566
- const currentRoot = getRoot(current);
567
- ancestorChain.push(currentRoot);
568
- if (currentRoot === rootEffect) {
569
- // Found a descendant - build the full chain from ancestor to active
570
- // The ancestorChain contains [descendant, parent, ..., ancestor] (walking up)
571
- // We need [ancestor (effect), ..., parent, descendant, ...stack from descendant to active]
572
- const chainFromAncestor = ancestorChain.reverse(); // [ancestor, ..., descendant]
573
- // Prepend the actual effect we're checking (in case current is a wrapper)
574
- if (chainFromAncestor[0] !== rootEffect) {
575
- chainFromAncestor.unshift(rootEffect);
576
- }
577
- // Append the rest of the stack from the descendant to the active effect
578
- const stackFromDescendant = stack.slice(0, i + 1).reverse(); // [descendant, ..., active]
579
- // Remove duplicate descendant (it's both at end of chainFromAncestor and start of stackFromDescendant)
580
- if (chainFromAncestor.length > 0 && stackFromDescendant.length > 0) {
581
- stackFromDescendant.shift(); // Remove duplicate descendant
582
- }
583
- return [...chainFromAncestor, ...stackFromDescendant];
584
- }
585
- current = effectParent.get(current);
586
- }
587
- }
588
- return false;
589
- }
590
- function withEffectStack(snapshot, fn) {
591
- const previousStack = stack.slice();
592
- assignStack(snapshot);
593
- try {
594
- return fn();
595
- }
596
- finally {
597
- assignStack(previousStack);
598
- }
599
- }
600
- function getActiveEffect() {
601
- return stack[0];
602
- }
603
- /**
604
- * Executes a function with a specific effect context
605
- * @param effect - The effect to use as context
606
- * @param fn - The function to execute
607
- * @param keepParent - Whether to keep the parent effect context
608
- * @returns The result of the function
609
- */
610
- function withEffect(effect, fn) {
611
- // console.log('[Mutts] withEffect', effect ? 'Active' : 'NULL');
612
- if (getRoot(effect) === getRoot(getActiveEffect()))
613
- return fn();
614
- stack.unshift(effect);
615
- try {
616
- return fn();
617
- }
618
- finally {
619
- const recoveredEffect = stack.shift();
620
- if (recoveredEffect !== effect)
621
- throw new ReactiveError('[reactive] Effect stack mismatch');
622
- }
623
- }
624
- function assignStack(values) {
625
- stack.length = 0;
626
- stack.push(...values);
627
- }
628
-
629
- const objectToProxy = new WeakMap();
630
- const proxyToObject = new WeakMap();
631
- function storeProxyRelationship(target, proxy) {
632
- objectToProxy.set(target, proxy);
633
- proxyToObject.set(proxy, target);
634
- }
635
- function getExistingProxy(target) {
636
- return objectToProxy.get(target);
637
- }
638
- function trackProxyObject(proxy, target) {
639
- proxyToObject.set(proxy, target);
640
- }
641
- function unwrap(obj) {
642
- let current = obj;
643
- while (current && typeof current === 'object' && current !== null && proxyToObject.has(current)) {
644
- current = proxyToObject.get(current);
645
- }
646
- return current;
647
- }
648
- function isReactive(obj) {
649
- return proxyToObject.has(obj);
650
- }
651
-
652
335
  // Track which effects are watching which reactive objects for cleanup
653
336
  const effectToReactiveObjects = new WeakMap();
654
337
  // Track effects per reactive object and property
@@ -657,13 +340,29 @@ const watchers = new WeakMap();
657
340
  const effectChildren = new WeakMap();
658
341
  // Track parent effect relationships for hierarchy traversal (used in deep touch filtering)
659
342
  const effectParent = new WeakMap();
343
+ // Track reverse mapping to ensure unicity: One Root -> One Function
344
+ const reverseRoots = new WeakMap();
660
345
  /**
661
346
  * Marks a function with its root function for effect tracking
347
+ * Enforces strict unicity: A root function can only identify ONE function.
662
348
  * @param fn - The function to mark
663
349
  * @param root - The root function
664
350
  * @returns The marked function
665
351
  */
666
352
  function markWithRoot(fn, root) {
353
+ // Check for collision
354
+ const existingRef = reverseRoots.get(root);
355
+ const existing = existingRef?.deref();
356
+ if (existing && existing !== fn) {
357
+ const rootName = root.name || 'anonymous';
358
+ const existingName = existing.name || 'anonymous';
359
+ const fnName = fn.name || 'anonymous';
360
+ throw new Error(`[reactive] Abusive Shared Root detected: Root '${rootName}' is already identifying function '${existingName}'. ` +
361
+ `Cannot reuse it for '${fnName}'. Shared roots cause lost updates and broken identity logic.`);
362
+ }
363
+ // Always update the map so subsequent checks find this one
364
+ // (Last writer wins for the check)
365
+ reverseRoots.set(root, new WeakRef(fn));
667
366
  // Mark fn with the new root
668
367
  return Object.defineProperty(fn, rootFunction, {
669
368
  value: getRoot(root),
@@ -676,64 +375,15 @@ function markWithRoot(fn, root) {
676
375
  * @returns The root function
677
376
  */
678
377
  function getRoot(fn) {
679
- return fn?.[rootFunction] || fn;
378
+ while (fn && rootFunction in fn)
379
+ fn = fn[rootFunction];
380
+ return fn;
680
381
  }
681
382
  // Flag to disable dependency tracking for the current active effect (not globally)
682
383
  const trackingDisabledEffects = new WeakSet();
683
384
  let globalTrackingDisabled = false;
684
- function getTrackingDisabled() {
685
- const active = getActiveEffect();
686
- if (!active)
687
- return globalTrackingDisabled;
688
- return trackingDisabledEffects.has(getRoot(active));
689
- }
690
- function setTrackingDisabled(value) {
691
- const active = getActiveEffect();
692
- if (!active) {
693
- globalTrackingDisabled = value;
694
- return;
695
- }
696
- const root = getRoot(active);
697
- if (value)
698
- trackingDisabledEffects.add(root);
699
- else
700
- trackingDisabledEffects.delete(root);
701
- }
702
- /**
703
- * Marks a property as a dependency of the current effect
704
- * @param obj - The object containing the property
705
- * @param prop - The property name (defaults to allProps)
706
- */
707
- function dependant(obj, prop = allProps) {
708
- obj = unwrap(obj);
709
- const currentActiveEffect = getActiveEffect();
710
- // Early return if no active effect, tracking disabled, or invalid prop
711
- if (!currentActiveEffect ||
712
- getTrackingDisabled() ||
713
- (typeof prop === 'symbol' && prop !== allProps))
714
- return;
715
- registerDependency(obj, prop, currentActiveEffect);
716
- }
717
- function registerDependency(obj, prop, currentActiveEffect) {
718
- let objectWatchers = watchers.get(obj);
719
- if (!objectWatchers) {
720
- objectWatchers = new Map();
721
- watchers.set(obj, objectWatchers);
722
- }
723
- let deps = objectWatchers.get(prop);
724
- if (!deps) {
725
- deps = new Set();
726
- objectWatchers.set(prop, deps);
727
- }
728
- deps.add(currentActiveEffect);
729
- // Track which reactive objects this effect is watching
730
- const effectObjects = effectToReactiveObjects.get(currentActiveEffect);
731
- if (effectObjects) {
732
- effectObjects.add(obj);
733
- }
734
- else {
735
- effectToReactiveObjects.set(currentActiveEffect, new Set([obj]));
736
- }
385
+ function setGlobalTrackingDisabled(value) {
386
+ globalTrackingDisabled = value;
737
387
  }
738
388
 
739
389
  /**
@@ -1088,6 +738,171 @@ function addToMutationHistory(source, target, obj, prop, evolution) {
1088
738
  }
1089
739
  }
1090
740
 
741
+ /**
742
+ * Effect context stack for nested tracking (front = active, next = parent)
743
+ */
744
+ const stack = [];
745
+ function captureEffectStack() {
746
+ return stack.slice();
747
+ }
748
+ function isRunning(effect) {
749
+ const rootEffect = getRoot(effect);
750
+ // Check if the effect is directly in the stack
751
+ const rootIndex = stack.indexOf(rootEffect);
752
+ if (rootIndex !== -1) {
753
+ return stack.slice(0, rootIndex + 1).reverse();
754
+ }
755
+ // Check if any effect in the stack is a descendant of this effect
756
+ // (i.e., walk up the parent chain from each stack effect to see if we reach this effect)
757
+ for (let i = 0; i < stack.length; i++) {
758
+ const stackEffect = stack[i];
759
+ let current = stackEffect;
760
+ const visited = new WeakSet();
761
+ const ancestorChain = [];
762
+ // TODO: That's perhaps a lot of computations for an `assert`
763
+ // Walk up the parent chain to find if this effect is an ancestor
764
+ while (current && !visited.has(current)) {
765
+ visited.add(current);
766
+ const currentRoot = getRoot(current);
767
+ ancestorChain.push(currentRoot);
768
+ if (currentRoot === rootEffect) {
769
+ // Found a descendant - build the full chain from ancestor to active
770
+ // The ancestorChain contains [descendant, parent, ..., ancestor] (walking up)
771
+ // We need [ancestor (effect), ..., parent, descendant, ...stack from descendant to active]
772
+ const chainFromAncestor = ancestorChain.reverse(); // [ancestor, ..., descendant]
773
+ // Prepend the actual effect we're checking (in case current is a wrapper)
774
+ if (chainFromAncestor[0] !== rootEffect) {
775
+ chainFromAncestor.unshift(rootEffect);
776
+ }
777
+ // Append the rest of the stack from the descendant to the active effect
778
+ const stackFromDescendant = stack.slice(0, i + 1).reverse(); // [descendant, ..., active]
779
+ // Remove duplicate descendant (it's both at end of chainFromAncestor and start of stackFromDescendant)
780
+ if (chainFromAncestor.length > 0 && stackFromDescendant.length > 0) {
781
+ stackFromDescendant.shift(); // Remove duplicate descendant
782
+ }
783
+ return [...chainFromAncestor, ...stackFromDescendant];
784
+ }
785
+ current = effectParent.get(current);
786
+ }
787
+ }
788
+ return false;
789
+ }
790
+ function withEffectStack(snapshot, fn) {
791
+ const previousStack = stack.slice();
792
+ assignStack(snapshot);
793
+ try {
794
+ return fn();
795
+ }
796
+ finally {
797
+ assignStack(previousStack);
798
+ }
799
+ }
800
+ function getActiveEffect() {
801
+ return stack[0];
802
+ }
803
+ /**
804
+ * Executes a function with a specific effect context
805
+ * @param effect - The effect to use as context
806
+ * @param fn - The function to execute
807
+ * @param keepParent - Whether to keep the parent effect context
808
+ * @returns The result of the function
809
+ */
810
+ function withEffect(effect, fn) {
811
+ if (getRoot(effect) === getRoot(getActiveEffect()))
812
+ return fn();
813
+ stack.unshift(effect);
814
+ try {
815
+ return fn();
816
+ }
817
+ finally {
818
+ const recoveredEffect = stack.shift();
819
+ if (recoveredEffect !== effect)
820
+ throw new ReactiveError('[reactive] Effect stack mismatch');
821
+ }
822
+ }
823
+ function assignStack(values) {
824
+ stack.length = 0;
825
+ stack.push(...values);
826
+ }
827
+
828
+ const objectToProxy = new WeakMap();
829
+ const proxyToObject = new WeakMap();
830
+ function storeProxyRelationship(target, proxy) {
831
+ objectToProxy.set(target, proxy);
832
+ proxyToObject.set(proxy, target);
833
+ }
834
+ function getExistingProxy(target) {
835
+ return objectToProxy.get(target);
836
+ }
837
+ function trackProxyObject(proxy, target) {
838
+ proxyToObject.set(proxy, target);
839
+ }
840
+ function unwrap(obj) {
841
+ let current = obj;
842
+ while (current && typeof current === 'object' && current !== null && proxyToObject.has(current)) {
843
+ current = proxyToObject.get(current);
844
+ }
845
+ return current;
846
+ }
847
+ function isReactive(obj) {
848
+ return proxyToObject.has(obj);
849
+ }
850
+
851
+ function getTrackingDisabled() {
852
+ const active = getActiveEffect();
853
+ if (!active)
854
+ return globalTrackingDisabled;
855
+ return trackingDisabledEffects.has(getRoot(active));
856
+ }
857
+ function setTrackingDisabled(value) {
858
+ const active = getActiveEffect();
859
+ if (!active) {
860
+ setGlobalTrackingDisabled(value);
861
+ return;
862
+ }
863
+ const root = getRoot(active);
864
+ if (value)
865
+ trackingDisabledEffects.add(root);
866
+ else
867
+ trackingDisabledEffects.delete(root);
868
+ }
869
+ /**
870
+ * Marks a property as a dependency of the current effect
871
+ * @param obj - The object containing the property
872
+ * @param prop - The property name (defaults to allProps)
873
+ */
874
+ function dependant(obj, prop = allProps) {
875
+ obj = unwrap(obj);
876
+ const currentActiveEffect = getActiveEffect();
877
+ // Early return if no active effect, tracking disabled, or invalid prop
878
+ if (!currentActiveEffect ||
879
+ getTrackingDisabled() ||
880
+ (typeof prop === 'symbol' && prop !== allProps))
881
+ return;
882
+ registerDependency(obj, prop, currentActiveEffect);
883
+ }
884
+ function registerDependency(obj, prop, currentActiveEffect) {
885
+ let objectWatchers = watchers.get(obj);
886
+ if (!objectWatchers) {
887
+ objectWatchers = new Map();
888
+ watchers.set(obj, objectWatchers);
889
+ }
890
+ let deps = objectWatchers.get(prop);
891
+ if (!deps) {
892
+ deps = new Set();
893
+ objectWatchers.set(prop, deps);
894
+ }
895
+ deps.add(currentActiveEffect);
896
+ // Track which reactive objects this effect is watching
897
+ const effectObjects = effectToReactiveObjects.get(currentActiveEffect);
898
+ if (effectObjects) {
899
+ effectObjects.add(obj);
900
+ }
901
+ else {
902
+ effectToReactiveObjects.set(currentActiveEffect, new Set([obj]));
903
+ }
904
+ }
905
+
1091
906
  /**
1092
907
  * Zone-like async context preservation for reactive effects
1093
908
  *
@@ -1473,6 +1288,7 @@ function addGraphEdge(callerRoot, targetRoot) {
1473
1288
  * @param end - Target node
1474
1289
  * @param exclude - Node to exclude from the path
1475
1290
  * @returns true if a path exists without going through the excluded node
1291
+ * @todo Can be REALLY costly - optimise or make optional or ...
1476
1292
  */
1477
1293
  function hasPathExcluding(start, end, exclude) {
1478
1294
  if (start === end)
@@ -1760,6 +1576,12 @@ function wouldCreateCycle(callerRoot, targetRoot) {
1760
1576
  * @param immediate - If true, don't create edges in the dependency graph
1761
1577
  */
1762
1578
  function addToBatch(effect, caller, immediate) {
1579
+ const cleanupFn = effect[cleanup$1];
1580
+ if (cleanupFn)
1581
+ cleanupFn();
1582
+ // If the effect was stopped during cleanup (e.g. lazy memoization), don't add it to the batch
1583
+ if (effect[stopped])
1584
+ return;
1763
1585
  if (!batchQueue)
1764
1586
  return;
1765
1587
  const root = getRoot(effect);
@@ -2195,12 +2017,18 @@ function batch(effect, immediate) {
2195
2017
  const atomic = decorator({
2196
2018
  method(original) {
2197
2019
  return function (...args) {
2198
- return batch(markWithRoot(() => original.apply(this, args), original), 'immediate');
2020
+ const atomicEffect = () => original.apply(this, args);
2021
+ // Debug: helpful to have a name
2022
+ Object.defineProperty(atomicEffect, 'name', { value: `atomic(${original.name})` });
2023
+ return batch(atomicEffect, 'immediate');
2199
2024
  };
2200
2025
  },
2201
2026
  default(original) {
2202
2027
  return function (...args) {
2203
- return batch(markWithRoot(() => original.apply(this, args), original), 'immediate');
2028
+ const atomicEffect = () => original.apply(this, args);
2029
+ // Debug: helpful to have a name
2030
+ Object.defineProperty(atomicEffect, 'name', { value: `atomic(${original.name})` });
2031
+ return batch(atomicEffect, 'immediate');
2204
2032
  };
2205
2033
  },
2206
2034
  });
@@ -2234,7 +2062,7 @@ fn, effectOptions) {
2234
2062
  let cleanup = null;
2235
2063
  // capture the parent effect at creation time for ascend
2236
2064
  const parentsForAscend = captureEffectStack();
2237
- const tracked = markWithRoot((cb) => withEffect(runEffect, cb), fn);
2065
+ const tracked = (cb) => withEffect(runEffect, cb);
2238
2066
  const ascend = (cb) => withEffectStack(parentsForAscend, cb);
2239
2067
  let effectStopped = false;
2240
2068
  let hasReacted = false;
@@ -2367,8 +2195,25 @@ fn, effectOptions) {
2367
2195
  cleanupEffectFromGraph(runEffect);
2368
2196
  fr.unregister(stopEffect);
2369
2197
  };
2198
+ function augmentedRv(rv) {
2199
+ Object.defineProperty(rv, stopped, {
2200
+ get() {
2201
+ return effectStopped;
2202
+ },
2203
+ });
2204
+ Object.defineProperty(rv, cleanup$1, {
2205
+ value: () => {
2206
+ if (cleanup) {
2207
+ const prevCleanup = cleanup;
2208
+ cleanup = null;
2209
+ withEffect(undefined, () => prevCleanup());
2210
+ }
2211
+ },
2212
+ });
2213
+ return rv;
2214
+ }
2370
2215
  if (isRootEffect) {
2371
- const callIfCollected = () => stopEffect();
2216
+ const callIfCollected = augmentedRv(() => stopEffect());
2372
2217
  fr.register(callIfCollected, () => {
2373
2218
  stopEffect();
2374
2219
  options.garbageCollected(fn);
@@ -2381,14 +2226,14 @@ fn, effectOptions) {
2381
2226
  children = new Set();
2382
2227
  effectChildren.set(parent, children);
2383
2228
  }
2384
- const subEffectCleanup = () => {
2229
+ const subEffectCleanup = augmentedRv(() => {
2385
2230
  children.delete(subEffectCleanup);
2386
2231
  if (children.size === 0) {
2387
2232
  effectChildren.delete(parent);
2388
2233
  }
2389
2234
  // Execute this child effect cleanup (which triggers its own mainCleanup)
2390
2235
  stopEffect();
2391
- };
2236
+ });
2392
2237
  children.add(subEffectCleanup);
2393
2238
  return subEffectCleanup;
2394
2239
  }
@@ -2987,13 +2832,13 @@ const reactiveHandlers = {
2987
2832
  const receiverDesc = Object.getOwnPropertyDescriptor(unwrappedReceiver, prop);
2988
2833
  const targetDesc = Object.getOwnPropertyDescriptor(unwrappedObj, prop);
2989
2834
  const desc = receiverDesc || targetDesc;
2990
- // If it's a getter-only accessor (has getter but no setter), read without tracking
2991
- // to avoid breaking memoization invalidation when the getter calls memoized functions
2835
+ // We *need* to use `receiver` and not `unwrappedObj` here, otherwise we break
2836
+ // the dependency tracking for memoized getters
2992
2837
  if (desc?.get && !desc?.set) {
2993
- oldVal = withEffect(undefined, () => Reflect.get(unwrappedObj, prop, unwrappedReceiver));
2838
+ oldVal = withEffect(undefined, () => Reflect.get(unwrappedObj, prop, receiver));
2994
2839
  }
2995
2840
  else {
2996
- oldVal = Reflect.get(unwrappedObj, prop, unwrappedReceiver);
2841
+ oldVal = withEffect(undefined, () => Reflect.get(unwrappedObj, prop, receiver));
2997
2842
  }
2998
2843
  }
2999
2844
  if (objectsWithDeepWatchers.has(obj)) {
@@ -3283,12 +3128,12 @@ function watchObject(value, changed, { immediate = false, deep = false } = {}) {
3283
3128
  const myParentEffect = getActiveEffect();
3284
3129
  if (deep)
3285
3130
  return deepWatch(value, changed, { immediate });
3286
- return effect(markWithRoot(function watchObjectEffect() {
3131
+ return effect(function watchObjectEffect() {
3287
3132
  dependant(value);
3288
3133
  if (immediate)
3289
3134
  withEffect(myParentEffect, () => changed(value));
3290
3135
  immediate = true;
3291
- }, changed));
3136
+ });
3292
3137
  }
3293
3138
  function watchCallBack(value, changed, { immediate = false, deep = false } = {}) {
3294
3139
  const myParentEffect = getActiveEffect();
@@ -3297,7 +3142,7 @@ function watchCallBack(value, changed, { immediate = false, deep = false } = {})
3297
3142
  const cbCleanup = effect(markWithRoot(function watchCallBackEffect(access) {
3298
3143
  const newValue = value(access);
3299
3144
  if (oldValue !== newValue)
3300
- withEffect(myParentEffect, markWithRoot(() => {
3145
+ withEffect(myParentEffect, () => {
3301
3146
  if (oldValue === unsetYet) {
3302
3147
  if (immediate)
3303
3148
  changed(newValue);
@@ -3308,9 +3153,9 @@ function watchCallBack(value, changed, { immediate = false, deep = false } = {})
3308
3153
  if (deep) {
3309
3154
  if (deepCleanup)
3310
3155
  deepCleanup();
3311
- deepCleanup = deepWatch(newValue, markWithRoot((value) => changed(value, value), changed));
3156
+ deepCleanup = deepWatch(newValue, (value) => changed(value, value));
3312
3157
  }
3313
- }, changed));
3158
+ });
3314
3159
  }, value));
3315
3160
  return () => {
3316
3161
  cbCleanup();
@@ -3383,9 +3228,9 @@ function cleanedBy(obj, cleanupFn) {
3383
3228
  */
3384
3229
  function derived(compute) {
3385
3230
  const rv = { value: undefined };
3386
- return cleanedBy(rv, untracked(() => effect(markWithRoot(function derivedEffect(access) {
3231
+ return cleanedBy(rv, untracked(() => effect(function derivedEffect(access) {
3387
3232
  rv.value = compute(access);
3388
- }, compute))));
3233
+ })));
3389
3234
  }
3390
3235
 
3391
3236
  /**
@@ -3841,6 +3686,7 @@ function reduced(inputs, compute) {
3841
3686
  }
3842
3687
 
3843
3688
  const memoizedRegistry = new WeakMap();
3689
+ const wrapperRegistry = new WeakMap();
3844
3690
  function getBranch(tree, key) {
3845
3691
  tree.branches ?? (tree.branches = new WeakMap());
3846
3692
  let branch = tree.branches.get(key);
@@ -3861,15 +3707,30 @@ function memoizeFunction(fn) {
3861
3707
  if (localArgs.some((arg) => !(arg && ['object', 'symbol', 'function'].includes(typeof arg))))
3862
3708
  throw new Error('memoize expects non-null object arguments');
3863
3709
  let node = cacheRoot;
3710
+ // Note: decorators add `this` as first argument
3864
3711
  for (const arg of localArgs) {
3865
3712
  node = getBranch(node, arg);
3866
3713
  }
3867
3714
  dependant(node, 'memoize');
3868
- if ('result' in node)
3715
+ if ('result' in node) {
3716
+ if (options.onMemoizationDiscrepancy) {
3717
+ const wasVerification = options.isVerificationRun;
3718
+ options.isVerificationRun = true;
3719
+ try {
3720
+ const fresh = untracked(() => fn(...localArgs));
3721
+ if (!deepCompare(node.result, fresh)) {
3722
+ options.onMemoizationDiscrepancy(node.result, fresh, fn, localArgs, 'calculation');
3723
+ }
3724
+ }
3725
+ finally {
3726
+ options.isVerificationRun = wasVerification;
3727
+ }
3728
+ }
3869
3729
  return node.result;
3730
+ }
3870
3731
  // Create memoize internal effect to track dependencies and invalidate cache
3871
3732
  // Use untracked to prevent the effect creation from being affected by parent effects
3872
- node.cleanup = root(() => effect(markWithRoot(() => {
3733
+ node.cleanup = root(() => effect(() => {
3873
3734
  // Execute the function and track its dependencies
3874
3735
  // The function execution will automatically track dependencies on reactive objects
3875
3736
  node.result = fn(...localArgs);
@@ -3877,8 +3738,27 @@ function memoizeFunction(fn) {
3877
3738
  // When dependencies change, clear the cache and notify consumers
3878
3739
  delete node.result;
3879
3740
  touched1(node, { type: 'invalidate', prop: localArgs }, 'memoize');
3741
+ // Lazy memoization: stop the effect so it doesn't re-run immediately.
3742
+ // It will be re-created on next access.
3743
+ if (node.cleanup) {
3744
+ node.cleanup();
3745
+ node.cleanup = undefined;
3746
+ }
3880
3747
  };
3881
- }, fnRoot), { opaque: true }));
3748
+ }, { opaque: true }));
3749
+ if (options.onMemoizationDiscrepancy) {
3750
+ const wasVerification = options.isVerificationRun;
3751
+ options.isVerificationRun = true;
3752
+ try {
3753
+ const fresh = untracked(() => fn(...localArgs));
3754
+ if (!deepCompare(node.result, fresh)) {
3755
+ options.onMemoizationDiscrepancy(node.result, fresh, fn, localArgs, 'comparison');
3756
+ }
3757
+ }
3758
+ finally {
3759
+ options.isVerificationRun = wasVerification;
3760
+ }
3761
+ }
3882
3762
  return node.result;
3883
3763
  }, fn);
3884
3764
  memoizedRegistry.set(fnRoot, memoized);
@@ -3886,19 +3766,37 @@ function memoizeFunction(fn) {
3886
3766
  return memoized;
3887
3767
  }
3888
3768
  const memoize = decorator({
3889
- getter(original, propertyKey) {
3890
- const memoized = memoizeFunction(markWithRoot(renamed((that) => {
3891
- return original.call(that);
3892
- }, `${String(this.constructor.name)}.${String(propertyKey)}`), original));
3769
+ getter(original, target, propertyKey) {
3893
3770
  return function () {
3771
+ let wrapper = wrapperRegistry.get(original);
3772
+ if (!wrapper) {
3773
+ wrapper = markWithRoot(renamed((that) => {
3774
+ return original.call(that);
3775
+ }, `${String(target?.constructor?.name ?? target?.name ?? 'Object')}.${String(propertyKey)}`), {
3776
+ method: original,
3777
+ propertyKey,
3778
+ ...(original[rootFunction] ? { [rootFunction]: original[rootFunction] } : {}),
3779
+ });
3780
+ wrapperRegistry.set(original, wrapper);
3781
+ }
3782
+ const memoized = memoizeFunction(wrapper);
3894
3783
  return memoized(this);
3895
3784
  };
3896
3785
  },
3897
- method(original, name) {
3898
- const memoized = memoizeFunction(markWithRoot(renamed((that, ...args) => {
3899
- return original.call(that, ...args);
3900
- }, `${String(this.constructor.name)}.${String(name)}`), original));
3786
+ method(original, target, name) {
3901
3787
  return function (...args) {
3788
+ let wrapper = wrapperRegistry.get(original);
3789
+ if (!wrapper) {
3790
+ wrapper = markWithRoot(renamed((that, ...args) => {
3791
+ return original.call(that, ...args);
3792
+ }, `${String(target?.constructor?.name ?? target?.name ?? 'Object')}.${String(name)}`), {
3793
+ method: original,
3794
+ propertyKey: name,
3795
+ ...(original[rootFunction] ? { [rootFunction]: original[rootFunction] } : {}),
3796
+ });
3797
+ wrapperRegistry.set(original, wrapper);
3798
+ }
3799
+ const memoized = memoizeFunction(wrapper);
3902
3800
  return memoized(this, ...args);
3903
3801
  };
3904
3802
  },
@@ -5003,5 +4901,5 @@ const profileInfo = {
5003
4901
  nonReactiveObjects,
5004
4902
  };
5005
4903
 
5006
- export { cleanup as A, derived as B, unreactive as C, watch as D, mapped as E, reduced as F, memoize as G, immutables as H, IterableWeakMap as I, isNonReactive as J, registerNativeReactivity as K, getActiveProjection as L, project as M, isReactive as N, ReactiveBase as O, reactive as P, unwrap as Q, ReadOnlyError as R, organize as S, organized as T, Register as U, register as V, options as W, ReactiveError as X, isZoneEnabled as Y, setZoneEnabled as Z, IterableWeakSet as a, touched1 as b, buildReactivityGraph as c, registerObjectForDebug as d, enableDevTools as e, setObjectName as f, getState as g, deepWatch as h, isDevtoolsEnabled as i, addBatchCleanup as j, atomic as k, batch as l, mixin as m, biDi as n, defer as o, profileInfo as p, effect as q, registerEffectForDebug as r, setEffectName as s, touched as t, getActivationLog as u, getActiveEffect as v, root as w, trackEffect as x, untracked as y, cleanedBy as z };
5007
- //# sourceMappingURL=index-qiWwozOc.esm.js.map
4904
+ export { mixin as A, organize as B, organized as C, profileInfo as D, project as E, reactive as F, reduced as G, register as H, IterableWeakMap as I, registerEffectForDebug as J, registerNativeReactivity as K, registerObjectForDebug as L, root as M, setEffectName as N, setObjectName as O, setZoneEnabled as P, touched as Q, ReactiveBase as R, touched1 as S, trackEffect as T, unreactive as U, untracked as V, unwrap as W, watch as X, IterableWeakSet as a, ReadOnlyError as b, Register as c, addBatchCleanup as d, atomic as e, batch as f, biDi as g, buildReactivityGraph as h, cleanedBy as i, cleanup as j, deepWatch as k, defer as l, derived as m, effect as n, enableDevTools as o, getActivationLog as p, getActiveEffect as q, getActiveProjection as r, getState as s, immutables as t, isDevtoolsEnabled as u, isNonReactive as v, isReactive as w, isZoneEnabled as x, mapped as y, memoize as z };
4905
+ //# sourceMappingURL=index-DiP0RXoZ.esm.js.map