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
1
  'use strict';
2
2
 
3
- var decorator = require('./decorator-DLvrD0UF.js');
3
+ var decorator = require('./decorator-BQ2eBTCj.js');
4
4
  var indexable = require('../indexable.js');
5
5
  var _tslib = require('./_tslib-BgjropY9.js');
6
6
 
@@ -334,323 +334,6 @@ function mixin(mixinFunction, unwrapFunction) {
334
334
  });
335
335
  }
336
336
 
337
- // biome-ignore-all lint/suspicious/noConfusingVoidType: Type 'void' is not assignable to type 'ScopedCallback | undefined'.
338
- // Argument of type '() => void' is not assignable to parameter of type '(dep: DependencyFunction) => ScopedCallback | undefined'.
339
- // Track native reactivity
340
- const nativeReactive = Symbol('native-reactive');
341
- /**
342
- * Symbol to mark individual objects as non-reactive
343
- */
344
- const nonReactiveMark = Symbol('non-reactive');
345
- /**
346
- * Symbol to mark class properties as non-reactive
347
- */
348
- const unreactiveProperties = Symbol('unreactive-properties');
349
- /**
350
- * Symbol for prototype forwarding in reactive objects
351
- */
352
- const prototypeForwarding = Symbol('prototype-forwarding');
353
- /**
354
- * Symbol representing all properties in reactive tracking
355
- */
356
- const allProps = Symbol('all-props');
357
- /**
358
- * Symbol for accessing projection information on reactive objects
359
- */
360
- const projectionInfo = Symbol('projection-info');
361
- // Symbol to mark functions with their root function
362
- const rootFunction = Symbol('root-function');
363
- /**
364
- * Structured error codes for machine-readable diagnosis
365
- */
366
- var ReactiveErrorCode;
367
- (function (ReactiveErrorCode) {
368
- ReactiveErrorCode["CycleDetected"] = "CYCLE_DETECTED";
369
- ReactiveErrorCode["MaxDepthExceeded"] = "MAX_DEPTH_EXCEEDED";
370
- ReactiveErrorCode["MaxReactionExceeded"] = "MAX_REACTION_EXCEEDED";
371
- ReactiveErrorCode["WriteInComputed"] = "WRITE_IN_COMPUTED";
372
- ReactiveErrorCode["TrackingError"] = "TRACKING_ERROR";
373
- })(ReactiveErrorCode || (ReactiveErrorCode = {}));
374
- /**
375
- * Error class for reactive system errors
376
- */
377
- class ReactiveError extends Error {
378
- constructor(message, debugInfo) {
379
- super(message);
380
- this.debugInfo = debugInfo;
381
- this.name = 'ReactiveError';
382
- }
383
- }
384
- // biome-ignore-start lint/correctness/noUnusedFunctionParameters: Interface declaration with empty defaults
385
- /**
386
- * Global options for the reactive system
387
- */
388
- const options = {
389
- /**
390
- * Debug purpose: called when an effect is entered
391
- * @param effect - The effect that is entered
392
- */
393
- enter: (_effect) => { },
394
- /**
395
- * Debug purpose: called when an effect is left
396
- * @param effect - The effect that is left
397
- */
398
- leave: (_effect) => { },
399
- /**
400
- * Debug purpose: called when an effect is chained
401
- * @param target - The effect that is being triggered
402
- * @param caller - The effect that is calling the target
403
- */
404
- chain: (_targets, _caller) => { },
405
- /**
406
- * Debug purpose: called when an effect chain is started
407
- * @param target - The effect that is being triggered
408
- */
409
- beginChain: (_targets) => { },
410
- /**
411
- * Debug purpose: called when an effect chain is ended
412
- */
413
- endChain: () => { },
414
- garbageCollected: (_fn) => { },
415
- /**
416
- * Debug purpose: called when an object is touched
417
- * @param obj - The object that is touched
418
- * @param evolution - The type of change
419
- * @param props - The properties that changed
420
- * @param deps - The dependencies that changed
421
- */
422
- touched: (_obj, _evolution, _props, _deps) => { },
423
- /**
424
- * Debug purpose: called when an effect is skipped because it's already running
425
- * @param effect - The effect that is already running
426
- * @param runningChain - The array of effects from the detected one to the currently running one
427
- */
428
- skipRunningEffect: (_effect, _runningChain) => { },
429
- /**
430
- * Debug purpose: maximum effect chain (like call stack max depth)
431
- * Used to prevent infinite loops
432
- * @default 100
433
- */
434
- maxEffectChain: 100,
435
- /**
436
- * Maximum number of times an effect can be triggered by the same cause in a single batch
437
- * Used to detect aggressive re-computation or infinite loops
438
- * @default 10
439
- */
440
- maxTriggerPerBatch: 10,
441
- /**
442
- * Debug purpose: maximum effect reaction (like call stack max depth)
443
- * Used to prevent infinite loops
444
- * @default 'throw'
445
- */
446
- maxEffectReaction: 'throw',
447
- /**
448
- * How to handle cycles detected in effect batches
449
- * - 'throw': Throw an error with cycle information (default, recommended for development)
450
- * - 'warn': Log a warning and break the cycle by executing one effect
451
- * - 'break': Silently break the cycle by executing one effect (recommended for production)
452
- * - 'strict': Prevent cycle creation by checking graph before execution (throws error)
453
- * @default 'throw'
454
- */
455
- cycleHandling: 'throw',
456
- /**
457
- * Maximum depth for deep watching traversal
458
- * Used to prevent infinite recursion in circular references
459
- * @default 100
460
- */
461
- maxDeepWatchDepth: 100,
462
- /**
463
- * Only react on instance members modification (not inherited properties)
464
- * For instance, do not track class methods
465
- * @default true
466
- */
467
- instanceMembers: true,
468
- /**
469
- * Ignore accessors (getters and setters) and only track direct properties
470
- * @default true
471
- */
472
- ignoreAccessors: true,
473
- /**
474
- * Enable recursive touching when objects with the same prototype are replaced
475
- * When enabled, replacing an object with another of the same prototype triggers
476
- * recursive diffing instead of notifying parent effects
477
- * @default true
478
- */
479
- recursiveTouching: true,
480
- /**
481
- * Default async execution mode for effects that return Promises
482
- * - 'cancel': Cancel previous async execution when dependencies change (default, enables async zone)
483
- * - 'queue': Queue next execution to run after current completes (enables async zone)
484
- * - 'ignore': Ignore new executions while async work is running (enables async zone)
485
- * - false: Disable async zone and async mode handling (effects run concurrently)
486
- *
487
- * **When truthy:** Enables async zone (Promise.prototype wrapping) for automatic context
488
- * preservation in Promise callbacks. Warning: This modifies Promise.prototype globally.
489
- * Only enable if no other library modifies Promise.prototype.
490
- *
491
- * **When false:** Async zone is disabled. Use `tracked()` manually in Promise callbacks.
492
- *
493
- * Can be overridden per-effect via EffectOptions
494
- * @default 'cancel'
495
- */
496
- asyncMode: 'cancel',
497
- // biome-ignore lint/suspicious/noConsole: This is the whole point here
498
- warn: (...args) => console.warn(...args),
499
- /**
500
- * Configuration for the introspection system
501
- */
502
- introspection: {
503
- /**
504
- * Whether to keep a history of mutations for debugging
505
- * @default false
506
- */
507
- enableHistory: false,
508
- /**
509
- * Number of mutations to keep in history
510
- * @default 50
511
- */
512
- historySize: 50,
513
- },
514
- /**
515
- * Configuration for zone hooks - control which async APIs are hooked
516
- * Each option controls whether the corresponding async API is wrapped to preserve effect context
517
- * Only applies when asyncMode is enabled (truthy)
518
- */
519
- zones: {
520
- /**
521
- * Hook setTimeout to preserve effect context
522
- * @default true
523
- */
524
- setTimeout: true,
525
- /**
526
- * Hook setInterval to preserve effect context
527
- * @default true
528
- */
529
- setInterval: true,
530
- /**
531
- * Hook requestAnimationFrame (runs in untracked context when hooked)
532
- * @default true
533
- */
534
- requestAnimationFrame: true,
535
- /**
536
- * Hook queueMicrotask to preserve effect context
537
- * @default true
538
- */
539
- queueMicrotask: true,
540
- },
541
- };
542
-
543
- /**
544
- * Effect context stack for nested tracking (front = active, next = parent)
545
- */
546
- const stack = [];
547
- function captureEffectStack() {
548
- return stack.slice();
549
- }
550
- function isRunning(effect) {
551
- const rootEffect = getRoot(effect);
552
- // Check if the effect is directly in the stack
553
- const rootIndex = stack.indexOf(rootEffect);
554
- if (rootIndex !== -1) {
555
- return stack.slice(0, rootIndex + 1).reverse();
556
- }
557
- // Check if any effect in the stack is a descendant of this effect
558
- // (i.e., walk up the parent chain from each stack effect to see if we reach this effect)
559
- for (let i = 0; i < stack.length; i++) {
560
- const stackEffect = stack[i];
561
- let current = stackEffect;
562
- const visited = new WeakSet();
563
- const ancestorChain = [];
564
- // TODO: That's perhaps a lot of computations for an `assert`
565
- // Walk up the parent chain to find if this effect is an ancestor
566
- while (current && !visited.has(current)) {
567
- visited.add(current);
568
- const currentRoot = getRoot(current);
569
- ancestorChain.push(currentRoot);
570
- if (currentRoot === rootEffect) {
571
- // Found a descendant - build the full chain from ancestor to active
572
- // The ancestorChain contains [descendant, parent, ..., ancestor] (walking up)
573
- // We need [ancestor (effect), ..., parent, descendant, ...stack from descendant to active]
574
- const chainFromAncestor = ancestorChain.reverse(); // [ancestor, ..., descendant]
575
- // Prepend the actual effect we're checking (in case current is a wrapper)
576
- if (chainFromAncestor[0] !== rootEffect) {
577
- chainFromAncestor.unshift(rootEffect);
578
- }
579
- // Append the rest of the stack from the descendant to the active effect
580
- const stackFromDescendant = stack.slice(0, i + 1).reverse(); // [descendant, ..., active]
581
- // Remove duplicate descendant (it's both at end of chainFromAncestor and start of stackFromDescendant)
582
- if (chainFromAncestor.length > 0 && stackFromDescendant.length > 0) {
583
- stackFromDescendant.shift(); // Remove duplicate descendant
584
- }
585
- return [...chainFromAncestor, ...stackFromDescendant];
586
- }
587
- current = effectParent.get(current);
588
- }
589
- }
590
- return false;
591
- }
592
- function withEffectStack(snapshot, fn) {
593
- const previousStack = stack.slice();
594
- assignStack(snapshot);
595
- try {
596
- return fn();
597
- }
598
- finally {
599
- assignStack(previousStack);
600
- }
601
- }
602
- function getActiveEffect() {
603
- return stack[0];
604
- }
605
- /**
606
- * Executes a function with a specific effect context
607
- * @param effect - The effect to use as context
608
- * @param fn - The function to execute
609
- * @param keepParent - Whether to keep the parent effect context
610
- * @returns The result of the function
611
- */
612
- function withEffect(effect, fn) {
613
- // console.log('[Mutts] withEffect', effect ? 'Active' : 'NULL');
614
- if (getRoot(effect) === getRoot(getActiveEffect()))
615
- return fn();
616
- stack.unshift(effect);
617
- try {
618
- return fn();
619
- }
620
- finally {
621
- const recoveredEffect = stack.shift();
622
- if (recoveredEffect !== effect)
623
- throw new ReactiveError('[reactive] Effect stack mismatch');
624
- }
625
- }
626
- function assignStack(values) {
627
- stack.length = 0;
628
- stack.push(...values);
629
- }
630
-
631
- const objectToProxy = new WeakMap();
632
- const proxyToObject = new WeakMap();
633
- function storeProxyRelationship(target, proxy) {
634
- objectToProxy.set(target, proxy);
635
- proxyToObject.set(proxy, target);
636
- }
637
- function getExistingProxy(target) {
638
- return objectToProxy.get(target);
639
- }
640
- function trackProxyObject(proxy, target) {
641
- proxyToObject.set(proxy, target);
642
- }
643
- function unwrap(obj) {
644
- let current = obj;
645
- while (current && typeof current === 'object' && current !== null && proxyToObject.has(current)) {
646
- current = proxyToObject.get(current);
647
- }
648
- return current;
649
- }
650
- function isReactive(obj) {
651
- return proxyToObject.has(obj);
652
- }
653
-
654
337
  // Track which effects are watching which reactive objects for cleanup
655
338
  const effectToReactiveObjects = new WeakMap();
656
339
  // Track effects per reactive object and property
@@ -659,15 +342,31 @@ const watchers = new WeakMap();
659
342
  const effectChildren = new WeakMap();
660
343
  // Track parent effect relationships for hierarchy traversal (used in deep touch filtering)
661
344
  const effectParent = new WeakMap();
345
+ // Track reverse mapping to ensure unicity: One Root -> One Function
346
+ const reverseRoots = new WeakMap();
662
347
  /**
663
348
  * Marks a function with its root function for effect tracking
349
+ * Enforces strict unicity: A root function can only identify ONE function.
664
350
  * @param fn - The function to mark
665
351
  * @param root - The root function
666
352
  * @returns The marked function
667
353
  */
668
354
  function markWithRoot(fn, root) {
355
+ // Check for collision
356
+ const existingRef = reverseRoots.get(root);
357
+ const existing = existingRef?.deref();
358
+ if (existing && existing !== fn) {
359
+ const rootName = root.name || 'anonymous';
360
+ const existingName = existing.name || 'anonymous';
361
+ const fnName = fn.name || 'anonymous';
362
+ throw new Error(`[reactive] Abusive Shared Root detected: Root '${rootName}' is already identifying function '${existingName}'. ` +
363
+ `Cannot reuse it for '${fnName}'. Shared roots cause lost updates and broken identity logic.`);
364
+ }
365
+ // Always update the map so subsequent checks find this one
366
+ // (Last writer wins for the check)
367
+ reverseRoots.set(root, new WeakRef(fn));
669
368
  // Mark fn with the new root
670
- return Object.defineProperty(fn, rootFunction, {
369
+ return Object.defineProperty(fn, decorator.rootFunction, {
671
370
  value: getRoot(root),
672
371
  writable: false,
673
372
  });
@@ -678,64 +377,15 @@ function markWithRoot(fn, root) {
678
377
  * @returns The root function
679
378
  */
680
379
  function getRoot(fn) {
681
- return fn?.[rootFunction] || fn;
380
+ while (fn && decorator.rootFunction in fn)
381
+ fn = fn[decorator.rootFunction];
382
+ return fn;
682
383
  }
683
384
  // Flag to disable dependency tracking for the current active effect (not globally)
684
385
  const trackingDisabledEffects = new WeakSet();
685
386
  let globalTrackingDisabled = false;
686
- function getTrackingDisabled() {
687
- const active = getActiveEffect();
688
- if (!active)
689
- return globalTrackingDisabled;
690
- return trackingDisabledEffects.has(getRoot(active));
691
- }
692
- function setTrackingDisabled(value) {
693
- const active = getActiveEffect();
694
- if (!active) {
695
- globalTrackingDisabled = value;
696
- return;
697
- }
698
- const root = getRoot(active);
699
- if (value)
700
- trackingDisabledEffects.add(root);
701
- else
702
- trackingDisabledEffects.delete(root);
703
- }
704
- /**
705
- * Marks a property as a dependency of the current effect
706
- * @param obj - The object containing the property
707
- * @param prop - The property name (defaults to allProps)
708
- */
709
- function dependant(obj, prop = allProps) {
710
- obj = unwrap(obj);
711
- const currentActiveEffect = getActiveEffect();
712
- // Early return if no active effect, tracking disabled, or invalid prop
713
- if (!currentActiveEffect ||
714
- getTrackingDisabled() ||
715
- (typeof prop === 'symbol' && prop !== allProps))
716
- return;
717
- registerDependency(obj, prop, currentActiveEffect);
718
- }
719
- function registerDependency(obj, prop, currentActiveEffect) {
720
- let objectWatchers = watchers.get(obj);
721
- if (!objectWatchers) {
722
- objectWatchers = new Map();
723
- watchers.set(obj, objectWatchers);
724
- }
725
- let deps = objectWatchers.get(prop);
726
- if (!deps) {
727
- deps = new Set();
728
- objectWatchers.set(prop, deps);
729
- }
730
- deps.add(currentActiveEffect);
731
- // Track which reactive objects this effect is watching
732
- const effectObjects = effectToReactiveObjects.get(currentActiveEffect);
733
- if (effectObjects) {
734
- effectObjects.add(obj);
735
- }
736
- else {
737
- effectToReactiveObjects.set(currentActiveEffect, new Set([obj]));
738
- }
387
+ function setGlobalTrackingDisabled(value) {
388
+ globalTrackingDisabled = value;
739
389
  }
740
390
 
741
391
  /**
@@ -776,7 +426,7 @@ function ensureObjectName(obj) {
776
426
  }
777
427
  function describeProp(obj, prop) {
778
428
  const objectName = ensureObjectName(obj);
779
- if (prop === allProps)
429
+ if (prop === decorator.allProps)
780
430
  return `${objectName}.*`;
781
431
  if (typeof prop === 'symbol')
782
432
  return `${objectName}.${prop.description ?? prop.toString()}`;
@@ -873,7 +523,7 @@ function registerObjectForDebug(obj) {
873
523
  * @param evolution - The type of change (set/add/del/bunch)
874
524
  */
875
525
  function recordTriggerLink(source, target, obj, prop, evolution) {
876
- if (options.introspection.enableHistory) {
526
+ if (decorator.options.introspection.enableHistory) {
877
527
  addToMutationHistory(source, target, obj, prop, evolution);
878
528
  }
879
529
  if (!devtoolsEnabled)
@@ -1085,11 +735,176 @@ function addToMutationHistory(source, target, obj, prop, evolution) {
1085
735
  type: evolution.type,
1086
736
  };
1087
737
  mutationHistory.push(record);
1088
- if (mutationHistory.length > options.introspection.historySize) {
738
+ if (mutationHistory.length > decorator.options.introspection.historySize) {
1089
739
  mutationHistory.shift();
1090
740
  }
1091
741
  }
1092
742
 
743
+ /**
744
+ * Effect context stack for nested tracking (front = active, next = parent)
745
+ */
746
+ const stack = [];
747
+ function captureEffectStack() {
748
+ return stack.slice();
749
+ }
750
+ function isRunning(effect) {
751
+ const rootEffect = getRoot(effect);
752
+ // Check if the effect is directly in the stack
753
+ const rootIndex = stack.indexOf(rootEffect);
754
+ if (rootIndex !== -1) {
755
+ return stack.slice(0, rootIndex + 1).reverse();
756
+ }
757
+ // Check if any effect in the stack is a descendant of this effect
758
+ // (i.e., walk up the parent chain from each stack effect to see if we reach this effect)
759
+ for (let i = 0; i < stack.length; i++) {
760
+ const stackEffect = stack[i];
761
+ let current = stackEffect;
762
+ const visited = new WeakSet();
763
+ const ancestorChain = [];
764
+ // TODO: That's perhaps a lot of computations for an `assert`
765
+ // Walk up the parent chain to find if this effect is an ancestor
766
+ while (current && !visited.has(current)) {
767
+ visited.add(current);
768
+ const currentRoot = getRoot(current);
769
+ ancestorChain.push(currentRoot);
770
+ if (currentRoot === rootEffect) {
771
+ // Found a descendant - build the full chain from ancestor to active
772
+ // The ancestorChain contains [descendant, parent, ..., ancestor] (walking up)
773
+ // We need [ancestor (effect), ..., parent, descendant, ...stack from descendant to active]
774
+ const chainFromAncestor = ancestorChain.reverse(); // [ancestor, ..., descendant]
775
+ // Prepend the actual effect we're checking (in case current is a wrapper)
776
+ if (chainFromAncestor[0] !== rootEffect) {
777
+ chainFromAncestor.unshift(rootEffect);
778
+ }
779
+ // Append the rest of the stack from the descendant to the active effect
780
+ const stackFromDescendant = stack.slice(0, i + 1).reverse(); // [descendant, ..., active]
781
+ // Remove duplicate descendant (it's both at end of chainFromAncestor and start of stackFromDescendant)
782
+ if (chainFromAncestor.length > 0 && stackFromDescendant.length > 0) {
783
+ stackFromDescendant.shift(); // Remove duplicate descendant
784
+ }
785
+ return [...chainFromAncestor, ...stackFromDescendant];
786
+ }
787
+ current = effectParent.get(current);
788
+ }
789
+ }
790
+ return false;
791
+ }
792
+ function withEffectStack(snapshot, fn) {
793
+ const previousStack = stack.slice();
794
+ assignStack(snapshot);
795
+ try {
796
+ return fn();
797
+ }
798
+ finally {
799
+ assignStack(previousStack);
800
+ }
801
+ }
802
+ function getActiveEffect() {
803
+ return stack[0];
804
+ }
805
+ /**
806
+ * Executes a function with a specific effect context
807
+ * @param effect - The effect to use as context
808
+ * @param fn - The function to execute
809
+ * @param keepParent - Whether to keep the parent effect context
810
+ * @returns The result of the function
811
+ */
812
+ function withEffect(effect, fn) {
813
+ if (getRoot(effect) === getRoot(getActiveEffect()))
814
+ return fn();
815
+ stack.unshift(effect);
816
+ try {
817
+ return fn();
818
+ }
819
+ finally {
820
+ const recoveredEffect = stack.shift();
821
+ if (recoveredEffect !== effect)
822
+ throw new decorator.ReactiveError('[reactive] Effect stack mismatch');
823
+ }
824
+ }
825
+ function assignStack(values) {
826
+ stack.length = 0;
827
+ stack.push(...values);
828
+ }
829
+
830
+ const objectToProxy = new WeakMap();
831
+ const proxyToObject = new WeakMap();
832
+ function storeProxyRelationship(target, proxy) {
833
+ objectToProxy.set(target, proxy);
834
+ proxyToObject.set(proxy, target);
835
+ }
836
+ function getExistingProxy(target) {
837
+ return objectToProxy.get(target);
838
+ }
839
+ function trackProxyObject(proxy, target) {
840
+ proxyToObject.set(proxy, target);
841
+ }
842
+ function unwrap(obj) {
843
+ let current = obj;
844
+ while (current && typeof current === 'object' && current !== null && proxyToObject.has(current)) {
845
+ current = proxyToObject.get(current);
846
+ }
847
+ return current;
848
+ }
849
+ function isReactive(obj) {
850
+ return proxyToObject.has(obj);
851
+ }
852
+
853
+ function getTrackingDisabled() {
854
+ const active = getActiveEffect();
855
+ if (!active)
856
+ return globalTrackingDisabled;
857
+ return trackingDisabledEffects.has(getRoot(active));
858
+ }
859
+ function setTrackingDisabled(value) {
860
+ const active = getActiveEffect();
861
+ if (!active) {
862
+ setGlobalTrackingDisabled(value);
863
+ return;
864
+ }
865
+ const root = getRoot(active);
866
+ if (value)
867
+ trackingDisabledEffects.add(root);
868
+ else
869
+ trackingDisabledEffects.delete(root);
870
+ }
871
+ /**
872
+ * Marks a property as a dependency of the current effect
873
+ * @param obj - The object containing the property
874
+ * @param prop - The property name (defaults to allProps)
875
+ */
876
+ function dependant(obj, prop = decorator.allProps) {
877
+ obj = unwrap(obj);
878
+ const currentActiveEffect = getActiveEffect();
879
+ // Early return if no active effect, tracking disabled, or invalid prop
880
+ if (!currentActiveEffect ||
881
+ getTrackingDisabled() ||
882
+ (typeof prop === 'symbol' && prop !== decorator.allProps))
883
+ return;
884
+ registerDependency(obj, prop, currentActiveEffect);
885
+ }
886
+ function registerDependency(obj, prop, currentActiveEffect) {
887
+ let objectWatchers = watchers.get(obj);
888
+ if (!objectWatchers) {
889
+ objectWatchers = new Map();
890
+ watchers.set(obj, objectWatchers);
891
+ }
892
+ let deps = objectWatchers.get(prop);
893
+ if (!deps) {
894
+ deps = new Set();
895
+ objectWatchers.set(prop, deps);
896
+ }
897
+ deps.add(currentActiveEffect);
898
+ // Track which reactive objects this effect is watching
899
+ const effectObjects = effectToReactiveObjects.get(currentActiveEffect);
900
+ if (effectObjects) {
901
+ effectObjects.add(obj);
902
+ }
903
+ else {
904
+ effectToReactiveObjects.set(currentActiveEffect, new Set([obj]));
905
+ }
906
+ }
907
+
1093
908
  /**
1094
909
  * Zone-like async context preservation for reactive effects
1095
910
  *
@@ -1130,7 +945,7 @@ let batchFn;
1130
945
  function ensureZoneHooked(batch) {
1131
946
  if (batch)
1132
947
  batchFn = batch;
1133
- if (zoneHooked || !options.asyncMode)
948
+ if (zoneHooked || !decorator.options.asyncMode)
1134
949
  return;
1135
950
  hookZone();
1136
951
  zoneHooked = true;
@@ -1154,7 +969,7 @@ function hookZone() {
1154
969
  };
1155
970
  // Hook setTimeout - preserve original function properties for Node.js compatibility
1156
971
  const wrappedSetTimeout = ((callback, delay, ...args) => {
1157
- const capturedStack = options.zones.setTimeout ? captureEffectStack() : undefined;
972
+ const capturedStack = decorator.options.zones.setTimeout ? captureEffectStack() : undefined;
1158
973
  return originalSetTimeout.apply(globalThis, [
1159
974
  wrapCallback(callback, capturedStack),
1160
975
  delay,
@@ -1165,7 +980,7 @@ function hookZone() {
1165
980
  globalThis.setTimeout = wrappedSetTimeout;
1166
981
  // Hook setInterval - preserve original function properties for Node.js compatibility
1167
982
  const wrappedSetInterval = ((callback, delay, ...args) => {
1168
- const capturedStack = options.zones.setInterval ? captureEffectStack() : undefined;
983
+ const capturedStack = decorator.options.zones.setInterval ? captureEffectStack() : undefined;
1169
984
  return originalSetInterval.apply(globalThis, [
1170
985
  wrapCallback(callback, capturedStack),
1171
986
  delay,
@@ -1177,14 +992,14 @@ function hookZone() {
1177
992
  // Hook requestAnimationFrame if available
1178
993
  if (originalRequestAnimationFrame) {
1179
994
  globalThis.requestAnimationFrame = ((callback) => {
1180
- const capturedStack = options.zones.requestAnimationFrame ? captureEffectStack() : undefined;
995
+ const capturedStack = decorator.options.zones.requestAnimationFrame ? captureEffectStack() : undefined;
1181
996
  return originalRequestAnimationFrame.call(globalThis, wrapCallback(callback, capturedStack));
1182
997
  });
1183
998
  }
1184
999
  // Hook queueMicrotask if available
1185
1000
  if (originalQueueMicrotask) {
1186
1001
  globalThis.queueMicrotask = ((callback) => {
1187
- const capturedStack = options.zones.queueMicrotask ? captureEffectStack() : undefined;
1002
+ const capturedStack = decorator.options.zones.queueMicrotask ? captureEffectStack() : undefined;
1188
1003
  originalQueueMicrotask.call(globalThis, wrapCallback(callback, capturedStack));
1189
1004
  });
1190
1005
  }
@@ -1301,17 +1116,17 @@ function recordActivation(effect, obj, evolution, prop) {
1301
1116
  prop,
1302
1117
  });
1303
1118
  activationLog.pop();
1304
- if (count >= options.maxTriggerPerBatch) {
1119
+ if (count >= decorator.options.maxTriggerPerBatch) {
1305
1120
  const effectName = root?.name || 'anonymous';
1306
1121
  const message = `Aggressive trigger detected: effect "${effectName}" triggered ${count} times in the batch by the same cause.`;
1307
- if (options.maxEffectReaction === 'throw') {
1308
- throw new ReactiveError(message, {
1309
- code: ReactiveErrorCode.MaxReactionExceeded,
1122
+ if (decorator.options.maxEffectReaction === 'throw') {
1123
+ throw new decorator.ReactiveError(message, {
1124
+ code: decorator.ReactiveErrorCode.MaxReactionExceeded,
1310
1125
  count,
1311
1126
  effect: effectName,
1312
1127
  });
1313
1128
  }
1314
- options.warn(`[reactive] ${message}`);
1129
+ decorator.options.warn(`[reactive] ${message}`);
1315
1130
  }
1316
1131
  }
1317
1132
  /**
@@ -1475,6 +1290,7 @@ function addGraphEdge(callerRoot, targetRoot) {
1475
1290
  * @param end - Target node
1476
1291
  * @param exclude - Node to exclude from the path
1477
1292
  * @returns true if a path exists without going through the excluded node
1293
+ * @todo Can be REALLY costly - optimise or make optional or ...
1478
1294
  */
1479
1295
  function hasPathExcluding(start, end, exclude) {
1480
1296
  if (start === end)
@@ -1762,6 +1578,12 @@ function wouldCreateCycle(callerRoot, targetRoot) {
1762
1578
  * @param immediate - If true, don't create edges in the dependency graph
1763
1579
  */
1764
1580
  function addToBatch(effect, caller, immediate) {
1581
+ const cleanupFn = effect[decorator.cleanup];
1582
+ if (cleanupFn)
1583
+ cleanupFn();
1584
+ // If the effect was stopped during cleanup (e.g. lazy memoization), don't add it to the batch
1585
+ if (effect[decorator.stopped])
1586
+ return;
1765
1587
  if (!batchQueue)
1766
1588
  return;
1767
1589
  const root = getRoot(effect);
@@ -1780,14 +1602,14 @@ function addToBatch(effect, caller, immediate) {
1780
1602
  const cycleMessage = cyclePath.length > 0
1781
1603
  ? `Cycle detected: ${cyclePath.map((r) => r.name || r.toString()).join(' → ')}`
1782
1604
  : `Cycle detected: ${callerRoot.name || callerRoot.toString()} → ${root.name || root.toString()} (and back)`;
1783
- const cycleHandling = options.cycleHandling;
1605
+ const cycleHandling = decorator.options.cycleHandling;
1784
1606
  // In strict mode, we throw immediately on detection
1785
1607
  if (cycleHandling === 'strict') {
1786
1608
  batchQueue.all.delete(root);
1787
1609
  const causalChain = getTriggerChain(effect);
1788
1610
  const creationStack = effectCreationStacks.get(root);
1789
- throw new ReactiveError(`[reactive] Strict Cycle Prevention: ${cycleMessage}`, {
1790
- code: ReactiveErrorCode.CycleDetected,
1611
+ throw new decorator.ReactiveError(`[reactive] Strict Cycle Prevention: ${cycleMessage}`, {
1612
+ code: decorator.ReactiveErrorCode.CycleDetected,
1791
1613
  cycle: cyclePath.map((r) => r.name || r.toString()),
1792
1614
  details: cycleMessage,
1793
1615
  causalChain,
@@ -1800,8 +1622,8 @@ function addToBatch(effect, caller, immediate) {
1800
1622
  batchQueue.all.delete(root);
1801
1623
  const causalChain = getTriggerChain(effect);
1802
1624
  const creationStack = effectCreationStacks.get(root);
1803
- throw new ReactiveError(`[reactive] ${cycleMessage}`, {
1804
- code: ReactiveErrorCode.CycleDetected,
1625
+ throw new decorator.ReactiveError(`[reactive] ${cycleMessage}`, {
1626
+ code: decorator.ReactiveErrorCode.CycleDetected,
1805
1627
  cycle: cyclePath.map((r) => r.name || r.toString()),
1806
1628
  details: cycleMessage,
1807
1629
  causalChain,
@@ -1809,7 +1631,7 @@ function addToBatch(effect, caller, immediate) {
1809
1631
  });
1810
1632
  }
1811
1633
  case 'warn':
1812
- options.warn(`[reactive] ${cycleMessage}`);
1634
+ decorator.options.warn(`[reactive] ${cycleMessage}`);
1813
1635
  // Don't add the edge, break the cycle
1814
1636
  batchQueue.all.delete(root);
1815
1637
  return;
@@ -1958,12 +1780,12 @@ function executeNext(effectuatedRoots) {
1958
1780
  const cycleMessage = cycle.length > 0
1959
1781
  ? `Cycle detected: ${cycle.map((r) => r.name || '<anonymous>').join(' → ')}`
1960
1782
  : 'Cycle detected in effect batch - all effects have dependencies that prevent execution';
1961
- const cycleHandling = options.cycleHandling;
1783
+ const cycleHandling = decorator.options.cycleHandling;
1962
1784
  switch (cycleHandling) {
1963
1785
  case 'throw':
1964
- throw new ReactiveError(`[reactive] ${cycleMessage}`);
1786
+ throw new decorator.ReactiveError(`[reactive] ${cycleMessage}`);
1965
1787
  case 'warn': {
1966
- options.warn(`[reactive] ${cycleMessage}`);
1788
+ decorator.options.warn(`[reactive] ${cycleMessage}`);
1967
1789
  // Break the cycle by executing one effect anyway
1968
1790
  const firstEffect = batchQueue.all.values().next().value;
1969
1791
  if (firstEffect) {
@@ -2006,7 +1828,7 @@ function batch(effect, immediate) {
2006
1828
  const roots = effect.map(getRoot);
2007
1829
  if (batchQueue) {
2008
1830
  // Nested batch - add to existing
2009
- options?.chain(roots, getRoot(getActiveEffect()));
1831
+ decorator.options?.chain(roots, getRoot(getActiveEffect()));
2010
1832
  const caller = getActiveEffect();
2011
1833
  for (let i = 0; i < effect.length; i++) {
2012
1834
  addToBatch(effect[i], caller, immediate === 'immediate');
@@ -2035,7 +1857,7 @@ function batch(effect, immediate) {
2035
1857
  activationRegistry = new Map();
2036
1858
  else
2037
1859
  throw new Error('Batch already in progress');
2038
- options.beginChain(roots);
1860
+ decorator.options.beginChain(roots);
2039
1861
  batchQueue = {
2040
1862
  all: new Map(),
2041
1863
  inDegrees: new Map(),
@@ -2065,7 +1887,7 @@ function batch(effect, immediate) {
2065
1887
  // After immediate execution, execute any effects that were triggered during execution
2066
1888
  // This is important for @atomic decorator - effects triggered inside should still run
2067
1889
  while (batchQueue.all.size > 0) {
2068
- if (effectuatedRoots.length > options.maxEffectChain) {
1890
+ if (effectuatedRoots.length > decorator.options.maxEffectChain) {
2069
1891
  const cycle = findCycleInChain(effectuatedRoots);
2070
1892
  const trace = formatRoots(effectuatedRoots);
2071
1893
  const message = cycle
@@ -2074,11 +1896,11 @@ function batch(effect, immediate) {
2074
1896
  const queuedRoots = batchQueue ? Array.from(batchQueue.all.keys()) : [];
2075
1897
  const queued = queuedRoots.map((r) => r.name || '<anonymous>');
2076
1898
  const debugInfo = {
2077
- code: ReactiveErrorCode.MaxDepthExceeded,
1899
+ code: decorator.ReactiveErrorCode.MaxDepthExceeded,
2078
1900
  effectuatedRoots,
2079
1901
  cycle,
2080
1902
  trace,
2081
- maxEffectChain: options.maxEffectChain,
1903
+ maxEffectChain: decorator.options.maxEffectChain,
2082
1904
  queued: queued.slice(0, 50),
2083
1905
  queuedCount: queued.length,
2084
1906
  // Try to get causation for the last effect
@@ -2086,15 +1908,15 @@ function batch(effect, immediate) {
2086
1908
  ? getTriggerChain(batchQueue.all.get(effectuatedRoots[effectuatedRoots.length - 1]))
2087
1909
  : [],
2088
1910
  };
2089
- switch (options.maxEffectReaction) {
1911
+ switch (decorator.options.maxEffectReaction) {
2090
1912
  case 'throw':
2091
- throw new ReactiveError(`[reactive] ${message}`, debugInfo);
1913
+ throw new decorator.ReactiveError(`[reactive] ${message}`, debugInfo);
2092
1914
  case 'debug':
2093
1915
  // biome-ignore lint/suspicious/noDebugger: This is the whole point here
2094
1916
  debugger;
2095
- throw new ReactiveError(`[reactive] ${message}`, debugInfo);
1917
+ throw new decorator.ReactiveError(`[reactive] ${message}`, debugInfo);
2096
1918
  case 'warn':
2097
- options.warn(`[reactive] ${message} (queued: ${queued.slice(0, 10).join(', ')}${queued.length > 10 ? ', …' : ''})`);
1919
+ decorator.options.warn(`[reactive] ${message} (queued: ${queued.slice(0, 10).join(', ')}${queued.length > 10 ? ', …' : ''})`);
2098
1920
  break;
2099
1921
  }
2100
1922
  }
@@ -2115,7 +1937,7 @@ function batch(effect, immediate) {
2115
1937
  finally {
2116
1938
  activationRegistry = undefined;
2117
1939
  batchQueue = undefined;
2118
- options.endChain();
1940
+ decorator.options.endChain();
2119
1941
  }
2120
1942
  }
2121
1943
  else {
@@ -2127,7 +1949,7 @@ function batch(effect, immediate) {
2127
1949
  while (batchQueue.all.size > 0 || batchCleanups.size > 0) {
2128
1950
  // Inner loop: execute all pending effects
2129
1951
  while (batchQueue.all.size > 0) {
2130
- if (effectuatedRoots.length > options.maxEffectChain) {
1952
+ if (effectuatedRoots.length > decorator.options.maxEffectChain) {
2131
1953
  const cycle = findCycleInChain(effectuatedRoots);
2132
1954
  const trace = formatRoots(effectuatedRoots);
2133
1955
  const message = cycle
@@ -2136,11 +1958,11 @@ function batch(effect, immediate) {
2136
1958
  const queuedRoots = batchQueue ? Array.from(batchQueue.all.keys()) : [];
2137
1959
  const queued = queuedRoots.map((r) => r.name || '<anonymous>');
2138
1960
  const debugInfo = {
2139
- code: ReactiveErrorCode.MaxDepthExceeded,
1961
+ code: decorator.ReactiveErrorCode.MaxDepthExceeded,
2140
1962
  effectuatedRoots,
2141
1963
  cycle,
2142
1964
  trace,
2143
- maxEffectChain: options.maxEffectChain,
1965
+ maxEffectChain: decorator.options.maxEffectChain,
2144
1966
  queued: queued.slice(0, 50),
2145
1967
  queuedCount: queued.length,
2146
1968
  // Try to get causation for the last effect
@@ -2148,15 +1970,15 @@ function batch(effect, immediate) {
2148
1970
  ? getTriggerChain(batchQueue.all.get(effectuatedRoots[effectuatedRoots.length - 1]))
2149
1971
  : [],
2150
1972
  };
2151
- switch (options.maxEffectReaction) {
1973
+ switch (decorator.options.maxEffectReaction) {
2152
1974
  case 'throw':
2153
- throw new ReactiveError(`[reactive] ${message}`, debugInfo);
1975
+ throw new decorator.ReactiveError(`[reactive] ${message}`, debugInfo);
2154
1976
  case 'debug':
2155
1977
  // biome-ignore lint/suspicious/noDebugger: This is the whole point here
2156
1978
  debugger;
2157
- throw new ReactiveError(`[reactive] ${message}`, debugInfo);
1979
+ throw new decorator.ReactiveError(`[reactive] ${message}`, debugInfo);
2158
1980
  case 'warn':
2159
- options.warn(`[reactive] ${message} (queued: ${queued.slice(0, 10).join(', ')}${queued.length > 10 ? ', …' : ''})`);
1981
+ decorator.options.warn(`[reactive] ${message} (queued: ${queued.slice(0, 10).join(', ')}${queued.length > 10 ? ', …' : ''})`);
2160
1982
  break;
2161
1983
  }
2162
1984
  }
@@ -2186,7 +2008,7 @@ function batch(effect, immediate) {
2186
2008
  finally {
2187
2009
  activationRegistry = undefined;
2188
2010
  batchQueue = undefined;
2189
- options.endChain();
2011
+ decorator.options.endChain();
2190
2012
  }
2191
2013
  }
2192
2014
  }
@@ -2197,12 +2019,18 @@ function batch(effect, immediate) {
2197
2019
  const atomic = decorator.decorator({
2198
2020
  method(original) {
2199
2021
  return function (...args) {
2200
- return batch(markWithRoot(() => original.apply(this, args), original), 'immediate');
2022
+ const atomicEffect = () => original.apply(this, args);
2023
+ // Debug: helpful to have a name
2024
+ Object.defineProperty(atomicEffect, 'name', { value: `atomic(${original.name})` });
2025
+ return batch(atomicEffect, 'immediate');
2201
2026
  };
2202
2027
  },
2203
2028
  default(original) {
2204
2029
  return function (...args) {
2205
- return batch(markWithRoot(() => original.apply(this, args), original), 'immediate');
2030
+ const atomicEffect = () => original.apply(this, args);
2031
+ // Debug: helpful to have a name
2032
+ Object.defineProperty(atomicEffect, 'name', { value: `atomic(${original.name})` });
2033
+ return batch(atomicEffect, 'immediate');
2206
2034
  };
2207
2035
  },
2208
2036
  });
@@ -2224,8 +2052,8 @@ fn, effectOptions) {
2224
2052
  // Inject batch function to allow atomic game loops in requestAnimationFrame
2225
2053
  ensureZoneHooked(batch);
2226
2054
  // Use per-effect asyncMode or fall back to global option
2227
- const asyncMode = effectOptions?.asyncMode ?? options.asyncMode ?? 'cancel';
2228
- if (options.introspection.enableHistory) {
2055
+ const asyncMode = effectOptions?.asyncMode ?? decorator.options.asyncMode ?? 'cancel';
2056
+ if (decorator.options.introspection.enableHistory) {
2229
2057
  const stack = new Error().stack;
2230
2058
  if (stack) {
2231
2059
  // Clean up the stack trace to remove internal frames
@@ -2236,7 +2064,7 @@ fn, effectOptions) {
2236
2064
  let cleanup = null;
2237
2065
  // capture the parent effect at creation time for ascend
2238
2066
  const parentsForAscend = captureEffectStack();
2239
- const tracked = markWithRoot((cb) => withEffect(runEffect, cb), fn);
2067
+ const tracked = (cb) => withEffect(runEffect, cb);
2240
2068
  const ascend = (cb) => withEffectStack(parentsForAscend, cb);
2241
2069
  let effectStopped = false;
2242
2070
  let hasReacted = false;
@@ -2266,7 +2094,7 @@ fn, effectOptions) {
2266
2094
  // The effect has been stopped after having been planned
2267
2095
  if (effectStopped)
2268
2096
  return;
2269
- options.enter(getRoot(fn));
2097
+ decorator.options.enter(getRoot(fn));
2270
2098
  let reactionCleanup;
2271
2099
  let result;
2272
2100
  try {
@@ -2274,7 +2102,7 @@ fn, effectOptions) {
2274
2102
  if (result &&
2275
2103
  typeof result !== 'function' &&
2276
2104
  (typeof result !== 'object' || !('then' in result)))
2277
- throw new ReactiveError(`[reactive] Effect returned a non-function value: ${result}`);
2105
+ throw new decorator.ReactiveError(`[reactive] Effect returned a non-function value: ${result}`);
2278
2106
  // Check if result is a Promise (async effect)
2279
2107
  if (result && typeof result === 'object' && typeof result.then === 'function') {
2280
2108
  const originalPromise = result;
@@ -2283,7 +2111,7 @@ fn, effectOptions) {
2283
2111
  const cancelPromise = new Promise((_, reject) => {
2284
2112
  cancelReject = reject;
2285
2113
  });
2286
- const cancelError = new ReactiveError('[reactive] Effect canceled due to dependency change');
2114
+ const cancelError = new decorator.ReactiveError('[reactive] Effect canceled due to dependency change');
2287
2115
  // Race between the actual promise and cancellation
2288
2116
  // If canceled, the race rejects, which will propagate through any promise chain
2289
2117
  runningPromise = Promise.race([originalPromise, cancelPromise]);
@@ -2305,7 +2133,7 @@ fn, effectOptions) {
2305
2133
  }
2306
2134
  finally {
2307
2135
  hasReacted = true;
2308
- options.leave(fn);
2136
+ decorator.options.leave(fn);
2309
2137
  }
2310
2138
  // Create cleanup function for next run
2311
2139
  cleanup = () => {
@@ -2369,11 +2197,28 @@ fn, effectOptions) {
2369
2197
  cleanupEffectFromGraph(runEffect);
2370
2198
  fr.unregister(stopEffect);
2371
2199
  };
2200
+ function augmentedRv(rv) {
2201
+ Object.defineProperty(rv, decorator.stopped, {
2202
+ get() {
2203
+ return effectStopped;
2204
+ },
2205
+ });
2206
+ Object.defineProperty(rv, decorator.cleanup, {
2207
+ value: () => {
2208
+ if (cleanup) {
2209
+ const prevCleanup = cleanup;
2210
+ cleanup = null;
2211
+ withEffect(undefined, () => prevCleanup());
2212
+ }
2213
+ },
2214
+ });
2215
+ return rv;
2216
+ }
2372
2217
  if (isRootEffect) {
2373
- const callIfCollected = () => stopEffect();
2218
+ const callIfCollected = augmentedRv(() => stopEffect());
2374
2219
  fr.register(callIfCollected, () => {
2375
2220
  stopEffect();
2376
- options.garbageCollected(fn);
2221
+ decorator.options.garbageCollected(fn);
2377
2222
  }, stopEffect);
2378
2223
  return callIfCollected;
2379
2224
  }
@@ -2383,14 +2228,14 @@ fn, effectOptions) {
2383
2228
  children = new Set();
2384
2229
  effectChildren.set(parent, children);
2385
2230
  }
2386
- const subEffectCleanup = () => {
2231
+ const subEffectCleanup = augmentedRv(() => {
2387
2232
  children.delete(subEffectCleanup);
2388
2233
  if (children.size === 0) {
2389
2234
  effectChildren.delete(parent);
2390
2235
  }
2391
2236
  // Execute this child effect cleanup (which triggers its own mainCleanup)
2392
2237
  stopEffect();
2393
- };
2238
+ });
2394
2239
  children.add(subEffectCleanup);
2395
2240
  return subEffectCleanup;
2396
2241
  }
@@ -2551,7 +2396,7 @@ function collectEffects(obj, evolution, effects, objectWatchers, ...keyChains) {
2551
2396
  for (const effect of deps) {
2552
2397
  const runningChain = isRunning(effect);
2553
2398
  if (runningChain) {
2554
- options.skipRunningEffect(effect, runningChain);
2399
+ decorator.options.skipRunningEffect(effect, runningChain);
2555
2400
  continue;
2556
2401
  }
2557
2402
  if (!effects.has(effect)) {
@@ -2592,10 +2437,10 @@ function touched(obj, evolution, props) {
2592
2437
  // Note: we have to collect effects to remove duplicates in the specific case when no batch is running
2593
2438
  const effects = new Set();
2594
2439
  if (props)
2595
- collectEffects(obj, evolution, effects, objectWatchers, [allProps], props);
2440
+ collectEffects(obj, evolution, effects, objectWatchers, [decorator.allProps], props);
2596
2441
  else
2597
2442
  collectEffects(obj, evolution, effects, objectWatchers, objectWatchers.keys());
2598
- options.touched(obj, evolution, props, effects);
2443
+ decorator.options.touched(obj, evolution, props, effects);
2599
2444
  batch(Array.from(effects));
2600
2445
  }
2601
2446
  // Bubble up changes if this object has deep watchers
@@ -2622,7 +2467,7 @@ function touchedOpaque(obj, evolution, prop) {
2622
2467
  continue;
2623
2468
  const runningChain = isRunning(effect);
2624
2469
  if (runningChain) {
2625
- options.skipRunningEffect(effect, runningChain);
2470
+ decorator.options.skipRunningEffect(effect, runningChain);
2626
2471
  continue;
2627
2472
  }
2628
2473
  effects.add(effect);
@@ -2636,7 +2481,7 @@ function touchedOpaque(obj, evolution, prop) {
2636
2481
  }
2637
2482
  }
2638
2483
  if (effects.size > 0) {
2639
- options.touched(obj, evolution, [prop], effects);
2484
+ decorator.options.touched(obj, evolution, [prop], effects);
2640
2485
  batch(Array.from(effects));
2641
2486
  }
2642
2487
  }
@@ -2647,7 +2492,7 @@ const absent = Symbol('absent');
2647
2492
  function markNonReactive(...obj) {
2648
2493
  for (const o of obj) {
2649
2494
  try {
2650
- Object.defineProperty(o, nonReactiveMark, {
2495
+ Object.defineProperty(o, decorator.nonReactiveMark, {
2651
2496
  value: true,
2652
2497
  writable: false,
2653
2498
  enumerable: false,
@@ -2655,7 +2500,7 @@ function markNonReactive(...obj) {
2655
2500
  });
2656
2501
  }
2657
2502
  catch { }
2658
- if (!(nonReactiveMark in o))
2503
+ if (!(decorator.nonReactiveMark in o))
2659
2504
  nonReactiveObjects.add(o);
2660
2505
  }
2661
2506
  return obj[0];
@@ -2663,7 +2508,7 @@ function markNonReactive(...obj) {
2663
2508
  function nonReactiveClass(...cls) {
2664
2509
  for (const c of cls)
2665
2510
  if (c)
2666
- c.prototype[nonReactiveMark] = true;
2511
+ c.prototype[decorator.nonReactiveMark] = true;
2667
2512
  return cls[0];
2668
2513
  }
2669
2514
  function isNonReactive(obj) {
@@ -2671,7 +2516,7 @@ function isNonReactive(obj) {
2671
2516
  return true;
2672
2517
  if (nonReactiveObjects.has(obj))
2673
2518
  return true;
2674
- if (obj[nonReactiveMark])
2519
+ if (obj[decorator.nonReactiveMark])
2675
2520
  return true;
2676
2521
  for (const fn of immutables)
2677
2522
  if (fn(obj))
@@ -2679,7 +2524,7 @@ function isNonReactive(obj) {
2679
2524
  return false;
2680
2525
  }
2681
2526
  function registerNativeReactivity(originalClass, reactiveClass) {
2682
- originalClass.prototype[nativeReactive] = reactiveClass;
2527
+ originalClass.prototype[decorator.nativeReactive] = reactiveClass;
2683
2528
  nonReactiveClass(reactiveClass);
2684
2529
  }
2685
2530
  nonReactiveClass(Date, RegExp, Error, Promise, Function);
@@ -2725,7 +2570,7 @@ function shouldRecurseTouch(oldValue, newValue) {
2725
2570
  */
2726
2571
  function notifyPropertyChange(targetObj, prop, oldValue, newValue, hadProperty) {
2727
2572
  const evolution = { type: hadProperty ? 'set' : 'add', prop };
2728
- if (options.recursiveTouching &&
2573
+ if (decorator.options.recursiveTouching &&
2729
2574
  oldValue !== undefined &&
2730
2575
  shouldRecurseTouch(oldValue, newValue)) {
2731
2576
  const unwrappedObj = unwrap(targetObj);
@@ -2862,7 +2707,7 @@ function dispatchNotifications(notifications) {
2862
2707
  const originWatchers = watchers.get(origin.obj);
2863
2708
  if (originWatchers) {
2864
2709
  const originEffects = new Set();
2865
- collectEffects(origin.obj, { type: 'set', prop: origin.prop }, originEffects, originWatchers, [allProps], [origin.prop]);
2710
+ collectEffects(origin.obj, { type: 'set', prop: origin.prop }, originEffects, originWatchers, [decorator.allProps], [origin.prop]);
2866
2711
  for (const effect of originEffects)
2867
2712
  allowedEffects.add(effect);
2868
2713
  }
@@ -2880,7 +2725,7 @@ function dispatchNotifications(notifications) {
2880
2725
  const propsArray = [prop];
2881
2726
  if (objectWatchers) {
2882
2727
  currentEffects = new Set();
2883
- collectEffects(obj, evolution, currentEffects, objectWatchers, [allProps], propsArray);
2728
+ collectEffects(obj, evolution, currentEffects, objectWatchers, [decorator.allProps], propsArray);
2884
2729
  // Filter effects by ancestor chain if origin exists
2885
2730
  // Include effects that either directly depend on origin or have an ancestor that does
2886
2731
  if (origin && allowedEffects) {
@@ -2896,7 +2741,7 @@ function dispatchNotifications(notifications) {
2896
2741
  for (const effect of currentEffects)
2897
2742
  combinedEffects.add(effect);
2898
2743
  }
2899
- options.touched(obj, evolution, propsArray, currentEffects);
2744
+ decorator.options.touched(obj, evolution, propsArray, currentEffects);
2900
2745
  if (objectsWithDeepWatchers.has(obj))
2901
2746
  bubbleUpChange(obj);
2902
2747
  }
@@ -2908,18 +2753,18 @@ const hasReentry = [];
2908
2753
  const reactiveHandlers = {
2909
2754
  [Symbol.toStringTag]: 'MutTs Reactive',
2910
2755
  get(obj, prop, receiver) {
2911
- if (prop === nonReactiveMark)
2756
+ if (prop === decorator.nonReactiveMark)
2912
2757
  return false;
2913
2758
  const unwrappedObj = unwrap(obj);
2914
2759
  // Check if this property is marked as unreactive
2915
- if (unwrappedObj[unreactiveProperties]?.has(prop) || typeof prop === 'symbol')
2760
+ if (unwrappedObj[decorator.unreactiveProperties]?.has(prop) || typeof prop === 'symbol')
2916
2761
  return decorator.ReflectGet(obj, prop, receiver);
2917
2762
  // Special-case: array wrappers use prototype forwarding + numeric accessors.
2918
2763
  // With options.instanceMembers=true, inherited reads are normally not tracked, which breaks
2919
2764
  // reactivity for array indices/length (they appear inherited on the proxy).
2920
- const isArrayCase = prototypeForwarding in obj &&
2765
+ const isArrayCase = decorator.prototypeForwarding in obj &&
2921
2766
  // biome-ignore lint/suspicious/useIsArray: This is the whole point here
2922
- obj[prototypeForwarding] instanceof Array &&
2767
+ obj[decorator.prototypeForwarding] instanceof Array &&
2923
2768
  typeof prop === 'string' &&
2924
2769
  (prop === 'length' || !Number.isNaN(Number(prop)));
2925
2770
  if (isArrayCase) {
@@ -2931,16 +2776,16 @@ const reactiveHandlers = {
2931
2776
  const isInheritedAccess = hasProp && !isOwnProp;
2932
2777
  // For accessor properties, check the unwrapped object to see if it's an accessor
2933
2778
  // This ensures ignoreAccessors works correctly even after operations like Object.setPrototypeOf
2934
- const shouldIgnoreAccessor = options.ignoreAccessors &&
2779
+ const shouldIgnoreAccessor = decorator.options.ignoreAccessors &&
2935
2780
  isOwnProp &&
2936
2781
  (decorator.isOwnAccessor(receiver, prop) || decorator.isOwnAccessor(unwrappedObj, prop));
2937
2782
  // Depend if...
2938
2783
  if (!hasProp ||
2939
- (!(options.instanceMembers && isInheritedAccess && obj instanceof Object) &&
2784
+ (!(decorator.options.instanceMembers && isInheritedAccess && obj instanceof Object) &&
2940
2785
  !shouldIgnoreAccessor))
2941
2786
  dependant(obj, prop);
2942
2787
  // Watch the whole prototype chain when requested or for null-proto objects
2943
- if (isInheritedAccess && (!options.instanceMembers || !(obj instanceof Object))) {
2788
+ if (isInheritedAccess && (!decorator.options.instanceMembers || !(obj instanceof Object))) {
2944
2789
  let current = reactiveObject(Object.getPrototypeOf(obj));
2945
2790
  while (current && current !== Object.prototype) {
2946
2791
  dependant(current, prop);
@@ -2969,12 +2814,12 @@ const reactiveHandlers = {
2969
2814
  const unwrappedObj = unwrap(obj);
2970
2815
  const unwrappedReceiver = unwrap(receiver);
2971
2816
  // Check if this property is marked as unreactive
2972
- if (unwrappedObj[unreactiveProperties]?.has(prop) || unwrappedObj !== unwrappedReceiver)
2817
+ if (unwrappedObj[decorator.unreactiveProperties]?.has(prop) || unwrappedObj !== unwrappedReceiver)
2973
2818
  return decorator.ReflectSet(obj, prop, value, receiver);
2974
2819
  // Really specific case for when Array is forwarder, in order to let it manage the reactivity
2975
- const isArrayCase = prototypeForwarding in obj &&
2820
+ const isArrayCase = decorator.prototypeForwarding in obj &&
2976
2821
  // biome-ignore lint/suspicious/useIsArray: This is the whole point here
2977
- obj[prototypeForwarding] instanceof Array &&
2822
+ obj[decorator.prototypeForwarding] instanceof Array &&
2978
2823
  (!Number.isNaN(Number(prop)) || prop === 'length');
2979
2824
  const newValue = unwrap(value);
2980
2825
  if (isArrayCase) {
@@ -2989,13 +2834,13 @@ const reactiveHandlers = {
2989
2834
  const receiverDesc = Object.getOwnPropertyDescriptor(unwrappedReceiver, prop);
2990
2835
  const targetDesc = Object.getOwnPropertyDescriptor(unwrappedObj, prop);
2991
2836
  const desc = receiverDesc || targetDesc;
2992
- // If it's a getter-only accessor (has getter but no setter), read without tracking
2993
- // to avoid breaking memoization invalidation when the getter calls memoized functions
2837
+ // We *need* to use `receiver` and not `unwrappedObj` here, otherwise we break
2838
+ // the dependency tracking for memoized getters
2994
2839
  if (desc?.get && !desc?.set) {
2995
- oldVal = withEffect(undefined, () => Reflect.get(unwrappedObj, prop, unwrappedReceiver));
2840
+ oldVal = withEffect(undefined, () => Reflect.get(unwrappedObj, prop, receiver));
2996
2841
  }
2997
2842
  else {
2998
- oldVal = Reflect.get(unwrappedObj, prop, unwrappedReceiver);
2843
+ oldVal = withEffect(undefined, () => Reflect.get(unwrappedObj, prop, receiver));
2999
2844
  }
3000
2845
  }
3001
2846
  if (objectsWithDeepWatchers.has(obj)) {
@@ -3018,8 +2863,8 @@ const reactiveHandlers = {
3018
2863
  },
3019
2864
  has(obj, prop) {
3020
2865
  if (hasReentry.includes(obj))
3021
- throw new ReactiveError(`[reactive] Circular dependency detected in 'has' check for property '${String(prop)}'`, {
3022
- code: ReactiveErrorCode.CycleDetected,
2866
+ throw new decorator.ReactiveError(`[reactive] Circular dependency detected in 'has' check for property '${String(prop)}'`, {
2867
+ code: decorator.ReactiveErrorCode.CycleDetected,
3023
2868
  cycle: [], // We don't have the full cycle here, but we know it involves obj
3024
2869
  });
3025
2870
  hasReentry.push(obj);
@@ -3045,18 +2890,18 @@ const reactiveHandlers = {
3045
2890
  return true;
3046
2891
  },
3047
2892
  getPrototypeOf(obj) {
3048
- if (prototypeForwarding in obj)
3049
- return obj[prototypeForwarding];
2893
+ if (decorator.prototypeForwarding in obj)
2894
+ return obj[decorator.prototypeForwarding];
3050
2895
  return Object.getPrototypeOf(obj);
3051
2896
  },
3052
2897
  setPrototypeOf(obj, proto) {
3053
- if (prototypeForwarding in obj)
2898
+ if (decorator.prototypeForwarding in obj)
3054
2899
  return false;
3055
2900
  Object.setPrototypeOf(obj, proto);
3056
2901
  return true;
3057
2902
  },
3058
2903
  ownKeys(obj) {
3059
- dependant(obj, allProps);
2904
+ dependant(obj, decorator.allProps);
3060
2905
  return Reflect.ownKeys(obj);
3061
2906
  },
3062
2907
  };
@@ -3092,8 +2937,8 @@ function reactiveObject(anyTarget) {
3092
2937
  const existing = getExistingProxy(target);
3093
2938
  if (existing !== undefined)
3094
2939
  return existing;
3095
- const proxied = nativeReactive in target && !(target instanceof target[nativeReactive])
3096
- ? new target[nativeReactive](target)
2940
+ const proxied = decorator.nativeReactive in target && !(target instanceof target[decorator.nativeReactive])
2941
+ ? new target[decorator.nativeReactive](target)
3097
2942
  : target;
3098
2943
  if (proxied !== target)
3099
2944
  trackProxyObject(proxied, target);
@@ -3116,7 +2961,7 @@ const reactive = decorator.decorator({
3116
2961
  constructor(...args) {
3117
2962
  super(...args);
3118
2963
  if (new.target !== Reactive && !reactiveClasses.has(new.target))
3119
- options.warn(`${original.name} has been inherited by ${this.constructor.name} that is not reactive.
2964
+ decorator.options.warn(`${original.name} has been inherited by ${this.constructor.name} that is not reactive.
3120
2965
  @reactive decorator must be applied to the leaf class OR classes have to extend ReactiveBase.`);
3121
2966
  // biome-ignore lint/correctness/noConstructorReturn: This is the whole point here
3122
2967
  return reactive(this);
@@ -3173,7 +3018,7 @@ function deepWatch(target, callback, { immediate = false } = {}) {
3173
3018
  const visited = new WeakSet();
3174
3019
  function traverseAndTrack(obj, depth = 0) {
3175
3020
  // Prevent infinite recursion and excessive depth
3176
- if (!obj || visited.has(obj) || !isObject(obj) || depth > options.maxDeepWatchDepth)
3021
+ if (!obj || visited.has(obj) || !isObject(obj) || depth > decorator.options.maxDeepWatchDepth)
3177
3022
  return;
3178
3023
  // Do not traverse into unreactive objects
3179
3024
  if (isNonReactive(obj))
@@ -3285,12 +3130,12 @@ function watchObject(value, changed, { immediate = false, deep = false } = {}) {
3285
3130
  const myParentEffect = getActiveEffect();
3286
3131
  if (deep)
3287
3132
  return deepWatch(value, changed, { immediate });
3288
- return effect(markWithRoot(function watchObjectEffect() {
3133
+ return effect(function watchObjectEffect() {
3289
3134
  dependant(value);
3290
3135
  if (immediate)
3291
3136
  withEffect(myParentEffect, () => changed(value));
3292
3137
  immediate = true;
3293
- }, changed));
3138
+ });
3294
3139
  }
3295
3140
  function watchCallBack(value, changed, { immediate = false, deep = false } = {}) {
3296
3141
  const myParentEffect = getActiveEffect();
@@ -3299,7 +3144,7 @@ function watchCallBack(value, changed, { immediate = false, deep = false } = {})
3299
3144
  const cbCleanup = effect(markWithRoot(function watchCallBackEffect(access) {
3300
3145
  const newValue = value(access);
3301
3146
  if (oldValue !== newValue)
3302
- withEffect(myParentEffect, markWithRoot(() => {
3147
+ withEffect(myParentEffect, () => {
3303
3148
  if (oldValue === unsetYet) {
3304
3149
  if (immediate)
3305
3150
  changed(newValue);
@@ -3310,9 +3155,9 @@ function watchCallBack(value, changed, { immediate = false, deep = false } = {})
3310
3155
  if (deep) {
3311
3156
  if (deepCleanup)
3312
3157
  deepCleanup();
3313
- deepCleanup = deepWatch(newValue, markWithRoot((value) => changed(value, value), changed));
3158
+ deepCleanup = deepWatch(newValue, (value) => changed(value, value));
3314
3159
  }
3315
- }, changed));
3160
+ });
3316
3161
  }, value));
3317
3162
  return () => {
3318
3163
  cbCleanup();
@@ -3331,7 +3176,7 @@ function deepNonReactive(obj) {
3331
3176
  if (isNonReactive(obj))
3332
3177
  return obj;
3333
3178
  try {
3334
- Object.defineProperty(obj, nonReactiveMark, {
3179
+ Object.defineProperty(obj, decorator.nonReactiveMark, {
3335
3180
  value: true,
3336
3181
  writable: false,
3337
3182
  enumerable: false,
@@ -3339,7 +3184,7 @@ function deepNonReactive(obj) {
3339
3184
  });
3340
3185
  }
3341
3186
  catch { }
3342
- if (!(nonReactiveMark in obj))
3187
+ if (!(decorator.nonReactiveMark in obj))
3343
3188
  nonReactiveObjects.add(obj);
3344
3189
  //for (const key in obj) deepNonReactive(obj[key])
3345
3190
  return obj;
@@ -3349,11 +3194,11 @@ function unreactiveApplication(arg1, ...args) {
3349
3194
  ? deepNonReactive(arg1)
3350
3195
  : ((original) => {
3351
3196
  // Copy the parent's unreactive properties if they exist
3352
- original.prototype[unreactiveProperties] = new Set(original.prototype[unreactiveProperties] || []);
3197
+ original.prototype[decorator.unreactiveProperties] = new Set(original.prototype[decorator.unreactiveProperties] || []);
3353
3198
  // Add all arguments (including the first one)
3354
- original.prototype[unreactiveProperties].add(arg1);
3199
+ original.prototype[decorator.unreactiveProperties].add(arg1);
3355
3200
  for (const arg of args)
3356
- original.prototype[unreactiveProperties].add(arg);
3201
+ original.prototype[decorator.unreactiveProperties].add(arg);
3357
3202
  return original; // Return the class
3358
3203
  });
3359
3204
  }
@@ -3385,9 +3230,9 @@ function cleanedBy(obj, cleanupFn) {
3385
3230
  */
3386
3231
  function derived(compute) {
3387
3232
  const rv = { value: undefined };
3388
- return cleanedBy(rv, untracked(() => effect(markWithRoot(function derivedEffect(access) {
3233
+ return cleanedBy(rv, untracked(() => effect(function derivedEffect(access) {
3389
3234
  rv.value = compute(access);
3390
- }, compute))));
3235
+ })));
3391
3236
  }
3392
3237
 
3393
3238
  /**
@@ -3417,8 +3262,8 @@ const isArray = Array.isArray;
3417
3262
  Array.isArray = ((value) => isArray(value) ||
3418
3263
  (value &&
3419
3264
  typeof value === 'object' &&
3420
- prototypeForwarding in value &&
3421
- Array.isArray(value[prototypeForwarding])));
3265
+ decorator.prototypeForwarding in value &&
3266
+ Array.isArray(value[decorator.prototypeForwarding])));
3422
3267
  class ReactiveBaseArray {
3423
3268
  // Safe array access with negative indices
3424
3269
  at(index) {
@@ -3631,7 +3476,7 @@ class ReactiveArray extends indexable.Indexable(ReactiveBaseArray, {
3631
3476
  Object.defineProperties(this, {
3632
3477
  // We have to make it double, as [native] must be `unique symbol` - impossible through import
3633
3478
  [native$2]: { value: original },
3634
- [prototypeForwarding]: { value: original },
3479
+ [decorator.prototypeForwarding]: { value: original },
3635
3480
  });
3636
3481
  }
3637
3482
  push(...items) {
@@ -3762,7 +3607,7 @@ class ReactiveReadOnlyArrayClass extends indexable.Indexable(ReactiveBaseArray,
3762
3607
  Object.defineProperties(this, {
3763
3608
  // We have to make it double, as [native] must be `unique symbol` - impossible through import
3764
3609
  [native$2]: { value: original },
3765
- [prototypeForwarding]: { value: original },
3610
+ [decorator.prototypeForwarding]: { value: original },
3766
3611
  });
3767
3612
  }
3768
3613
  push(..._items) {
@@ -3843,6 +3688,7 @@ function reduced(inputs, compute) {
3843
3688
  }
3844
3689
 
3845
3690
  const memoizedRegistry = new WeakMap();
3691
+ const wrapperRegistry = new WeakMap();
3846
3692
  function getBranch(tree, key) {
3847
3693
  tree.branches ?? (tree.branches = new WeakMap());
3848
3694
  let branch = tree.branches.get(key);
@@ -3863,15 +3709,30 @@ function memoizeFunction(fn) {
3863
3709
  if (localArgs.some((arg) => !(arg && ['object', 'symbol', 'function'].includes(typeof arg))))
3864
3710
  throw new Error('memoize expects non-null object arguments');
3865
3711
  let node = cacheRoot;
3712
+ // Note: decorators add `this` as first argument
3866
3713
  for (const arg of localArgs) {
3867
3714
  node = getBranch(node, arg);
3868
3715
  }
3869
3716
  dependant(node, 'memoize');
3870
- if ('result' in node)
3717
+ if ('result' in node) {
3718
+ if (decorator.options.onMemoizationDiscrepancy) {
3719
+ const wasVerification = decorator.options.isVerificationRun;
3720
+ decorator.options.isVerificationRun = true;
3721
+ try {
3722
+ const fresh = untracked(() => fn(...localArgs));
3723
+ if (!decorator.deepCompare(node.result, fresh)) {
3724
+ decorator.options.onMemoizationDiscrepancy(node.result, fresh, fn, localArgs, 'calculation');
3725
+ }
3726
+ }
3727
+ finally {
3728
+ decorator.options.isVerificationRun = wasVerification;
3729
+ }
3730
+ }
3871
3731
  return node.result;
3732
+ }
3872
3733
  // Create memoize internal effect to track dependencies and invalidate cache
3873
3734
  // Use untracked to prevent the effect creation from being affected by parent effects
3874
- node.cleanup = root(() => effect(markWithRoot(() => {
3735
+ node.cleanup = root(() => effect(() => {
3875
3736
  // Execute the function and track its dependencies
3876
3737
  // The function execution will automatically track dependencies on reactive objects
3877
3738
  node.result = fn(...localArgs);
@@ -3879,8 +3740,27 @@ function memoizeFunction(fn) {
3879
3740
  // When dependencies change, clear the cache and notify consumers
3880
3741
  delete node.result;
3881
3742
  touched1(node, { type: 'invalidate', prop: localArgs }, 'memoize');
3743
+ // Lazy memoization: stop the effect so it doesn't re-run immediately.
3744
+ // It will be re-created on next access.
3745
+ if (node.cleanup) {
3746
+ node.cleanup();
3747
+ node.cleanup = undefined;
3748
+ }
3882
3749
  };
3883
- }, fnRoot), { opaque: true }));
3750
+ }, { opaque: true }));
3751
+ if (decorator.options.onMemoizationDiscrepancy) {
3752
+ const wasVerification = decorator.options.isVerificationRun;
3753
+ decorator.options.isVerificationRun = true;
3754
+ try {
3755
+ const fresh = untracked(() => fn(...localArgs));
3756
+ if (!decorator.deepCompare(node.result, fresh)) {
3757
+ decorator.options.onMemoizationDiscrepancy(node.result, fresh, fn, localArgs, 'comparison');
3758
+ }
3759
+ }
3760
+ finally {
3761
+ decorator.options.isVerificationRun = wasVerification;
3762
+ }
3763
+ }
3884
3764
  return node.result;
3885
3765
  }, fn);
3886
3766
  memoizedRegistry.set(fnRoot, memoized);
@@ -3888,19 +3768,37 @@ function memoizeFunction(fn) {
3888
3768
  return memoized;
3889
3769
  }
3890
3770
  const memoize = decorator.decorator({
3891
- getter(original, propertyKey) {
3892
- const memoized = memoizeFunction(markWithRoot(decorator.renamed((that) => {
3893
- return original.call(that);
3894
- }, `${String(this.constructor.name)}.${String(propertyKey)}`), original));
3771
+ getter(original, target, propertyKey) {
3895
3772
  return function () {
3773
+ let wrapper = wrapperRegistry.get(original);
3774
+ if (!wrapper) {
3775
+ wrapper = markWithRoot(decorator.renamed((that) => {
3776
+ return original.call(that);
3777
+ }, `${String(target?.constructor?.name ?? target?.name ?? 'Object')}.${String(propertyKey)}`), {
3778
+ method: original,
3779
+ propertyKey,
3780
+ ...(original[decorator.rootFunction] ? { [decorator.rootFunction]: original[decorator.rootFunction] } : {}),
3781
+ });
3782
+ wrapperRegistry.set(original, wrapper);
3783
+ }
3784
+ const memoized = memoizeFunction(wrapper);
3896
3785
  return memoized(this);
3897
3786
  };
3898
3787
  },
3899
- method(original, name) {
3900
- const memoized = memoizeFunction(markWithRoot(decorator.renamed((that, ...args) => {
3901
- return original.call(that, ...args);
3902
- }, `${String(this.constructor.name)}.${String(name)}`), original));
3788
+ method(original, target, name) {
3903
3789
  return function (...args) {
3790
+ let wrapper = wrapperRegistry.get(original);
3791
+ if (!wrapper) {
3792
+ wrapper = markWithRoot(decorator.renamed((that, ...args) => {
3793
+ return original.call(that, ...args);
3794
+ }, `${String(target?.constructor?.name ?? target?.name ?? 'Object')}.${String(name)}`), {
3795
+ method: original,
3796
+ propertyKey: name,
3797
+ ...(original[decorator.rootFunction] ? { [decorator.rootFunction]: original[decorator.rootFunction] } : {}),
3798
+ });
3799
+ wrapperRegistry.set(original, wrapper);
3800
+ }
3801
+ const memoized = memoizeFunction(wrapper);
3904
3802
  return memoized(this, ...args);
3905
3803
  };
3906
3804
  },
@@ -3962,7 +3860,7 @@ let RegisterClass = (() => {
3962
3860
  _tslib.__classPrivateFieldSet(this, _RegisterClass_keys, reactive([]), "f");
3963
3861
  _tslib.__classPrivateFieldSet(this, _RegisterClass_values, reactive(new Map()), "f");
3964
3862
  Object.defineProperties(this, {
3965
- [prototypeForwarding]: { value: _tslib.__classPrivateFieldGet(this, _RegisterClass_keys, "f") },
3863
+ [decorator.prototypeForwarding]: { value: _tslib.__classPrivateFieldGet(this, _RegisterClass_keys, "f") },
3966
3864
  });
3967
3865
  if (initial)
3968
3866
  this.push(...initial);
@@ -4331,7 +4229,7 @@ function defineAccessValue(access) {
4331
4229
  }
4332
4230
  function makeCleanup(target, effectMap, onDispose, metadata) {
4333
4231
  if (metadata) {
4334
- Object.defineProperty(target, projectionInfo, {
4232
+ Object.defineProperty(target, decorator.projectionInfo, {
4335
4233
  value: metadata,
4336
4234
  writable: false,
4337
4235
  enumerable: false,
@@ -4738,7 +4636,7 @@ class ReactiveWeakMap {
4738
4636
  constructor(original) {
4739
4637
  Object.defineProperties(this, {
4740
4638
  [native$1]: { value: original },
4741
- [prototypeForwarding]: { value: original },
4639
+ [decorator.prototypeForwarding]: { value: original },
4742
4640
  content: { value: Symbol('WeakMapContent') },
4743
4641
  [Symbol.toStringTag]: { value: 'ReactiveWeakMap' },
4744
4642
  });
@@ -4778,7 +4676,7 @@ class ReactiveMap {
4778
4676
  constructor(original) {
4779
4677
  Object.defineProperties(this, {
4780
4678
  [native$1]: { value: original },
4781
- [prototypeForwarding]: { value: original },
4679
+ [decorator.prototypeForwarding]: { value: original },
4782
4680
  content: { value: Symbol('MapContent') },
4783
4681
  [Symbol.toStringTag]: { value: 'ReactiveMap' },
4784
4682
  });
@@ -4873,7 +4771,7 @@ class ReactiveWeakSet {
4873
4771
  constructor(original) {
4874
4772
  Object.defineProperties(this, {
4875
4773
  [native]: { value: original },
4876
- [prototypeForwarding]: { value: original },
4774
+ [decorator.prototypeForwarding]: { value: original },
4877
4775
  content: { value: Symbol('WeakSetContent') },
4878
4776
  [Symbol.toStringTag]: { value: 'ReactiveWeakSet' },
4879
4777
  });
@@ -4908,7 +4806,7 @@ class ReactiveSet {
4908
4806
  constructor(original) {
4909
4807
  Object.defineProperties(this, {
4910
4808
  [native]: { value: original },
4911
- [prototypeForwarding]: { value: original },
4809
+ [decorator.prototypeForwarding]: { value: original },
4912
4810
  content: { value: Symbol('SetContent') },
4913
4811
  [Symbol.toStringTag]: { value: 'ReactiveSet' },
4914
4812
  });
@@ -5008,7 +4906,6 @@ const profileInfo = {
5008
4906
  exports.IterableWeakMap = IterableWeakMap;
5009
4907
  exports.IterableWeakSet = IterableWeakSet;
5010
4908
  exports.ReactiveBase = ReactiveBase;
5011
- exports.ReactiveError = ReactiveError;
5012
4909
  exports.ReadOnlyError = ReadOnlyError;
5013
4910
  exports.Register = Register;
5014
4911
  exports.addBatchCleanup = addBatchCleanup;
@@ -5035,7 +4932,6 @@ exports.isZoneEnabled = isZoneEnabled;
5035
4932
  exports.mapped = mapped;
5036
4933
  exports.memoize = memoize;
5037
4934
  exports.mixin = mixin;
5038
- exports.options = options;
5039
4935
  exports.organize = organize;
5040
4936
  exports.organized = organized;
5041
4937
  exports.profileInfo = profileInfo;
@@ -5057,4 +4953,4 @@ exports.unreactive = unreactive;
5057
4953
  exports.untracked = untracked;
5058
4954
  exports.unwrap = unwrap;
5059
4955
  exports.watch = watch;
5060
- //# sourceMappingURL=index-Cvxdw6Ax.js.map
4956
+ //# sourceMappingURL=index-CDCOjzTy.js.map