mutts 1.0.4 → 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 (75) hide show
  1. package/README.md +2 -1
  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-GRBSx0mB.js → index-CDCOjzTy.js} +543 -495
  9. package/dist/chunks/index-CDCOjzTy.js.map +1 -0
  10. package/dist/chunks/{index-79Kk8D6e.esm.js → index-DiP0RXoZ.esm.js} +452 -404
  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 +50 -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 +54 -1
  30. package/dist/reactive.esm.js +3 -3
  31. package/dist/reactive.js +6 -4
  32. package/dist/reactive.js.map +1 -1
  33. package/dist/std-decorators.d.ts +1 -1
  34. package/dist/std-decorators.esm.js +10 -10
  35. package/dist/std-decorators.esm.js.map +1 -1
  36. package/dist/std-decorators.js +10 -10
  37. package/dist/std-decorators.js.map +1 -1
  38. package/docs/ai/manual.md +14 -95
  39. package/docs/reactive/advanced.md +6 -107
  40. package/docs/reactive/core.md +16 -16
  41. package/docs/reactive/debugging.md +158 -0
  42. package/docs/reactive.md +8 -0
  43. package/package.json +16 -66
  44. package/src/decorator.ts +11 -9
  45. package/src/destroyable.ts +5 -5
  46. package/src/index.ts +46 -0
  47. package/src/reactive/array.ts +3 -5
  48. package/src/reactive/change.ts +7 -3
  49. package/src/reactive/debug.ts +1 -1
  50. package/src/reactive/deep-touch.ts +1 -1
  51. package/src/reactive/deep-watch.ts +1 -1
  52. package/src/reactive/effect-context.ts +2 -2
  53. package/src/reactive/effects.ts +114 -17
  54. package/src/reactive/index.ts +3 -2
  55. package/src/reactive/interface.ts +10 -9
  56. package/src/reactive/map.ts +6 -6
  57. package/src/reactive/mapped.ts +2 -3
  58. package/src/reactive/memoize.ts +77 -31
  59. package/src/reactive/project.ts +103 -6
  60. package/src/reactive/proxy.ts +4 -4
  61. package/src/reactive/registry.ts +67 -0
  62. package/src/reactive/set.ts +6 -6
  63. package/src/reactive/tracking.ts +12 -41
  64. package/src/reactive/types.ts +59 -0
  65. package/src/reactive/zone.ts +1 -1
  66. package/src/std-decorators.ts +10 -10
  67. package/src/utils.ts +141 -0
  68. package/dist/chunks/_tslib-Mzh1rNsX.esm.js.map +0 -1
  69. package/dist/chunks/decorator-DLvrD0UF.js +0 -265
  70. package/dist/chunks/decorator-DLvrD0UF.js.map +0 -1
  71. package/dist/chunks/decorator-DqiszP7i.esm.js +0 -253
  72. package/dist/chunks/decorator-DqiszP7i.esm.js.map +0 -1
  73. package/dist/chunks/index-79Kk8D6e.esm.js.map +0 -1
  74. package/dist/chunks/index-GRBSx0mB.js.map +0 -1
  75. /package/{src/reactive/project.project.md → docs/reactive/project.md} +0 -0
@@ -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,313 +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
- // Symbol to mark functions with their root function
358
- const rootFunction = Symbol('root-function');
359
- /**
360
- * Structured error codes for machine-readable diagnosis
361
- */
362
- var ReactiveErrorCode;
363
- (function (ReactiveErrorCode) {
364
- ReactiveErrorCode["CycleDetected"] = "CYCLE_DETECTED";
365
- ReactiveErrorCode["MaxDepthExceeded"] = "MAX_DEPTH_EXCEEDED";
366
- ReactiveErrorCode["MaxReactionExceeded"] = "MAX_REACTION_EXCEEDED";
367
- ReactiveErrorCode["WriteInComputed"] = "WRITE_IN_COMPUTED";
368
- ReactiveErrorCode["TrackingError"] = "TRACKING_ERROR";
369
- })(ReactiveErrorCode || (ReactiveErrorCode = {}));
370
- /**
371
- * Error class for reactive system errors
372
- */
373
- class ReactiveError extends Error {
374
- constructor(message, debugInfo) {
375
- super(message);
376
- this.debugInfo = debugInfo;
377
- this.name = 'ReactiveError';
378
- }
379
- }
380
- // biome-ignore-start lint/correctness/noUnusedFunctionParameters: Interface declaration with empty defaults
381
- /**
382
- * Global options for the reactive system
383
- */
384
- const options = {
385
- /**
386
- * Debug purpose: called when an effect is entered
387
- * @param effect - The effect that is entered
388
- */
389
- enter: (_effect) => { },
390
- /**
391
- * Debug purpose: called when an effect is left
392
- * @param effect - The effect that is left
393
- */
394
- leave: (_effect) => { },
395
- /**
396
- * Debug purpose: called when an effect is chained
397
- * @param target - The effect that is being triggered
398
- * @param caller - The effect that is calling the target
399
- */
400
- chain: (_targets, _caller) => { },
401
- /**
402
- * Debug purpose: called when an effect chain is started
403
- * @param target - The effect that is being triggered
404
- */
405
- beginChain: (_targets) => { },
406
- /**
407
- * Debug purpose: called when an effect chain is ended
408
- */
409
- endChain: () => { },
410
- garbageCollected: (_fn) => { },
411
- /**
412
- * Debug purpose: called when an object is touched
413
- * @param obj - The object that is touched
414
- * @param evolution - The type of change
415
- * @param props - The properties that changed
416
- * @param deps - The dependencies that changed
417
- */
418
- touched: (_obj, _evolution, _props, _deps) => { },
419
- /**
420
- * Debug purpose: called when an effect is skipped because it's already running
421
- * @param effect - The effect that is already running
422
- * @param runningChain - The array of effects from the detected one to the currently running one
423
- */
424
- skipRunningEffect: (_effect, _runningChain) => { },
425
- /**
426
- * Debug purpose: maximum effect chain (like call stack max depth)
427
- * Used to prevent infinite loops
428
- * @default 100
429
- */
430
- maxEffectChain: 100,
431
- /**
432
- * Debug purpose: maximum effect reaction (like call stack max depth)
433
- * Used to prevent infinite loops
434
- * @default 'throw'
435
- */
436
- maxEffectReaction: 'throw',
437
- /**
438
- * How to handle cycles detected in effect batches
439
- * - 'throw': Throw an error with cycle information (default, recommended for development)
440
- * - 'warn': Log a warning and break the cycle by executing one effect
441
- * - 'break': Silently break the cycle by executing one effect (recommended for production)
442
- * - 'strict': Prevent cycle creation by checking graph before execution (throws error)
443
- * @default 'throw'
444
- */
445
- cycleHandling: 'throw',
446
- /**
447
- * Maximum depth for deep watching traversal
448
- * Used to prevent infinite recursion in circular references
449
- * @default 100
450
- */
451
- maxDeepWatchDepth: 100,
452
- /**
453
- * Only react on instance members modification (not inherited properties)
454
- * For instance, do not track class methods
455
- * @default true
456
- */
457
- instanceMembers: true,
458
- /**
459
- * Ignore accessors (getters and setters) and only track direct properties
460
- * @default true
461
- */
462
- ignoreAccessors: true,
463
- /**
464
- * Enable recursive touching when objects with the same prototype are replaced
465
- * When enabled, replacing an object with another of the same prototype triggers
466
- * recursive diffing instead of notifying parent effects
467
- * @default true
468
- */
469
- recursiveTouching: true,
470
- /**
471
- * Default async execution mode for effects that return Promises
472
- * - 'cancel': Cancel previous async execution when dependencies change (default, enables async zone)
473
- * - 'queue': Queue next execution to run after current completes (enables async zone)
474
- * - 'ignore': Ignore new executions while async work is running (enables async zone)
475
- * - false: Disable async zone and async mode handling (effects run concurrently)
476
- *
477
- * **When truthy:** Enables async zone (Promise.prototype wrapping) for automatic context
478
- * preservation in Promise callbacks. Warning: This modifies Promise.prototype globally.
479
- * Only enable if no other library modifies Promise.prototype.
480
- *
481
- * **When false:** Async zone is disabled. Use `tracked()` manually in Promise callbacks.
482
- *
483
- * Can be overridden per-effect via EffectOptions
484
- * @default 'cancel'
485
- */
486
- asyncMode: 'cancel',
487
- // biome-ignore lint/suspicious/noConsole: This is the whole point here
488
- warn: (...args) => console.warn(...args),
489
- /**
490
- * Configuration for the introspection system
491
- */
492
- introspection: {
493
- /**
494
- * Whether to keep a history of mutations for debugging
495
- * @default false
496
- */
497
- enableHistory: false,
498
- /**
499
- * Number of mutations to keep in history
500
- * @default 50
501
- */
502
- historySize: 50,
503
- },
504
- /**
505
- * Configuration for zone hooks - control which async APIs are hooked
506
- * Each option controls whether the corresponding async API is wrapped to preserve effect context
507
- * Only applies when asyncMode is enabled (truthy)
508
- */
509
- zones: {
510
- /**
511
- * Hook setTimeout to preserve effect context
512
- * @default true
513
- */
514
- setTimeout: true,
515
- /**
516
- * Hook setInterval to preserve effect context
517
- * @default true
518
- */
519
- setInterval: true,
520
- /**
521
- * Hook requestAnimationFrame (runs in untracked context when hooked)
522
- * @default true
523
- */
524
- requestAnimationFrame: true,
525
- /**
526
- * Hook queueMicrotask to preserve effect context
527
- * @default true
528
- */
529
- queueMicrotask: true,
530
- },
531
- };
532
-
533
- /**
534
- * Effect context stack for nested tracking (front = active, next = parent)
535
- */
536
- const stack = [];
537
- function captureEffectStack() {
538
- return stack.slice();
539
- }
540
- function isRunning(effect) {
541
- const rootEffect = getRoot(effect);
542
- // Check if the effect is directly in the stack
543
- const rootIndex = stack.indexOf(rootEffect);
544
- if (rootIndex !== -1) {
545
- return stack.slice(0, rootIndex + 1).reverse();
546
- }
547
- // Check if any effect in the stack is a descendant of this effect
548
- // (i.e., walk up the parent chain from each stack effect to see if we reach this effect)
549
- for (let i = 0; i < stack.length; i++) {
550
- const stackEffect = stack[i];
551
- let current = stackEffect;
552
- const visited = new WeakSet();
553
- const ancestorChain = [];
554
- // TODO: That's perhaps a lot of computations for an `assert`
555
- // Walk up the parent chain to find if this effect is an ancestor
556
- while (current && !visited.has(current)) {
557
- visited.add(current);
558
- const currentRoot = getRoot(current);
559
- ancestorChain.push(currentRoot);
560
- if (currentRoot === rootEffect) {
561
- // Found a descendant - build the full chain from ancestor to active
562
- // The ancestorChain contains [descendant, parent, ..., ancestor] (walking up)
563
- // We need [ancestor (effect), ..., parent, descendant, ...stack from descendant to active]
564
- const chainFromAncestor = ancestorChain.reverse(); // [ancestor, ..., descendant]
565
- // Prepend the actual effect we're checking (in case current is a wrapper)
566
- if (chainFromAncestor[0] !== rootEffect) {
567
- chainFromAncestor.unshift(rootEffect);
568
- }
569
- // Append the rest of the stack from the descendant to the active effect
570
- const stackFromDescendant = stack.slice(0, i + 1).reverse(); // [descendant, ..., active]
571
- // Remove duplicate descendant (it's both at end of chainFromAncestor and start of stackFromDescendant)
572
- if (chainFromAncestor.length > 0 && stackFromDescendant.length > 0) {
573
- stackFromDescendant.shift(); // Remove duplicate descendant
574
- }
575
- return [...chainFromAncestor, ...stackFromDescendant];
576
- }
577
- current = effectParent.get(current);
578
- }
579
- }
580
- return false;
581
- }
582
- function withEffectStack(snapshot, fn) {
583
- const previousStack = stack.slice();
584
- assignStack(snapshot);
585
- try {
586
- return fn();
587
- }
588
- finally {
589
- assignStack(previousStack);
590
- }
591
- }
592
- function getActiveEffect() {
593
- return stack[0];
594
- }
595
- /**
596
- * Executes a function with a specific effect context
597
- * @param effect - The effect to use as context
598
- * @param fn - The function to execute
599
- * @param keepParent - Whether to keep the parent effect context
600
- * @returns The result of the function
601
- */
602
- function withEffect(effect, fn) {
603
- // console.log('[Mutts] withEffect', effect ? 'Active' : 'NULL');
604
- if (getRoot(effect) === getRoot(getActiveEffect()))
605
- return fn();
606
- stack.unshift(effect);
607
- try {
608
- return fn();
609
- }
610
- finally {
611
- const recoveredEffect = stack.shift();
612
- if (recoveredEffect !== effect)
613
- throw new ReactiveError('[reactive] Effect stack mismatch');
614
- }
615
- }
616
- function assignStack(values) {
617
- stack.length = 0;
618
- stack.push(...values);
619
- }
620
-
621
- const objectToProxy = new WeakMap();
622
- const proxyToObject = new WeakMap();
623
- function storeProxyRelationship(target, proxy) {
624
- objectToProxy.set(target, proxy);
625
- proxyToObject.set(proxy, target);
626
- }
627
- function getExistingProxy(target) {
628
- return objectToProxy.get(target);
629
- }
630
- function trackProxyObject(proxy, target) {
631
- proxyToObject.set(proxy, target);
632
- }
633
- function unwrap(obj) {
634
- let current = obj;
635
- while (current && typeof current === 'object' && current !== null && proxyToObject.has(current)) {
636
- current = proxyToObject.get(current);
637
- }
638
- return current;
639
- }
640
- function isReactive(obj) {
641
- return proxyToObject.has(obj);
642
- }
643
-
644
337
  // Track which effects are watching which reactive objects for cleanup
645
338
  const effectToReactiveObjects = new WeakMap();
646
339
  // Track effects per reactive object and property
@@ -649,15 +342,31 @@ const watchers = new WeakMap();
649
342
  const effectChildren = new WeakMap();
650
343
  // Track parent effect relationships for hierarchy traversal (used in deep touch filtering)
651
344
  const effectParent = new WeakMap();
345
+ // Track reverse mapping to ensure unicity: One Root -> One Function
346
+ const reverseRoots = new WeakMap();
652
347
  /**
653
348
  * Marks a function with its root function for effect tracking
349
+ * Enforces strict unicity: A root function can only identify ONE function.
654
350
  * @param fn - The function to mark
655
351
  * @param root - The root function
656
352
  * @returns The marked function
657
353
  */
658
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));
659
368
  // Mark fn with the new root
660
- return Object.defineProperty(fn, rootFunction, {
369
+ return Object.defineProperty(fn, decorator.rootFunction, {
661
370
  value: getRoot(root),
662
371
  writable: false,
663
372
  });
@@ -668,64 +377,15 @@ function markWithRoot(fn, root) {
668
377
  * @returns The root function
669
378
  */
670
379
  function getRoot(fn) {
671
- return fn?.[rootFunction] || fn;
380
+ while (fn && decorator.rootFunction in fn)
381
+ fn = fn[decorator.rootFunction];
382
+ return fn;
672
383
  }
673
384
  // Flag to disable dependency tracking for the current active effect (not globally)
674
385
  const trackingDisabledEffects = new WeakSet();
675
386
  let globalTrackingDisabled = false;
676
- function getTrackingDisabled() {
677
- const active = getActiveEffect();
678
- if (!active)
679
- return globalTrackingDisabled;
680
- return trackingDisabledEffects.has(getRoot(active));
681
- }
682
- function setTrackingDisabled(value) {
683
- const active = getActiveEffect();
684
- if (!active) {
685
- globalTrackingDisabled = value;
686
- return;
687
- }
688
- const root = getRoot(active);
689
- if (value)
690
- trackingDisabledEffects.add(root);
691
- else
692
- trackingDisabledEffects.delete(root);
693
- }
694
- /**
695
- * Marks a property as a dependency of the current effect
696
- * @param obj - The object containing the property
697
- * @param prop - The property name (defaults to allProps)
698
- */
699
- function dependant(obj, prop = allProps) {
700
- obj = unwrap(obj);
701
- const currentActiveEffect = getActiveEffect();
702
- // Early return if no active effect, tracking disabled, or invalid prop
703
- if (!currentActiveEffect ||
704
- getTrackingDisabled() ||
705
- (typeof prop === 'symbol' && prop !== allProps))
706
- return;
707
- registerDependency(obj, prop, currentActiveEffect);
708
- }
709
- function registerDependency(obj, prop, currentActiveEffect) {
710
- let objectWatchers = watchers.get(obj);
711
- if (!objectWatchers) {
712
- objectWatchers = new Map();
713
- watchers.set(obj, objectWatchers);
714
- }
715
- let deps = objectWatchers.get(prop);
716
- if (!deps) {
717
- deps = new Set();
718
- objectWatchers.set(prop, deps);
719
- }
720
- deps.add(currentActiveEffect);
721
- // Track which reactive objects this effect is watching
722
- const effectObjects = effectToReactiveObjects.get(currentActiveEffect);
723
- if (effectObjects) {
724
- effectObjects.add(obj);
725
- }
726
- else {
727
- effectToReactiveObjects.set(currentActiveEffect, new Set([obj]));
728
- }
387
+ function setGlobalTrackingDisabled(value) {
388
+ globalTrackingDisabled = value;
729
389
  }
730
390
 
731
391
  /**
@@ -766,7 +426,7 @@ function ensureObjectName(obj) {
766
426
  }
767
427
  function describeProp(obj, prop) {
768
428
  const objectName = ensureObjectName(obj);
769
- if (prop === allProps)
429
+ if (prop === decorator.allProps)
770
430
  return `${objectName}.*`;
771
431
  if (typeof prop === 'symbol')
772
432
  return `${objectName}.${prop.description ?? prop.toString()}`;
@@ -863,7 +523,7 @@ function registerObjectForDebug(obj) {
863
523
  * @param evolution - The type of change (set/add/del/bunch)
864
524
  */
865
525
  function recordTriggerLink(source, target, obj, prop, evolution) {
866
- if (options.introspection.enableHistory) {
526
+ if (decorator.options.introspection.enableHistory) {
867
527
  addToMutationHistory(source, target, obj, prop, evolution);
868
528
  }
869
529
  if (!devtoolsEnabled)
@@ -1075,11 +735,176 @@ function addToMutationHistory(source, target, obj, prop, evolution) {
1075
735
  type: evolution.type,
1076
736
  };
1077
737
  mutationHistory.push(record);
1078
- if (mutationHistory.length > options.introspection.historySize) {
738
+ if (mutationHistory.length > decorator.options.introspection.historySize) {
1079
739
  mutationHistory.shift();
1080
740
  }
1081
741
  }
1082
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
+
1083
908
  /**
1084
909
  * Zone-like async context preservation for reactive effects
1085
910
  *
@@ -1120,7 +945,7 @@ let batchFn;
1120
945
  function ensureZoneHooked(batch) {
1121
946
  if (batch)
1122
947
  batchFn = batch;
1123
- if (zoneHooked || !options.asyncMode)
948
+ if (zoneHooked || !decorator.options.asyncMode)
1124
949
  return;
1125
950
  hookZone();
1126
951
  zoneHooked = true;
@@ -1144,7 +969,7 @@ function hookZone() {
1144
969
  };
1145
970
  // Hook setTimeout - preserve original function properties for Node.js compatibility
1146
971
  const wrappedSetTimeout = ((callback, delay, ...args) => {
1147
- const capturedStack = options.zones.setTimeout ? captureEffectStack() : undefined;
972
+ const capturedStack = decorator.options.zones.setTimeout ? captureEffectStack() : undefined;
1148
973
  return originalSetTimeout.apply(globalThis, [
1149
974
  wrapCallback(callback, capturedStack),
1150
975
  delay,
@@ -1155,7 +980,7 @@ function hookZone() {
1155
980
  globalThis.setTimeout = wrappedSetTimeout;
1156
981
  // Hook setInterval - preserve original function properties for Node.js compatibility
1157
982
  const wrappedSetInterval = ((callback, delay, ...args) => {
1158
- const capturedStack = options.zones.setInterval ? captureEffectStack() : undefined;
983
+ const capturedStack = decorator.options.zones.setInterval ? captureEffectStack() : undefined;
1159
984
  return originalSetInterval.apply(globalThis, [
1160
985
  wrapCallback(callback, capturedStack),
1161
986
  delay,
@@ -1167,14 +992,14 @@ function hookZone() {
1167
992
  // Hook requestAnimationFrame if available
1168
993
  if (originalRequestAnimationFrame) {
1169
994
  globalThis.requestAnimationFrame = ((callback) => {
1170
- const capturedStack = options.zones.requestAnimationFrame ? captureEffectStack() : undefined;
995
+ const capturedStack = decorator.options.zones.requestAnimationFrame ? captureEffectStack() : undefined;
1171
996
  return originalRequestAnimationFrame.call(globalThis, wrapCallback(callback, capturedStack));
1172
997
  });
1173
998
  }
1174
999
  // Hook queueMicrotask if available
1175
1000
  if (originalQueueMicrotask) {
1176
1001
  globalThis.queueMicrotask = ((callback) => {
1177
- const capturedStack = options.zones.queueMicrotask ? captureEffectStack() : undefined;
1002
+ const capturedStack = decorator.options.zones.queueMicrotask ? captureEffectStack() : undefined;
1178
1003
  originalQueueMicrotask.call(globalThis, wrapCallback(callback, capturedStack));
1179
1004
  });
1180
1005
  }
@@ -1260,6 +1085,50 @@ function formatRoots(roots, limit = 20) {
1260
1085
  const end = names.slice(-10);
1261
1086
  return `${start.join(' → ')} ... (${names.length - 15} more) ... ${end.join(' → ')}`;
1262
1087
  }
1088
+ // Nested map structure for efficient counting and batch cleanup
1089
+ // batchId -> effect root -> obj -> prop -> count
1090
+ let activationRegistry;
1091
+ const activationLog = new Array(100);
1092
+ function getActivationLog() {
1093
+ return activationLog;
1094
+ }
1095
+ function recordActivation(effect, obj, evolution, prop) {
1096
+ const root = getRoot(effect);
1097
+ if (!activationRegistry)
1098
+ return;
1099
+ let effectData = activationRegistry.get(root);
1100
+ if (!effectData) {
1101
+ effectData = new Map();
1102
+ activationRegistry.set(root, effectData);
1103
+ }
1104
+ let objData = effectData.get(obj);
1105
+ if (!objData) {
1106
+ objData = new Map();
1107
+ effectData.set(obj, objData);
1108
+ }
1109
+ const count = (objData.get(prop) ?? 0) + 1;
1110
+ objData.set(prop, count);
1111
+ // Keep a limited history for diagnostics
1112
+ activationLog.unshift({
1113
+ effect,
1114
+ obj,
1115
+ evolution,
1116
+ prop,
1117
+ });
1118
+ activationLog.pop();
1119
+ if (count >= decorator.options.maxTriggerPerBatch) {
1120
+ const effectName = root?.name || 'anonymous';
1121
+ const message = `Aggressive trigger detected: effect "${effectName}" triggered ${count} times in the batch by the same cause.`;
1122
+ if (decorator.options.maxEffectReaction === 'throw') {
1123
+ throw new decorator.ReactiveError(message, {
1124
+ code: decorator.ReactiveErrorCode.MaxReactionExceeded,
1125
+ count,
1126
+ effect: effectName,
1127
+ });
1128
+ }
1129
+ decorator.options.warn(`[reactive] ${message}`);
1130
+ }
1131
+ }
1263
1132
  /**
1264
1133
  * Registers a debug callback that is called when the current effect is triggered by a dependency change
1265
1134
  *
@@ -1421,6 +1290,7 @@ function addGraphEdge(callerRoot, targetRoot) {
1421
1290
  * @param end - Target node
1422
1291
  * @param exclude - Node to exclude from the path
1423
1292
  * @returns true if a path exists without going through the excluded node
1293
+ * @todo Can be REALLY costly - optimise or make optional or ...
1424
1294
  */
1425
1295
  function hasPathExcluding(start, end, exclude) {
1426
1296
  if (start === end)
@@ -1549,6 +1419,9 @@ function cleanupEffectFromGraph(effect) {
1549
1419
  // Track currently executing effects to prevent re-execution
1550
1420
  // These are all the effects triggered under `activeEffect`
1551
1421
  let batchQueue;
1422
+ function hasBatched(effect) {
1423
+ return batchQueue?.all.has(getRoot(effect));
1424
+ }
1552
1425
  const batchCleanups = new Set();
1553
1426
  /**
1554
1427
  * Computes and caches in-degrees for all effects in the batch
@@ -1705,6 +1578,12 @@ function wouldCreateCycle(callerRoot, targetRoot) {
1705
1578
  * @param immediate - If true, don't create edges in the dependency graph
1706
1579
  */
1707
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;
1708
1587
  if (!batchQueue)
1709
1588
  return;
1710
1589
  const root = getRoot(effect);
@@ -1723,14 +1602,14 @@ function addToBatch(effect, caller, immediate) {
1723
1602
  const cycleMessage = cyclePath.length > 0
1724
1603
  ? `Cycle detected: ${cyclePath.map((r) => r.name || r.toString()).join(' → ')}`
1725
1604
  : `Cycle detected: ${callerRoot.name || callerRoot.toString()} → ${root.name || root.toString()} (and back)`;
1726
- const cycleHandling = options.cycleHandling;
1605
+ const cycleHandling = decorator.options.cycleHandling;
1727
1606
  // In strict mode, we throw immediately on detection
1728
1607
  if (cycleHandling === 'strict') {
1729
1608
  batchQueue.all.delete(root);
1730
1609
  const causalChain = getTriggerChain(effect);
1731
1610
  const creationStack = effectCreationStacks.get(root);
1732
- throw new ReactiveError(`[reactive] Strict Cycle Prevention: ${cycleMessage}`, {
1733
- code: ReactiveErrorCode.CycleDetected,
1611
+ throw new decorator.ReactiveError(`[reactive] Strict Cycle Prevention: ${cycleMessage}`, {
1612
+ code: decorator.ReactiveErrorCode.CycleDetected,
1734
1613
  cycle: cyclePath.map((r) => r.name || r.toString()),
1735
1614
  details: cycleMessage,
1736
1615
  causalChain,
@@ -1743,8 +1622,8 @@ function addToBatch(effect, caller, immediate) {
1743
1622
  batchQueue.all.delete(root);
1744
1623
  const causalChain = getTriggerChain(effect);
1745
1624
  const creationStack = effectCreationStacks.get(root);
1746
- throw new ReactiveError(`[reactive] ${cycleMessage}`, {
1747
- code: ReactiveErrorCode.CycleDetected,
1625
+ throw new decorator.ReactiveError(`[reactive] ${cycleMessage}`, {
1626
+ code: decorator.ReactiveErrorCode.CycleDetected,
1748
1627
  cycle: cyclePath.map((r) => r.name || r.toString()),
1749
1628
  details: cycleMessage,
1750
1629
  causalChain,
@@ -1752,7 +1631,7 @@ function addToBatch(effect, caller, immediate) {
1752
1631
  });
1753
1632
  }
1754
1633
  case 'warn':
1755
- options.warn(`[reactive] ${cycleMessage}`);
1634
+ decorator.options.warn(`[reactive] ${cycleMessage}`);
1756
1635
  // Don't add the edge, break the cycle
1757
1636
  batchQueue.all.delete(root);
1758
1637
  return;
@@ -1901,12 +1780,12 @@ function executeNext(effectuatedRoots) {
1901
1780
  const cycleMessage = cycle.length > 0
1902
1781
  ? `Cycle detected: ${cycle.map((r) => r.name || '<anonymous>').join(' → ')}`
1903
1782
  : 'Cycle detected in effect batch - all effects have dependencies that prevent execution';
1904
- const cycleHandling = options.cycleHandling;
1783
+ const cycleHandling = decorator.options.cycleHandling;
1905
1784
  switch (cycleHandling) {
1906
1785
  case 'throw':
1907
- throw new ReactiveError(`[reactive] ${cycleMessage}`);
1786
+ throw new decorator.ReactiveError(`[reactive] ${cycleMessage}`);
1908
1787
  case 'warn': {
1909
- options.warn(`[reactive] ${cycleMessage}`);
1788
+ decorator.options.warn(`[reactive] ${cycleMessage}`);
1910
1789
  // Break the cycle by executing one effect anyway
1911
1790
  const firstEffect = batchQueue.all.values().next().value;
1912
1791
  if (firstEffect) {
@@ -1949,7 +1828,7 @@ function batch(effect, immediate) {
1949
1828
  const roots = effect.map(getRoot);
1950
1829
  if (batchQueue) {
1951
1830
  // Nested batch - add to existing
1952
- options?.chain(roots, getRoot(getActiveEffect()));
1831
+ decorator.options?.chain(roots, getRoot(getActiveEffect()));
1953
1832
  const caller = getActiveEffect();
1954
1833
  for (let i = 0; i < effect.length; i++) {
1955
1834
  addToBatch(effect[i], caller, immediate === 'immediate');
@@ -1974,7 +1853,11 @@ function batch(effect, immediate) {
1974
1853
  }
1975
1854
  else {
1976
1855
  // New batch - initialize
1977
- options.beginChain(roots);
1856
+ if (!activationRegistry)
1857
+ activationRegistry = new Map();
1858
+ else
1859
+ throw new Error('Batch already in progress');
1860
+ decorator.options.beginChain(roots);
1978
1861
  batchQueue = {
1979
1862
  all: new Map(),
1980
1863
  inDegrees: new Map(),
@@ -2004,7 +1887,7 @@ function batch(effect, immediate) {
2004
1887
  // After immediate execution, execute any effects that were triggered during execution
2005
1888
  // This is important for @atomic decorator - effects triggered inside should still run
2006
1889
  while (batchQueue.all.size > 0) {
2007
- if (effectuatedRoots.length > options.maxEffectChain) {
1890
+ if (effectuatedRoots.length > decorator.options.maxEffectChain) {
2008
1891
  const cycle = findCycleInChain(effectuatedRoots);
2009
1892
  const trace = formatRoots(effectuatedRoots);
2010
1893
  const message = cycle
@@ -2013,11 +1896,11 @@ function batch(effect, immediate) {
2013
1896
  const queuedRoots = batchQueue ? Array.from(batchQueue.all.keys()) : [];
2014
1897
  const queued = queuedRoots.map((r) => r.name || '<anonymous>');
2015
1898
  const debugInfo = {
2016
- code: ReactiveErrorCode.MaxDepthExceeded,
1899
+ code: decorator.ReactiveErrorCode.MaxDepthExceeded,
2017
1900
  effectuatedRoots,
2018
1901
  cycle,
2019
1902
  trace,
2020
- maxEffectChain: options.maxEffectChain,
1903
+ maxEffectChain: decorator.options.maxEffectChain,
2021
1904
  queued: queued.slice(0, 50),
2022
1905
  queuedCount: queued.length,
2023
1906
  // Try to get causation for the last effect
@@ -2025,15 +1908,15 @@ function batch(effect, immediate) {
2025
1908
  ? getTriggerChain(batchQueue.all.get(effectuatedRoots[effectuatedRoots.length - 1]))
2026
1909
  : [],
2027
1910
  };
2028
- switch (options.maxEffectReaction) {
1911
+ switch (decorator.options.maxEffectReaction) {
2029
1912
  case 'throw':
2030
- throw new ReactiveError(`[reactive] ${message}`, debugInfo);
1913
+ throw new decorator.ReactiveError(`[reactive] ${message}`, debugInfo);
2031
1914
  case 'debug':
2032
1915
  // biome-ignore lint/suspicious/noDebugger: This is the whole point here
2033
1916
  debugger;
2034
- throw new ReactiveError(`[reactive] ${message}`, debugInfo);
1917
+ throw new decorator.ReactiveError(`[reactive] ${message}`, debugInfo);
2035
1918
  case 'warn':
2036
- 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 ? ', …' : ''})`);
2037
1920
  break;
2038
1921
  }
2039
1922
  }
@@ -2052,8 +1935,9 @@ function batch(effect, immediate) {
2052
1935
  return firstReturn.value;
2053
1936
  }
2054
1937
  finally {
1938
+ activationRegistry = undefined;
2055
1939
  batchQueue = undefined;
2056
- options.endChain();
1940
+ decorator.options.endChain();
2057
1941
  }
2058
1942
  }
2059
1943
  else {
@@ -2065,7 +1949,7 @@ function batch(effect, immediate) {
2065
1949
  while (batchQueue.all.size > 0 || batchCleanups.size > 0) {
2066
1950
  // Inner loop: execute all pending effects
2067
1951
  while (batchQueue.all.size > 0) {
2068
- if (effectuatedRoots.length > options.maxEffectChain) {
1952
+ if (effectuatedRoots.length > decorator.options.maxEffectChain) {
2069
1953
  const cycle = findCycleInChain(effectuatedRoots);
2070
1954
  const trace = formatRoots(effectuatedRoots);
2071
1955
  const message = cycle
@@ -2074,11 +1958,11 @@ function batch(effect, immediate) {
2074
1958
  const queuedRoots = batchQueue ? Array.from(batchQueue.all.keys()) : [];
2075
1959
  const queued = queuedRoots.map((r) => r.name || '<anonymous>');
2076
1960
  const debugInfo = {
2077
- code: ReactiveErrorCode.MaxDepthExceeded,
1961
+ code: decorator.ReactiveErrorCode.MaxDepthExceeded,
2078
1962
  effectuatedRoots,
2079
1963
  cycle,
2080
1964
  trace,
2081
- maxEffectChain: options.maxEffectChain,
1965
+ maxEffectChain: decorator.options.maxEffectChain,
2082
1966
  queued: queued.slice(0, 50),
2083
1967
  queuedCount: queued.length,
2084
1968
  // Try to get causation for the last effect
@@ -2086,15 +1970,15 @@ function batch(effect, immediate) {
2086
1970
  ? getTriggerChain(batchQueue.all.get(effectuatedRoots[effectuatedRoots.length - 1]))
2087
1971
  : [],
2088
1972
  };
2089
- switch (options.maxEffectReaction) {
1973
+ switch (decorator.options.maxEffectReaction) {
2090
1974
  case 'throw':
2091
- throw new ReactiveError(`[reactive] ${message}`, debugInfo);
1975
+ throw new decorator.ReactiveError(`[reactive] ${message}`, debugInfo);
2092
1976
  case 'debug':
2093
1977
  // biome-ignore lint/suspicious/noDebugger: This is the whole point here
2094
1978
  debugger;
2095
- throw new ReactiveError(`[reactive] ${message}`, debugInfo);
1979
+ throw new decorator.ReactiveError(`[reactive] ${message}`, debugInfo);
2096
1980
  case 'warn':
2097
- 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 ? ', …' : ''})`);
2098
1982
  break;
2099
1983
  }
2100
1984
  }
@@ -2122,8 +2006,9 @@ function batch(effect, immediate) {
2122
2006
  return firstReturn.value;
2123
2007
  }
2124
2008
  finally {
2009
+ activationRegistry = undefined;
2125
2010
  batchQueue = undefined;
2126
- options.endChain();
2011
+ decorator.options.endChain();
2127
2012
  }
2128
2013
  }
2129
2014
  }
@@ -2134,12 +2019,18 @@ function batch(effect, immediate) {
2134
2019
  const atomic = decorator.decorator({
2135
2020
  method(original) {
2136
2021
  return function (...args) {
2137
- 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');
2138
2026
  };
2139
2027
  },
2140
2028
  default(original) {
2141
2029
  return function (...args) {
2142
- 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');
2143
2034
  };
2144
2035
  },
2145
2036
  });
@@ -2161,8 +2052,8 @@ fn, effectOptions) {
2161
2052
  // Inject batch function to allow atomic game loops in requestAnimationFrame
2162
2053
  ensureZoneHooked(batch);
2163
2054
  // Use per-effect asyncMode or fall back to global option
2164
- const asyncMode = effectOptions?.asyncMode ?? options.asyncMode ?? 'cancel';
2165
- if (options.introspection.enableHistory) {
2055
+ const asyncMode = effectOptions?.asyncMode ?? decorator.options.asyncMode ?? 'cancel';
2056
+ if (decorator.options.introspection.enableHistory) {
2166
2057
  const stack = new Error().stack;
2167
2058
  if (stack) {
2168
2059
  // Clean up the stack trace to remove internal frames
@@ -2173,7 +2064,7 @@ fn, effectOptions) {
2173
2064
  let cleanup = null;
2174
2065
  // capture the parent effect at creation time for ascend
2175
2066
  const parentsForAscend = captureEffectStack();
2176
- const tracked = markWithRoot((cb) => withEffect(runEffect, cb), fn);
2067
+ const tracked = (cb) => withEffect(runEffect, cb);
2177
2068
  const ascend = (cb) => withEffectStack(parentsForAscend, cb);
2178
2069
  let effectStopped = false;
2179
2070
  let hasReacted = false;
@@ -2203,7 +2094,7 @@ fn, effectOptions) {
2203
2094
  // The effect has been stopped after having been planned
2204
2095
  if (effectStopped)
2205
2096
  return;
2206
- options.enter(getRoot(fn));
2097
+ decorator.options.enter(getRoot(fn));
2207
2098
  let reactionCleanup;
2208
2099
  let result;
2209
2100
  try {
@@ -2211,7 +2102,7 @@ fn, effectOptions) {
2211
2102
  if (result &&
2212
2103
  typeof result !== 'function' &&
2213
2104
  (typeof result !== 'object' || !('then' in result)))
2214
- throw new ReactiveError(`[reactive] Effect returned a non-function value: ${result}`);
2105
+ throw new decorator.ReactiveError(`[reactive] Effect returned a non-function value: ${result}`);
2215
2106
  // Check if result is a Promise (async effect)
2216
2107
  if (result && typeof result === 'object' && typeof result.then === 'function') {
2217
2108
  const originalPromise = result;
@@ -2220,7 +2111,7 @@ fn, effectOptions) {
2220
2111
  const cancelPromise = new Promise((_, reject) => {
2221
2112
  cancelReject = reject;
2222
2113
  });
2223
- const cancelError = new ReactiveError('[reactive] Effect canceled due to dependency change');
2114
+ const cancelError = new decorator.ReactiveError('[reactive] Effect canceled due to dependency change');
2224
2115
  // Race between the actual promise and cancellation
2225
2116
  // If canceled, the race rejects, which will propagate through any promise chain
2226
2117
  runningPromise = Promise.race([originalPromise, cancelPromise]);
@@ -2242,7 +2133,7 @@ fn, effectOptions) {
2242
2133
  }
2243
2134
  finally {
2244
2135
  hasReacted = true;
2245
- options.leave(fn);
2136
+ decorator.options.leave(fn);
2246
2137
  }
2247
2138
  // Create cleanup function for next run
2248
2139
  cleanup = () => {
@@ -2306,11 +2197,28 @@ fn, effectOptions) {
2306
2197
  cleanupEffectFromGraph(runEffect);
2307
2198
  fr.unregister(stopEffect);
2308
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
+ }
2309
2217
  if (isRootEffect) {
2310
- const callIfCollected = () => stopEffect();
2218
+ const callIfCollected = augmentedRv(() => stopEffect());
2311
2219
  fr.register(callIfCollected, () => {
2312
2220
  stopEffect();
2313
- options.garbageCollected(fn);
2221
+ decorator.options.garbageCollected(fn);
2314
2222
  }, stopEffect);
2315
2223
  return callIfCollected;
2316
2224
  }
@@ -2320,14 +2228,14 @@ fn, effectOptions) {
2320
2228
  children = new Set();
2321
2229
  effectChildren.set(parent, children);
2322
2230
  }
2323
- const subEffectCleanup = () => {
2231
+ const subEffectCleanup = augmentedRv(() => {
2324
2232
  children.delete(subEffectCleanup);
2325
2233
  if (children.size === 0) {
2326
2234
  effectChildren.delete(parent);
2327
2235
  }
2328
2236
  // Execute this child effect cleanup (which triggers its own mainCleanup)
2329
2237
  stopEffect();
2330
- };
2238
+ });
2331
2239
  children.add(subEffectCleanup);
2332
2240
  return subEffectCleanup;
2333
2241
  }
@@ -2488,10 +2396,14 @@ function collectEffects(obj, evolution, effects, objectWatchers, ...keyChains) {
2488
2396
  for (const effect of deps) {
2489
2397
  const runningChain = isRunning(effect);
2490
2398
  if (runningChain) {
2491
- options.skipRunningEffect(effect, runningChain);
2399
+ decorator.options.skipRunningEffect(effect, runningChain);
2492
2400
  continue;
2493
2401
  }
2494
- effects.add(effect);
2402
+ if (!effects.has(effect)) {
2403
+ effects.add(effect);
2404
+ if (!hasBatched(effect))
2405
+ recordActivation(effect, obj, evolution, key);
2406
+ }
2495
2407
  const trackers = effectTrackers.get(effect);
2496
2408
  recordTriggerLink(sourceEffect, effect, obj, key, evolution);
2497
2409
  if (trackers) {
@@ -2525,10 +2437,10 @@ function touched(obj, evolution, props) {
2525
2437
  // Note: we have to collect effects to remove duplicates in the specific case when no batch is running
2526
2438
  const effects = new Set();
2527
2439
  if (props)
2528
- collectEffects(obj, evolution, effects, objectWatchers, [allProps], props);
2440
+ collectEffects(obj, evolution, effects, objectWatchers, [decorator.allProps], props);
2529
2441
  else
2530
2442
  collectEffects(obj, evolution, effects, objectWatchers, objectWatchers.keys());
2531
- options.touched(obj, evolution, props, effects);
2443
+ decorator.options.touched(obj, evolution, props, effects);
2532
2444
  batch(Array.from(effects));
2533
2445
  }
2534
2446
  // Bubble up changes if this object has deep watchers
@@ -2555,10 +2467,11 @@ function touchedOpaque(obj, evolution, prop) {
2555
2467
  continue;
2556
2468
  const runningChain = isRunning(effect);
2557
2469
  if (runningChain) {
2558
- options.skipRunningEffect(effect, runningChain);
2470
+ decorator.options.skipRunningEffect(effect, runningChain);
2559
2471
  continue;
2560
2472
  }
2561
2473
  effects.add(effect);
2474
+ recordActivation(effect, obj, evolution, prop);
2562
2475
  const trackers = effectTrackers.get(effect);
2563
2476
  recordTriggerLink(sourceEffect, effect, obj, prop, evolution);
2564
2477
  if (trackers) {
@@ -2568,7 +2481,7 @@ function touchedOpaque(obj, evolution, prop) {
2568
2481
  }
2569
2482
  }
2570
2483
  if (effects.size > 0) {
2571
- options.touched(obj, evolution, [prop], effects);
2484
+ decorator.options.touched(obj, evolution, [prop], effects);
2572
2485
  batch(Array.from(effects));
2573
2486
  }
2574
2487
  }
@@ -2579,7 +2492,7 @@ const absent = Symbol('absent');
2579
2492
  function markNonReactive(...obj) {
2580
2493
  for (const o of obj) {
2581
2494
  try {
2582
- Object.defineProperty(o, nonReactiveMark, {
2495
+ Object.defineProperty(o, decorator.nonReactiveMark, {
2583
2496
  value: true,
2584
2497
  writable: false,
2585
2498
  enumerable: false,
@@ -2587,7 +2500,7 @@ function markNonReactive(...obj) {
2587
2500
  });
2588
2501
  }
2589
2502
  catch { }
2590
- if (!(nonReactiveMark in o))
2503
+ if (!(decorator.nonReactiveMark in o))
2591
2504
  nonReactiveObjects.add(o);
2592
2505
  }
2593
2506
  return obj[0];
@@ -2595,7 +2508,7 @@ function markNonReactive(...obj) {
2595
2508
  function nonReactiveClass(...cls) {
2596
2509
  for (const c of cls)
2597
2510
  if (c)
2598
- c.prototype[nonReactiveMark] = true;
2511
+ c.prototype[decorator.nonReactiveMark] = true;
2599
2512
  return cls[0];
2600
2513
  }
2601
2514
  function isNonReactive(obj) {
@@ -2603,7 +2516,7 @@ function isNonReactive(obj) {
2603
2516
  return true;
2604
2517
  if (nonReactiveObjects.has(obj))
2605
2518
  return true;
2606
- if (obj[nonReactiveMark])
2519
+ if (obj[decorator.nonReactiveMark])
2607
2520
  return true;
2608
2521
  for (const fn of immutables)
2609
2522
  if (fn(obj))
@@ -2611,7 +2524,7 @@ function isNonReactive(obj) {
2611
2524
  return false;
2612
2525
  }
2613
2526
  function registerNativeReactivity(originalClass, reactiveClass) {
2614
- originalClass.prototype[nativeReactive] = reactiveClass;
2527
+ originalClass.prototype[decorator.nativeReactive] = reactiveClass;
2615
2528
  nonReactiveClass(reactiveClass);
2616
2529
  }
2617
2530
  nonReactiveClass(Date, RegExp, Error, Promise, Function);
@@ -2657,7 +2570,7 @@ function shouldRecurseTouch(oldValue, newValue) {
2657
2570
  */
2658
2571
  function notifyPropertyChange(targetObj, prop, oldValue, newValue, hadProperty) {
2659
2572
  const evolution = { type: hadProperty ? 'set' : 'add', prop };
2660
- if (options.recursiveTouching &&
2573
+ if (decorator.options.recursiveTouching &&
2661
2574
  oldValue !== undefined &&
2662
2575
  shouldRecurseTouch(oldValue, newValue)) {
2663
2576
  const unwrappedObj = unwrap(targetObj);
@@ -2794,7 +2707,7 @@ function dispatchNotifications(notifications) {
2794
2707
  const originWatchers = watchers.get(origin.obj);
2795
2708
  if (originWatchers) {
2796
2709
  const originEffects = new Set();
2797
- 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]);
2798
2711
  for (const effect of originEffects)
2799
2712
  allowedEffects.add(effect);
2800
2713
  }
@@ -2812,7 +2725,7 @@ function dispatchNotifications(notifications) {
2812
2725
  const propsArray = [prop];
2813
2726
  if (objectWatchers) {
2814
2727
  currentEffects = new Set();
2815
- collectEffects(obj, evolution, currentEffects, objectWatchers, [allProps], propsArray);
2728
+ collectEffects(obj, evolution, currentEffects, objectWatchers, [decorator.allProps], propsArray);
2816
2729
  // Filter effects by ancestor chain if origin exists
2817
2730
  // Include effects that either directly depend on origin or have an ancestor that does
2818
2731
  if (origin && allowedEffects) {
@@ -2828,7 +2741,7 @@ function dispatchNotifications(notifications) {
2828
2741
  for (const effect of currentEffects)
2829
2742
  combinedEffects.add(effect);
2830
2743
  }
2831
- options.touched(obj, evolution, propsArray, currentEffects);
2744
+ decorator.options.touched(obj, evolution, propsArray, currentEffects);
2832
2745
  if (objectsWithDeepWatchers.has(obj))
2833
2746
  bubbleUpChange(obj);
2834
2747
  }
@@ -2840,18 +2753,18 @@ const hasReentry = [];
2840
2753
  const reactiveHandlers = {
2841
2754
  [Symbol.toStringTag]: 'MutTs Reactive',
2842
2755
  get(obj, prop, receiver) {
2843
- if (prop === nonReactiveMark)
2756
+ if (prop === decorator.nonReactiveMark)
2844
2757
  return false;
2845
2758
  const unwrappedObj = unwrap(obj);
2846
2759
  // Check if this property is marked as unreactive
2847
- if (unwrappedObj[unreactiveProperties]?.has(prop) || typeof prop === 'symbol')
2760
+ if (unwrappedObj[decorator.unreactiveProperties]?.has(prop) || typeof prop === 'symbol')
2848
2761
  return decorator.ReflectGet(obj, prop, receiver);
2849
2762
  // Special-case: array wrappers use prototype forwarding + numeric accessors.
2850
2763
  // With options.instanceMembers=true, inherited reads are normally not tracked, which breaks
2851
2764
  // reactivity for array indices/length (they appear inherited on the proxy).
2852
- const isArrayCase = prototypeForwarding in obj &&
2765
+ const isArrayCase = decorator.prototypeForwarding in obj &&
2853
2766
  // biome-ignore lint/suspicious/useIsArray: This is the whole point here
2854
- obj[prototypeForwarding] instanceof Array &&
2767
+ obj[decorator.prototypeForwarding] instanceof Array &&
2855
2768
  typeof prop === 'string' &&
2856
2769
  (prop === 'length' || !Number.isNaN(Number(prop)));
2857
2770
  if (isArrayCase) {
@@ -2863,16 +2776,16 @@ const reactiveHandlers = {
2863
2776
  const isInheritedAccess = hasProp && !isOwnProp;
2864
2777
  // For accessor properties, check the unwrapped object to see if it's an accessor
2865
2778
  // This ensures ignoreAccessors works correctly even after operations like Object.setPrototypeOf
2866
- const shouldIgnoreAccessor = options.ignoreAccessors &&
2779
+ const shouldIgnoreAccessor = decorator.options.ignoreAccessors &&
2867
2780
  isOwnProp &&
2868
2781
  (decorator.isOwnAccessor(receiver, prop) || decorator.isOwnAccessor(unwrappedObj, prop));
2869
2782
  // Depend if...
2870
2783
  if (!hasProp ||
2871
- (!(options.instanceMembers && isInheritedAccess && obj instanceof Object) &&
2784
+ (!(decorator.options.instanceMembers && isInheritedAccess && obj instanceof Object) &&
2872
2785
  !shouldIgnoreAccessor))
2873
2786
  dependant(obj, prop);
2874
2787
  // Watch the whole prototype chain when requested or for null-proto objects
2875
- if (isInheritedAccess && (!options.instanceMembers || !(obj instanceof Object))) {
2788
+ if (isInheritedAccess && (!decorator.options.instanceMembers || !(obj instanceof Object))) {
2876
2789
  let current = reactiveObject(Object.getPrototypeOf(obj));
2877
2790
  while (current && current !== Object.prototype) {
2878
2791
  dependant(current, prop);
@@ -2901,12 +2814,12 @@ const reactiveHandlers = {
2901
2814
  const unwrappedObj = unwrap(obj);
2902
2815
  const unwrappedReceiver = unwrap(receiver);
2903
2816
  // Check if this property is marked as unreactive
2904
- if (unwrappedObj[unreactiveProperties]?.has(prop) || unwrappedObj !== unwrappedReceiver)
2817
+ if (unwrappedObj[decorator.unreactiveProperties]?.has(prop) || unwrappedObj !== unwrappedReceiver)
2905
2818
  return decorator.ReflectSet(obj, prop, value, receiver);
2906
2819
  // Really specific case for when Array is forwarder, in order to let it manage the reactivity
2907
- const isArrayCase = prototypeForwarding in obj &&
2820
+ const isArrayCase = decorator.prototypeForwarding in obj &&
2908
2821
  // biome-ignore lint/suspicious/useIsArray: This is the whole point here
2909
- obj[prototypeForwarding] instanceof Array &&
2822
+ obj[decorator.prototypeForwarding] instanceof Array &&
2910
2823
  (!Number.isNaN(Number(prop)) || prop === 'length');
2911
2824
  const newValue = unwrap(value);
2912
2825
  if (isArrayCase) {
@@ -2921,13 +2834,13 @@ const reactiveHandlers = {
2921
2834
  const receiverDesc = Object.getOwnPropertyDescriptor(unwrappedReceiver, prop);
2922
2835
  const targetDesc = Object.getOwnPropertyDescriptor(unwrappedObj, prop);
2923
2836
  const desc = receiverDesc || targetDesc;
2924
- // If it's a getter-only accessor (has getter but no setter), read without tracking
2925
- // 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
2926
2839
  if (desc?.get && !desc?.set) {
2927
- oldVal = withEffect(undefined, () => Reflect.get(unwrappedObj, prop, unwrappedReceiver));
2840
+ oldVal = withEffect(undefined, () => Reflect.get(unwrappedObj, prop, receiver));
2928
2841
  }
2929
2842
  else {
2930
- oldVal = Reflect.get(unwrappedObj, prop, unwrappedReceiver);
2843
+ oldVal = withEffect(undefined, () => Reflect.get(unwrappedObj, prop, receiver));
2931
2844
  }
2932
2845
  }
2933
2846
  if (objectsWithDeepWatchers.has(obj)) {
@@ -2950,8 +2863,8 @@ const reactiveHandlers = {
2950
2863
  },
2951
2864
  has(obj, prop) {
2952
2865
  if (hasReentry.includes(obj))
2953
- throw new ReactiveError(`[reactive] Circular dependency detected in 'has' check for property '${String(prop)}'`, {
2954
- code: ReactiveErrorCode.CycleDetected,
2866
+ throw new decorator.ReactiveError(`[reactive] Circular dependency detected in 'has' check for property '${String(prop)}'`, {
2867
+ code: decorator.ReactiveErrorCode.CycleDetected,
2955
2868
  cycle: [], // We don't have the full cycle here, but we know it involves obj
2956
2869
  });
2957
2870
  hasReentry.push(obj);
@@ -2977,18 +2890,18 @@ const reactiveHandlers = {
2977
2890
  return true;
2978
2891
  },
2979
2892
  getPrototypeOf(obj) {
2980
- if (prototypeForwarding in obj)
2981
- return obj[prototypeForwarding];
2893
+ if (decorator.prototypeForwarding in obj)
2894
+ return obj[decorator.prototypeForwarding];
2982
2895
  return Object.getPrototypeOf(obj);
2983
2896
  },
2984
2897
  setPrototypeOf(obj, proto) {
2985
- if (prototypeForwarding in obj)
2898
+ if (decorator.prototypeForwarding in obj)
2986
2899
  return false;
2987
2900
  Object.setPrototypeOf(obj, proto);
2988
2901
  return true;
2989
2902
  },
2990
2903
  ownKeys(obj) {
2991
- dependant(obj, allProps);
2904
+ dependant(obj, decorator.allProps);
2992
2905
  return Reflect.ownKeys(obj);
2993
2906
  },
2994
2907
  };
@@ -3024,8 +2937,8 @@ function reactiveObject(anyTarget) {
3024
2937
  const existing = getExistingProxy(target);
3025
2938
  if (existing !== undefined)
3026
2939
  return existing;
3027
- const proxied = nativeReactive in target && !(target instanceof target[nativeReactive])
3028
- ? new target[nativeReactive](target)
2940
+ const proxied = decorator.nativeReactive in target && !(target instanceof target[decorator.nativeReactive])
2941
+ ? new target[decorator.nativeReactive](target)
3029
2942
  : target;
3030
2943
  if (proxied !== target)
3031
2944
  trackProxyObject(proxied, target);
@@ -3048,7 +2961,7 @@ const reactive = decorator.decorator({
3048
2961
  constructor(...args) {
3049
2962
  super(...args);
3050
2963
  if (new.target !== Reactive && !reactiveClasses.has(new.target))
3051
- 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.
3052
2965
  @reactive decorator must be applied to the leaf class OR classes have to extend ReactiveBase.`);
3053
2966
  // biome-ignore lint/correctness/noConstructorReturn: This is the whole point here
3054
2967
  return reactive(this);
@@ -3105,7 +3018,7 @@ function deepWatch(target, callback, { immediate = false } = {}) {
3105
3018
  const visited = new WeakSet();
3106
3019
  function traverseAndTrack(obj, depth = 0) {
3107
3020
  // Prevent infinite recursion and excessive depth
3108
- if (!obj || visited.has(obj) || !isObject(obj) || depth > options.maxDeepWatchDepth)
3021
+ if (!obj || visited.has(obj) || !isObject(obj) || depth > decorator.options.maxDeepWatchDepth)
3109
3022
  return;
3110
3023
  // Do not traverse into unreactive objects
3111
3024
  if (isNonReactive(obj))
@@ -3217,12 +3130,12 @@ function watchObject(value, changed, { immediate = false, deep = false } = {}) {
3217
3130
  const myParentEffect = getActiveEffect();
3218
3131
  if (deep)
3219
3132
  return deepWatch(value, changed, { immediate });
3220
- return effect(markWithRoot(function watchObjectEffect() {
3133
+ return effect(function watchObjectEffect() {
3221
3134
  dependant(value);
3222
3135
  if (immediate)
3223
3136
  withEffect(myParentEffect, () => changed(value));
3224
3137
  immediate = true;
3225
- }, changed));
3138
+ });
3226
3139
  }
3227
3140
  function watchCallBack(value, changed, { immediate = false, deep = false } = {}) {
3228
3141
  const myParentEffect = getActiveEffect();
@@ -3231,7 +3144,7 @@ function watchCallBack(value, changed, { immediate = false, deep = false } = {})
3231
3144
  const cbCleanup = effect(markWithRoot(function watchCallBackEffect(access) {
3232
3145
  const newValue = value(access);
3233
3146
  if (oldValue !== newValue)
3234
- withEffect(myParentEffect, markWithRoot(() => {
3147
+ withEffect(myParentEffect, () => {
3235
3148
  if (oldValue === unsetYet) {
3236
3149
  if (immediate)
3237
3150
  changed(newValue);
@@ -3242,9 +3155,9 @@ function watchCallBack(value, changed, { immediate = false, deep = false } = {})
3242
3155
  if (deep) {
3243
3156
  if (deepCleanup)
3244
3157
  deepCleanup();
3245
- deepCleanup = deepWatch(newValue, markWithRoot((value) => changed(value, value), changed));
3158
+ deepCleanup = deepWatch(newValue, (value) => changed(value, value));
3246
3159
  }
3247
- }, changed));
3160
+ });
3248
3161
  }, value));
3249
3162
  return () => {
3250
3163
  cbCleanup();
@@ -3263,7 +3176,7 @@ function deepNonReactive(obj) {
3263
3176
  if (isNonReactive(obj))
3264
3177
  return obj;
3265
3178
  try {
3266
- Object.defineProperty(obj, nonReactiveMark, {
3179
+ Object.defineProperty(obj, decorator.nonReactiveMark, {
3267
3180
  value: true,
3268
3181
  writable: false,
3269
3182
  enumerable: false,
@@ -3271,7 +3184,7 @@ function deepNonReactive(obj) {
3271
3184
  });
3272
3185
  }
3273
3186
  catch { }
3274
- if (!(nonReactiveMark in obj))
3187
+ if (!(decorator.nonReactiveMark in obj))
3275
3188
  nonReactiveObjects.add(obj);
3276
3189
  //for (const key in obj) deepNonReactive(obj[key])
3277
3190
  return obj;
@@ -3281,11 +3194,11 @@ function unreactiveApplication(arg1, ...args) {
3281
3194
  ? deepNonReactive(arg1)
3282
3195
  : ((original) => {
3283
3196
  // Copy the parent's unreactive properties if they exist
3284
- original.prototype[unreactiveProperties] = new Set(original.prototype[unreactiveProperties] || []);
3197
+ original.prototype[decorator.unreactiveProperties] = new Set(original.prototype[decorator.unreactiveProperties] || []);
3285
3198
  // Add all arguments (including the first one)
3286
- original.prototype[unreactiveProperties].add(arg1);
3199
+ original.prototype[decorator.unreactiveProperties].add(arg1);
3287
3200
  for (const arg of args)
3288
- original.prototype[unreactiveProperties].add(arg);
3201
+ original.prototype[decorator.unreactiveProperties].add(arg);
3289
3202
  return original; // Return the class
3290
3203
  });
3291
3204
  }
@@ -3317,9 +3230,9 @@ function cleanedBy(obj, cleanupFn) {
3317
3230
  */
3318
3231
  function derived(compute) {
3319
3232
  const rv = { value: undefined };
3320
- return cleanedBy(rv, untracked(() => effect(markWithRoot(function derivedEffect(access) {
3233
+ return cleanedBy(rv, untracked(() => effect(function derivedEffect(access) {
3321
3234
  rv.value = compute(access);
3322
- }, compute))));
3235
+ })));
3323
3236
  }
3324
3237
 
3325
3238
  /**
@@ -3347,11 +3260,10 @@ function* makeReactiveEntriesIterator(iterator) {
3347
3260
  const native$2 = Symbol('native');
3348
3261
  const isArray = Array.isArray;
3349
3262
  Array.isArray = ((value) => isArray(value) ||
3350
- // biome-ignore lint/suspicious/useIsArray: We are defining it
3351
3263
  (value &&
3352
3264
  typeof value === 'object' &&
3353
- prototypeForwarding in value &&
3354
- Array.isArray(value[prototypeForwarding])));
3265
+ decorator.prototypeForwarding in value &&
3266
+ Array.isArray(value[decorator.prototypeForwarding])));
3355
3267
  class ReactiveBaseArray {
3356
3268
  // Safe array access with negative indices
3357
3269
  at(index) {
@@ -3564,7 +3476,7 @@ class ReactiveArray extends indexable.Indexable(ReactiveBaseArray, {
3564
3476
  Object.defineProperties(this, {
3565
3477
  // We have to make it double, as [native] must be `unique symbol` - impossible through import
3566
3478
  [native$2]: { value: original },
3567
- [prototypeForwarding]: { value: original },
3479
+ [decorator.prototypeForwarding]: { value: original },
3568
3480
  });
3569
3481
  }
3570
3482
  push(...items) {
@@ -3695,7 +3607,7 @@ class ReactiveReadOnlyArrayClass extends indexable.Indexable(ReactiveBaseArray,
3695
3607
  Object.defineProperties(this, {
3696
3608
  // We have to make it double, as [native] must be `unique symbol` - impossible through import
3697
3609
  [native$2]: { value: original },
3698
- [prototypeForwarding]: { value: original },
3610
+ [decorator.prototypeForwarding]: { value: original },
3699
3611
  });
3700
3612
  }
3701
3613
  push(..._items) {
@@ -3776,6 +3688,7 @@ function reduced(inputs, compute) {
3776
3688
  }
3777
3689
 
3778
3690
  const memoizedRegistry = new WeakMap();
3691
+ const wrapperRegistry = new WeakMap();
3779
3692
  function getBranch(tree, key) {
3780
3693
  tree.branches ?? (tree.branches = new WeakMap());
3781
3694
  let branch = tree.branches.get(key);
@@ -3796,15 +3709,30 @@ function memoizeFunction(fn) {
3796
3709
  if (localArgs.some((arg) => !(arg && ['object', 'symbol', 'function'].includes(typeof arg))))
3797
3710
  throw new Error('memoize expects non-null object arguments');
3798
3711
  let node = cacheRoot;
3712
+ // Note: decorators add `this` as first argument
3799
3713
  for (const arg of localArgs) {
3800
3714
  node = getBranch(node, arg);
3801
3715
  }
3802
3716
  dependant(node, 'memoize');
3803
- 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
+ }
3804
3731
  return node.result;
3732
+ }
3805
3733
  // Create memoize internal effect to track dependencies and invalidate cache
3806
3734
  // Use untracked to prevent the effect creation from being affected by parent effects
3807
- node.cleanup = root(() => effect(markWithRoot(() => {
3735
+ node.cleanup = root(() => effect(() => {
3808
3736
  // Execute the function and track its dependencies
3809
3737
  // The function execution will automatically track dependencies on reactive objects
3810
3738
  node.result = fn(...localArgs);
@@ -3812,8 +3740,27 @@ function memoizeFunction(fn) {
3812
3740
  // When dependencies change, clear the cache and notify consumers
3813
3741
  delete node.result;
3814
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
+ }
3815
3749
  };
3816
- }, 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
+ }
3817
3764
  return node.result;
3818
3765
  }, fn);
3819
3766
  memoizedRegistry.set(fnRoot, memoized);
@@ -3821,19 +3768,37 @@ function memoizeFunction(fn) {
3821
3768
  return memoized;
3822
3769
  }
3823
3770
  const memoize = decorator.decorator({
3824
- getter(original, propertyKey) {
3825
- const memoized = memoizeFunction(markWithRoot(decorator.renamed((that) => {
3826
- return original.call(that);
3827
- }, `${String(this.constructor.name)}.${String(propertyKey)}`), original));
3771
+ getter(original, target, propertyKey) {
3828
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);
3829
3785
  return memoized(this);
3830
3786
  };
3831
3787
  },
3832
- method(original, name) {
3833
- const memoized = memoizeFunction(markWithRoot(decorator.renamed((that, ...args) => {
3834
- return original.call(that, ...args);
3835
- }, `${String(this.constructor.name)}.${String(name)}`), original));
3788
+ method(original, target, name) {
3836
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);
3837
3802
  return memoized(this, ...args);
3838
3803
  };
3839
3804
  },
@@ -3895,7 +3860,7 @@ let RegisterClass = (() => {
3895
3860
  _tslib.__classPrivateFieldSet(this, _RegisterClass_keys, reactive([]), "f");
3896
3861
  _tslib.__classPrivateFieldSet(this, _RegisterClass_values, reactive(new Map()), "f");
3897
3862
  Object.defineProperties(this, {
3898
- [prototypeForwarding]: { value: _tslib.__classPrivateFieldGet(this, _RegisterClass_keys, "f") },
3863
+ [decorator.prototypeForwarding]: { value: _tslib.__classPrivateFieldGet(this, _RegisterClass_keys, "f") },
3899
3864
  });
3900
3865
  if (initial)
3901
3866
  this.push(...initial);
@@ -4243,6 +4208,17 @@ function register(keyFn, initial) {
4243
4208
  return new RegisterClass(keyFn, initial);
4244
4209
  }
4245
4210
 
4211
+ /**
4212
+ * Maps projection effects (item effects) to their projection context
4213
+ */
4214
+ const effectProjectionMetadata = new WeakMap();
4215
+ /**
4216
+ * Returns the projection context of the currently running effect, if any.
4217
+ */
4218
+ function getActiveProjection() {
4219
+ const active = getActiveEffect();
4220
+ return active ? effectProjectionMetadata.get(active) : undefined;
4221
+ }
4246
4222
  function defineAccessValue(access) {
4247
4223
  Object.defineProperty(access, 'value', {
4248
4224
  get: access.get,
@@ -4251,7 +4227,15 @@ function defineAccessValue(access) {
4251
4227
  enumerable: true,
4252
4228
  });
4253
4229
  }
4254
- function makeCleanup(target, effectMap, onDispose) {
4230
+ function makeCleanup(target, effectMap, onDispose, metadata) {
4231
+ if (metadata) {
4232
+ Object.defineProperty(target, decorator.projectionInfo, {
4233
+ value: metadata,
4234
+ writable: false,
4235
+ enumerable: false,
4236
+ configurable: true,
4237
+ });
4238
+ }
4255
4239
  return cleanedBy(target, () => {
4256
4240
  onDispose();
4257
4241
  for (const stop of effectMap.values())
@@ -4274,6 +4258,8 @@ function projectArray(source, apply) {
4274
4258
  Reflect.deleteProperty(target, index);
4275
4259
  }
4276
4260
  }
4261
+ const parent = getActiveProjection();
4262
+ const depth = parent ? parent.depth + 1 : 0;
4277
4263
  const cleanupLength = effect(function projectArrayLengthEffect({ ascend }) {
4278
4264
  const length = observedSource.length;
4279
4265
  normalizeTargetLength(length);
@@ -4296,6 +4282,14 @@ function projectArray(source, apply) {
4296
4282
  const produced = apply(accessBase, target);
4297
4283
  target[index] = produced;
4298
4284
  });
4285
+ setEffectName(stop, `project[${depth}]:${index}`);
4286
+ effectProjectionMetadata.set(stop, {
4287
+ source: observedSource,
4288
+ key: index,
4289
+ target,
4290
+ depth,
4291
+ parent,
4292
+ });
4299
4293
  indexEffects.set(i, stop);
4300
4294
  });
4301
4295
  }
@@ -4303,7 +4297,13 @@ function projectArray(source, apply) {
4303
4297
  if (index >= length)
4304
4298
  disposeIndex(index);
4305
4299
  });
4306
- return makeCleanup(target, indexEffects, () => cleanupLength());
4300
+ return makeCleanup(target, indexEffects, () => cleanupLength(), {
4301
+ source: observedSource,
4302
+ target,
4303
+ apply,
4304
+ depth,
4305
+ parent,
4306
+ });
4307
4307
  }
4308
4308
  function projectRegister(source, apply) {
4309
4309
  const observedSource = reactive(source);
@@ -4318,6 +4318,8 @@ function projectRegister(source, apply) {
4318
4318
  target.delete(key);
4319
4319
  }
4320
4320
  }
4321
+ const parent = getActiveProjection();
4322
+ const depth = parent ? parent.depth + 1 : 0;
4321
4323
  const cleanupKeys = effect(function projectRegisterEffect({ ascend }) {
4322
4324
  const keys = new Set();
4323
4325
  for (const key of observedSource.mapKeys())
@@ -4342,6 +4344,14 @@ function projectRegister(source, apply) {
4342
4344
  const produced = apply(accessBase, target);
4343
4345
  target.set(key, produced);
4344
4346
  });
4347
+ setEffectName(stop, `project[${depth}]:${String(key)}`);
4348
+ effectProjectionMetadata.set(stop, {
4349
+ source: observedSource,
4350
+ key,
4351
+ target,
4352
+ depth,
4353
+ parent,
4354
+ });
4345
4355
  keyEffects.set(key, stop);
4346
4356
  });
4347
4357
  }
@@ -4349,7 +4359,13 @@ function projectRegister(source, apply) {
4349
4359
  if (!keys.has(key))
4350
4360
  disposeKey(key);
4351
4361
  });
4352
- return makeCleanup(target, keyEffects, () => cleanupKeys());
4362
+ return makeCleanup(target, keyEffects, () => cleanupKeys(), {
4363
+ source: observedSource,
4364
+ target,
4365
+ apply,
4366
+ depth,
4367
+ parent,
4368
+ });
4353
4369
  }
4354
4370
  function projectRecord(source, apply) {
4355
4371
  const observedSource = reactive(source);
@@ -4363,6 +4379,8 @@ function projectRecord(source, apply) {
4363
4379
  Reflect.deleteProperty(target, key);
4364
4380
  }
4365
4381
  }
4382
+ const parent = getActiveProjection();
4383
+ const depth = parent ? parent.depth + 1 : 0;
4366
4384
  const cleanupKeys = effect(function projectRecordEffect({ ascend }) {
4367
4385
  const keys = new Set();
4368
4386
  for (const key in observedSource)
@@ -4388,6 +4406,14 @@ function projectRecord(source, apply) {
4388
4406
  const produced = apply(accessBase, target);
4389
4407
  target[sourceKey] = produced;
4390
4408
  });
4409
+ setEffectName(stop, `project[${depth}]:${String(key)}`);
4410
+ effectProjectionMetadata.set(stop, {
4411
+ source: observedSource,
4412
+ key,
4413
+ target,
4414
+ depth,
4415
+ parent,
4416
+ });
4391
4417
  keyEffects.set(key, stop);
4392
4418
  });
4393
4419
  }
@@ -4395,7 +4421,13 @@ function projectRecord(source, apply) {
4395
4421
  if (!keys.has(key))
4396
4422
  disposeKey(key);
4397
4423
  });
4398
- return makeCleanup(target, keyEffects, () => cleanupKeys());
4424
+ return makeCleanup(target, keyEffects, () => cleanupKeys(), {
4425
+ source: observedSource,
4426
+ target,
4427
+ apply,
4428
+ depth,
4429
+ parent,
4430
+ });
4399
4431
  }
4400
4432
  function projectMap(source, apply) {
4401
4433
  const observedSource = reactive(source);
@@ -4410,6 +4442,8 @@ function projectMap(source, apply) {
4410
4442
  target.delete(key);
4411
4443
  }
4412
4444
  }
4445
+ const parent = getActiveProjection();
4446
+ const depth = parent ? parent.depth + 1 : 0;
4413
4447
  const cleanupKeys = effect(function projectMapEffect({ ascend }) {
4414
4448
  const keys = new Set();
4415
4449
  for (const key of observedSource.keys())
@@ -4434,6 +4468,14 @@ function projectMap(source, apply) {
4434
4468
  const produced = apply(accessBase, target);
4435
4469
  target.set(key, produced);
4436
4470
  });
4471
+ setEffectName(stop, `project[${depth}]:${String(key)}`);
4472
+ effectProjectionMetadata.set(stop, {
4473
+ source: observedSource,
4474
+ key,
4475
+ target,
4476
+ depth,
4477
+ parent,
4478
+ });
4437
4479
  keyEffects.set(key, stop);
4438
4480
  });
4439
4481
  }
@@ -4441,7 +4483,13 @@ function projectMap(source, apply) {
4441
4483
  if (!keys.has(key))
4442
4484
  disposeKey(key);
4443
4485
  });
4444
- return makeCleanup(target, keyEffects, () => cleanupKeys());
4486
+ return makeCleanup(target, keyEffects, () => cleanupKeys(), {
4487
+ source: observedSource,
4488
+ target,
4489
+ apply,
4490
+ depth,
4491
+ parent,
4492
+ });
4445
4493
  }
4446
4494
  function projectCore(source, apply) {
4447
4495
  if (Array.isArray(source))
@@ -4588,8 +4636,8 @@ class ReactiveWeakMap {
4588
4636
  constructor(original) {
4589
4637
  Object.defineProperties(this, {
4590
4638
  [native$1]: { value: original },
4591
- [prototypeForwarding]: { value: original },
4592
- content: { value: Symbol('content') },
4639
+ [decorator.prototypeForwarding]: { value: original },
4640
+ content: { value: Symbol('WeakMapContent') },
4593
4641
  [Symbol.toStringTag]: { value: 'ReactiveWeakMap' },
4594
4642
  });
4595
4643
  }
@@ -4628,8 +4676,8 @@ class ReactiveMap {
4628
4676
  constructor(original) {
4629
4677
  Object.defineProperties(this, {
4630
4678
  [native$1]: { value: original },
4631
- [prototypeForwarding]: { value: original },
4632
- content: { value: Symbol('content') },
4679
+ [decorator.prototypeForwarding]: { value: original },
4680
+ content: { value: Symbol('MapContent') },
4633
4681
  [Symbol.toStringTag]: { value: 'ReactiveMap' },
4634
4682
  });
4635
4683
  }
@@ -4723,8 +4771,8 @@ class ReactiveWeakSet {
4723
4771
  constructor(original) {
4724
4772
  Object.defineProperties(this, {
4725
4773
  [native]: { value: original },
4726
- [prototypeForwarding]: { value: original },
4727
- content: { value: Symbol('content') },
4774
+ [decorator.prototypeForwarding]: { value: original },
4775
+ content: { value: Symbol('WeakSetContent') },
4728
4776
  [Symbol.toStringTag]: { value: 'ReactiveWeakSet' },
4729
4777
  });
4730
4778
  }
@@ -4758,8 +4806,8 @@ class ReactiveSet {
4758
4806
  constructor(original) {
4759
4807
  Object.defineProperties(this, {
4760
4808
  [native]: { value: original },
4761
- [prototypeForwarding]: { value: original },
4762
- content: { value: Symbol('content') },
4809
+ [decorator.prototypeForwarding]: { value: original },
4810
+ content: { value: Symbol('SetContent') },
4763
4811
  [Symbol.toStringTag]: { value: 'ReactiveSet' },
4764
4812
  });
4765
4813
  }
@@ -4858,7 +4906,6 @@ const profileInfo = {
4858
4906
  exports.IterableWeakMap = IterableWeakMap;
4859
4907
  exports.IterableWeakSet = IterableWeakSet;
4860
4908
  exports.ReactiveBase = ReactiveBase;
4861
- exports.ReactiveError = ReactiveError;
4862
4909
  exports.ReadOnlyError = ReadOnlyError;
4863
4910
  exports.Register = Register;
4864
4911
  exports.addBatchCleanup = addBatchCleanup;
@@ -4873,7 +4920,9 @@ exports.defer = defer;
4873
4920
  exports.derived = derived;
4874
4921
  exports.effect = effect;
4875
4922
  exports.enableDevTools = enableDevTools;
4923
+ exports.getActivationLog = getActivationLog;
4876
4924
  exports.getActiveEffect = getActiveEffect;
4925
+ exports.getActiveProjection = getActiveProjection;
4877
4926
  exports.getState = getState;
4878
4927
  exports.immutables = immutables;
4879
4928
  exports.isDevtoolsEnabled = isDevtoolsEnabled;
@@ -4883,7 +4932,6 @@ exports.isZoneEnabled = isZoneEnabled;
4883
4932
  exports.mapped = mapped;
4884
4933
  exports.memoize = memoize;
4885
4934
  exports.mixin = mixin;
4886
- exports.options = options;
4887
4935
  exports.organize = organize;
4888
4936
  exports.organized = organized;
4889
4937
  exports.profileInfo = profileInfo;
@@ -4905,4 +4953,4 @@ exports.unreactive = unreactive;
4905
4953
  exports.untracked = untracked;
4906
4954
  exports.unwrap = unwrap;
4907
4955
  exports.watch = watch;
4908
- //# sourceMappingURL=index-GRBSx0mB.js.map
4956
+ //# sourceMappingURL=index-CDCOjzTy.js.map