mutts 1.0.5 → 1.0.7

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 (114) hide show
  1. package/README.md +2 -1
  2. package/dist/browser.d.ts +2 -0
  3. package/dist/browser.esm.js +70 -0
  4. package/dist/browser.esm.js.map +1 -0
  5. package/dist/browser.js +161 -0
  6. package/dist/browser.js.map +1 -0
  7. package/dist/chunks/{index-Cvxdw6Ax.js → index-BFYK02LG.js} +5377 -4059
  8. package/dist/chunks/index-BFYK02LG.js.map +1 -0
  9. package/dist/chunks/{index-qiWwozOc.esm.js → index-CNR6QRUl.esm.js} +5247 -3963
  10. package/dist/chunks/index-CNR6QRUl.esm.js.map +1 -0
  11. package/dist/mutts.umd.js +1 -1
  12. package/dist/mutts.umd.js.map +1 -1
  13. package/dist/mutts.umd.min.js +1 -1
  14. package/dist/mutts.umd.min.js.map +1 -1
  15. package/dist/node.d.ts +2 -0
  16. package/dist/node.esm.js +45 -0
  17. package/dist/node.esm.js.map +1 -0
  18. package/dist/node.js +136 -0
  19. package/dist/node.js.map +1 -0
  20. package/docs/ai/api-reference.md +0 -2
  21. package/docs/ai/manual.md +14 -95
  22. package/docs/reactive/advanced.md +7 -111
  23. package/docs/reactive/collections.md +0 -125
  24. package/docs/reactive/core.md +27 -24
  25. package/docs/reactive/debugging.md +168 -0
  26. package/docs/reactive/project.md +1 -1
  27. package/docs/reactive/scan.md +78 -0
  28. package/docs/reactive.md +8 -6
  29. package/docs/std-decorators.md +1 -0
  30. package/docs/zone.md +88 -0
  31. package/package.json +47 -65
  32. package/src/async/browser.ts +87 -0
  33. package/src/async/index.ts +8 -0
  34. package/src/async/node.ts +46 -0
  35. package/src/decorator.ts +15 -9
  36. package/src/destroyable.ts +4 -4
  37. package/src/index.ts +54 -0
  38. package/src/indexable.ts +42 -0
  39. package/src/mixins.ts +2 -2
  40. package/src/reactive/array.ts +149 -141
  41. package/src/reactive/buffer.ts +168 -0
  42. package/src/reactive/change.ts +3 -3
  43. package/src/reactive/debug.ts +1 -1
  44. package/src/reactive/deep-touch.ts +1 -1
  45. package/src/reactive/deep-watch.ts +1 -1
  46. package/src/reactive/effect-context.ts +15 -91
  47. package/src/reactive/effects.ts +138 -170
  48. package/src/reactive/index.ts +10 -13
  49. package/src/reactive/interface.ts +20 -33
  50. package/src/reactive/map.ts +48 -61
  51. package/src/reactive/memoize.ts +87 -31
  52. package/src/reactive/project.ts +43 -22
  53. package/src/reactive/proxy.ts +18 -43
  54. package/src/reactive/record.ts +3 -3
  55. package/src/reactive/register.ts +5 -7
  56. package/src/reactive/registry.ts +59 -0
  57. package/src/reactive/set.ts +42 -56
  58. package/src/reactive/tracking.ts +5 -62
  59. package/src/reactive/types.ts +79 -19
  60. package/src/std-decorators.ts +9 -9
  61. package/src/utils.ts +203 -19
  62. package/src/zone.ts +127 -0
  63. package/dist/chunks/_tslib-BgjropY9.js +0 -81
  64. package/dist/chunks/_tslib-BgjropY9.js.map +0 -1
  65. package/dist/chunks/_tslib-Mzh1rNsX.esm.js +0 -75
  66. package/dist/chunks/_tslib-Mzh1rNsX.esm.js.map +0 -1
  67. package/dist/chunks/decorator-DLvrD0UF.js +0 -265
  68. package/dist/chunks/decorator-DLvrD0UF.js.map +0 -1
  69. package/dist/chunks/decorator-DqiszP7i.esm.js +0 -253
  70. package/dist/chunks/decorator-DqiszP7i.esm.js.map +0 -1
  71. package/dist/chunks/index-Cvxdw6Ax.js.map +0 -1
  72. package/dist/chunks/index-qiWwozOc.esm.js.map +0 -1
  73. package/dist/decorator.d.ts +0 -107
  74. package/dist/decorator.esm.js +0 -2
  75. package/dist/decorator.esm.js.map +0 -1
  76. package/dist/decorator.js +0 -11
  77. package/dist/decorator.js.map +0 -1
  78. package/dist/destroyable.d.ts +0 -90
  79. package/dist/destroyable.esm.js +0 -109
  80. package/dist/destroyable.esm.js.map +0 -1
  81. package/dist/destroyable.js +0 -116
  82. package/dist/destroyable.js.map +0 -1
  83. package/dist/eventful.d.ts +0 -20
  84. package/dist/eventful.esm.js +0 -66
  85. package/dist/eventful.esm.js.map +0 -1
  86. package/dist/eventful.js +0 -68
  87. package/dist/eventful.js.map +0 -1
  88. package/dist/index.d.ts +0 -19
  89. package/dist/index.esm.js +0 -8
  90. package/dist/index.esm.js.map +0 -1
  91. package/dist/index.js +0 -95
  92. package/dist/index.js.map +0 -1
  93. package/dist/indexable.d.ts +0 -243
  94. package/dist/indexable.esm.js +0 -285
  95. package/dist/indexable.esm.js.map +0 -1
  96. package/dist/indexable.js +0 -291
  97. package/dist/indexable.js.map +0 -1
  98. package/dist/promiseChain.d.ts +0 -21
  99. package/dist/promiseChain.esm.js +0 -78
  100. package/dist/promiseChain.esm.js.map +0 -1
  101. package/dist/promiseChain.js +0 -80
  102. package/dist/promiseChain.js.map +0 -1
  103. package/dist/reactive.d.ts +0 -885
  104. package/dist/reactive.esm.js +0 -5
  105. package/dist/reactive.esm.js.map +0 -1
  106. package/dist/reactive.js +0 -59
  107. package/dist/reactive.js.map +0 -1
  108. package/dist/std-decorators.d.ts +0 -52
  109. package/dist/std-decorators.esm.js +0 -196
  110. package/dist/std-decorators.esm.js.map +0 -1
  111. package/dist/std-decorators.js +0 -204
  112. package/dist/std-decorators.js.map +0 -1
  113. package/src/reactive/mapped.ts +0 -129
  114. package/src/reactive/zone.ts +0 -208
@@ -2,23 +2,20 @@ import { decorator } from '../decorator'
2
2
  import { IterableWeakSet } from '../iterableWeak'
3
3
  import { getTriggerChain, isDevtoolsEnabled, registerEffectForDebug } from './debug'
4
4
  import {
5
- captureEffectStack,
6
- effectStack,
5
+ effectAggregator,
6
+ effectHistory,
7
7
  getActiveEffect,
8
- withEffect,
9
- withEffectStack,
10
8
  } from './effect-context'
11
9
  import {
12
10
  effectChildren,
13
11
  effectParent,
14
12
  effectToReactiveObjects,
15
13
  getRoot,
16
- getTrackingDisabled,
17
14
  markWithRoot,
18
- setTrackingDisabled,
19
15
  watchers,
20
- } from './tracking'
16
+ } from './registry'
21
17
  import {
18
+ cleanup as cleanupSymbol,
22
19
  type DependencyAccess,
23
20
  type EffectOptions,
24
21
  type Evolution,
@@ -27,8 +24,11 @@ import {
27
24
  ReactiveErrorCode,
28
25
  // type AsyncExecutionMode,
29
26
  type ScopedCallback,
27
+ stopped,
30
28
  } from './types'
31
29
 
30
+ import { unwrap } from './proxy-state'
31
+
32
32
  /**
33
33
  * Finds a cycle in a sequence of functions by looking for the first repetition
34
34
  */
@@ -55,11 +55,8 @@ function formatRoots(roots: Function[], limit = 20): string {
55
55
  return `${start.join(' → ')} ... (${names.length - 15} more) ... ${end.join(' → ')}`
56
56
  }
57
57
 
58
- import { ensureZoneHooked } from './zone'
59
-
60
58
  type EffectTracking = (obj: any, evolution: Evolution, prop: any) => void
61
59
 
62
- export { captureEffectStack, withEffectStack, getActiveEffect, effectStack }
63
60
  export interface ActivationRecord {
64
61
  effect: ScopedCallback
65
62
  obj: any
@@ -207,6 +204,7 @@ function getOrCreateClosure(
207
204
  * @param targetRoot - Root function of the effect being triggered
208
205
  */
209
206
  function addGraphEdge(callerRoot: Function, targetRoot: Function) {
207
+ if (options.cycleHandling === 'none') return
210
208
  // Skip if edge already exists
211
209
  const triggers = effectTriggers.get(callerRoot)
212
210
  if (triggers?.has(targetRoot)) {
@@ -297,6 +295,7 @@ function addGraphEdge(callerRoot: Function, targetRoot: Function) {
297
295
  * @param end - Target node
298
296
  * @param exclude - Node to exclude from the path
299
297
  * @returns true if a path exists without going through the excluded node
298
+ * @todo Can be REALLY costly - optimise or make optional or ...
300
299
  */
301
300
  function hasPathExcluding(start: Function, end: Function, exclude: Function): boolean {
302
301
  if (start === end) return true
@@ -331,6 +330,7 @@ function hasPathExcluding(start: Function, end: Function, exclude: Function): bo
331
330
  * @param effect - The effect being cleaned up
332
331
  */
333
332
  function cleanupEffectFromGraph(effect: ScopedCallback) {
333
+ if (options.cycleHandling === 'none') return
334
334
  const root = getRoot(effect)
335
335
 
336
336
  // Get closures before removing direct edges (needed for propagation)
@@ -453,6 +453,7 @@ const batchCleanups = new Set<ScopedCallback>()
453
453
  * Called once when batch starts or when new effects are added
454
454
  */
455
455
  function computeAllInDegrees(batch: BatchQueue): void {
456
+ if (options.cycleHandling === 'none') return
456
457
  const activeEffect = getActiveEffect()
457
458
  const activeRoot = activeEffect ? getRoot(activeEffect) : null
458
459
 
@@ -592,6 +593,10 @@ function getCyclePathForEdge(callerRoot: Function, targetRoot: Function): Functi
592
593
  * Checks if adding an edge would create a cycle
593
594
  * Uses causesClosure to check if callerRoot is already a cause of targetRoot
594
595
  * Self-loops (callerRoot === targetRoot) are explicitly ignored and return false
596
+ *
597
+ * **Note**: This is the primary optimization benefit of the transitive closure system.
598
+ * It allows detecting cycles in O(1) time before they are executed.
599
+ *
595
600
  * @param callerRoot - Root of the effect that triggers
596
601
  * @param targetRoot - Root of the effect being triggered
597
602
  * @returns true if adding this edge would create a cycle
@@ -621,16 +626,25 @@ function wouldCreateCycle(callerRoot: Function, targetRoot: Function): boolean {
621
626
  * @param immediate - If true, don't create edges in the dependency graph
622
627
  */
623
628
  function addToBatch(effect: ScopedCallback, caller?: ScopedCallback, immediate?: boolean) {
629
+ (effect as any)[cleanupSymbol]?.()
630
+ // If the effect was stopped during cleanup (e.g. lazy memoization), don't add it to the batch
631
+ if ((effect as any)[stopped]) return
632
+
624
633
  if (!batchQueue) return
625
634
 
626
635
  const root = getRoot(effect)
627
636
 
628
637
  // 1. Add to batch first (needed for cycle detection)
638
+ if (options.cycleHandling === 'none' && batchQueue.all.has(root)) {
639
+ // If already present in flat mode, remove it so that the next set puts it at the end
640
+ batchQueue.all.delete(root)
641
+ }
642
+
629
643
  batchQueue.all.set(root, effect)
630
644
 
631
645
  // 2. Add to global graph (if caller exists and not immediate) - USE ROOTS ONLY
632
646
  // When immediate is true, don't create edges - the effect is not considered as a consequence
633
- if (caller && !immediate) {
647
+ if (caller && !immediate && options.cycleHandling !== 'none') {
634
648
  const callerRoot = getRoot(caller)
635
649
 
636
650
  // Check for cycle BEFORE adding edge
@@ -797,14 +811,22 @@ function executeNext(effectuatedRoots: Function[]): any {
797
811
  let nextEffect: ScopedCallback | null = null
798
812
  let nextRoot: Function | null = null
799
813
 
800
- // Find an effect with in-degree 0 (no dependencies in batch that still need execution)
801
- // Using cached in-degrees for O(n) lookup instead of O()
802
- for (const [root, effect] of batchQueue!.all) {
803
- const inDegree = batchQueue!.inDegrees.get(root) ?? 0
804
- if (inDegree === 0) {
805
- nextEffect = effect
806
- nextRoot = root
807
- break
814
+ if (options.cycleHandling === 'none') {
815
+ // In flat mode, we just take the first effect in the queue (FIFO)
816
+ const first = batchQueue!.all.entries().next().value
817
+ if (first) {
818
+ ;[nextRoot, nextEffect] = first
819
+ }
820
+ } else {
821
+ // Find an effect with in-degree 0 (no dependencies in batch that still need execution)
822
+ // Using cached in-degrees for O(n) lookup instead of O(n²)
823
+ for (const [root, effect] of batchQueue!.all) {
824
+ const inDegree = batchQueue!.inDegrees.get(root) ?? 0
825
+ if (inDegree === 0) {
826
+ nextEffect = effect
827
+ nextRoot = root
828
+ break
829
+ }
808
830
  }
809
831
  }
810
832
 
@@ -897,9 +919,8 @@ export function batch(effect: ScopedCallback | ScopedCallback[], immediate?: 'im
897
919
  // Nested batch - add to existing
898
920
  options?.chain(roots, getRoot(getActiveEffect()))
899
921
  const caller = getActiveEffect()
900
- for (let i = 0; i < effect.length; i++) {
922
+ for (let i = 0; i < effect.length; i++)
901
923
  addToBatch(effect[i], caller, immediate === 'immediate')
902
- }
903
924
  if (immediate) {
904
925
  const firstReturn: { value?: any } = {}
905
926
  // Execute immediately (before batch returns)
@@ -925,30 +946,30 @@ export function batch(effect: ScopedCallback | ScopedCallback[], immediate?: 'im
925
946
  inDegrees: new Map(),
926
947
  }
927
948
 
928
- // Add initial effects
929
949
  const caller = getActiveEffect()
930
- for (let i = 0; i < effect.length; i++) {
931
- addToBatch(effect[i], caller, immediate === 'immediate')
932
- }
933
-
934
950
  const effectuatedRoots: ScopedCallback[] = []
935
- computeAllInDegrees(batchQueue)
936
- if (immediate) {
937
- // Execute immediately (before batch returns)
938
- const firstReturn: { value?: any } = {}
939
- try {
951
+ const firstReturn: { value?: any } = {}
952
+
953
+ try {
954
+ if (immediate) {
955
+ // Execute initial effects in providing order
940
956
  for (let i = 0; i < effect.length; i++) {
941
957
  try {
942
958
  const rv = effect[i]()
943
959
  if (rv !== undefined && !('value' in firstReturn)) firstReturn.value = rv
944
960
  } finally {
945
- const root = getRoot(effect[i])
946
- batchQueue.all.delete(root)
961
+ batchQueue.all.delete(getRoot(effect[i]))
947
962
  }
948
963
  }
949
- // After immediate execution, execute any effects that were triggered during execution
950
- // This is important for @atomic decorator - effects triggered inside should still run
951
- while (batchQueue.all.size > 0) {
964
+ } else {
965
+ // Add initial effects to batch and compute dependencies
966
+ for (let i = 0; i < effect.length; i++) addToBatch(effect[i], caller, false)
967
+ computeAllInDegrees(batchQueue)
968
+ }
969
+
970
+ // Processing loop for all triggered effects and cleanups
971
+ while (batchQueue.all.size > 0 || batchCleanups.size > 0) {
972
+ if (batchQueue.all.size > 0) {
952
973
  if (effectuatedRoots.length > options.maxEffectChain) {
953
974
  const cycle = findCycleInChain(effectuatedRoots as any)
954
975
  const trace = formatRoots(effectuatedRoots as any)
@@ -988,118 +1009,55 @@ export function batch(effect: ScopedCallback | ScopedCallback[], immediate?: 'im
988
1009
  break
989
1010
  }
990
1011
  }
991
- if (!batchQueue || batchQueue.all.size === 0) break
992
1012
  const rv = executeNext(effectuatedRoots)
993
- // If executeNext() returned null but batch is not empty, it means a cycle was detected
994
- // and an error was thrown, so we won't reach here
995
1013
  if (rv !== undefined && !('value' in firstReturn)) firstReturn.value = rv
1014
+ } else {
1015
+ // Process cleanups. If they trigger more effects, they will be caught in the next iteration.
1016
+ const cleanups = Array.from(batchCleanups)
1017
+ batchCleanups.clear()
1018
+ for (const cleanup of cleanups) cleanup()
1019
+
1020
+ // In immediate mode, we traditionally don't process recursive effects from cleanups.
1021
+ // If we want to keep that behavior: if (immediate) break
996
1022
  }
997
- const cleanups = Array.from(batchCleanups)
998
- batchCleanups.clear()
999
- for (const cleanup of cleanups) cleanup()
1000
- return firstReturn.value
1001
- } finally {
1002
- activationRegistry = undefined
1003
- batchQueue = undefined
1004
- options.endChain()
1005
- }
1006
- } else {
1007
- // Execute in dependency order
1008
- const firstReturn: { value?: any } = {}
1009
- try {
1010
- // Outer loop: continue while there are effects OR cleanups pending.
1011
- // This ensures effects triggered by cleanups are not lost.
1012
- while (batchQueue.all.size > 0 || batchCleanups.size > 0) {
1013
- // Inner loop: execute all pending effects
1014
- while (batchQueue.all.size > 0) {
1015
- if (effectuatedRoots.length > options.maxEffectChain) {
1016
- const cycle = findCycleInChain(effectuatedRoots as any)
1017
- const trace = formatRoots(effectuatedRoots as any)
1018
- const message = cycle
1019
- ? `Max effect chain reached (cycle detected: ${formatRoots(cycle)})`
1020
- : `Max effect chain reached (trace: ${trace})`
1021
-
1022
- const queuedRoots = batchQueue ? Array.from(batchQueue.all.keys()) : []
1023
- const queued = queuedRoots.map((r) => r.name || '<anonymous>')
1024
- const debugInfo = {
1025
- code: ReactiveErrorCode.MaxDepthExceeded,
1026
- effectuatedRoots,
1027
- cycle,
1028
- trace,
1029
- maxEffectChain: options.maxEffectChain,
1030
- queued: queued.slice(0, 50),
1031
- queuedCount: queued.length,
1032
- // Try to get causation for the last effect
1033
- causalChain:
1034
- effectuatedRoots.length > 0
1035
- ? getTriggerChain(
1036
- batchQueue.all.get(effectuatedRoots[effectuatedRoots.length - 1])!
1037
- )
1038
- : [],
1039
- }
1040
- switch (options.maxEffectReaction) {
1041
- case 'throw':
1042
- throw new ReactiveError(`[reactive] ${message}`, debugInfo)
1043
- case 'debug':
1044
- // biome-ignore lint/suspicious/noDebugger: This is the whole point here
1045
- debugger
1046
- throw new ReactiveError(`[reactive] ${message}`, debugInfo)
1047
- case 'warn':
1048
- options.warn(
1049
- `[reactive] ${message} (queued: ${queued.slice(0, 10).join(', ')}${queued.length > 10 ? ', …' : ''})`
1050
- )
1051
- break
1052
- }
1053
- }
1054
- const rv = executeNext(effectuatedRoots)
1055
- // executeNext() returns null when batch is complete or cycle detected (throws error)
1056
- // But functions can legitimately return null, so we check batchQueue.all.size instead
1057
- if (batchQueue.all.size === 0) {
1058
- // Batch complete
1059
- break
1060
- }
1061
- // If executeNext() returned null but batch is not empty, it means a cycle was detected
1062
- // and an error was thrown, so we won't reach here
1063
- if (rv !== undefined && !('value' in firstReturn)) firstReturn.value = rv
1064
- // Note: executeNext() already removed it from batchQueue, so we track by count
1065
- }
1066
- // Process cleanups. If they trigger new effects, the outer loop will catch them.
1067
- if (batchCleanups.size > 0) {
1068
- const cleanups = Array.from(batchCleanups)
1069
- batchCleanups.clear()
1070
- for (const cleanup of cleanups) cleanup()
1071
- }
1072
- }
1073
- return firstReturn.value
1074
- } finally {
1075
- activationRegistry = undefined
1076
- batchQueue = undefined
1077
- options.endChain()
1078
1023
  }
1024
+ return firstReturn.value
1025
+ } catch (error) {
1026
+ throw error instanceof ReactiveError
1027
+ ? error
1028
+ : new ReactiveError('Effects are broken', { code: ReactiveErrorCode.BrokenEffects, cause: error })
1029
+ } finally {
1030
+ activationRegistry = undefined
1031
+ batchQueue = undefined
1032
+ options.endChain()
1079
1033
  }
1080
1034
  }
1081
1035
  }
1082
1036
 
1037
+ // Inject batch function to allow atomic game loops in requestAnimationFrame/setTimeout/...
1038
+ // TODO: perhaps introduce somewhere a way to wrap async functions - find out if it's necessary
1039
+ // wrapAsync(fn=> batch(fn, 'immediate'))
1040
+
1083
1041
  /**
1084
1042
  * Decorator that makes methods atomic - batches all effects triggered within the method
1085
1043
  */
1086
1044
  export const atomic = decorator({
1087
1045
  method(original) {
1088
- return function (...args: any[]) {
1089
- return batch(
1090
- markWithRoot(() => original.apply(this, args), original),
1091
- 'immediate'
1092
- )
1046
+ return function (this: any, ...args: any[]) {
1047
+ const atomicEffect = () => original.apply(this, args)
1048
+ // Debug: helpful to have a name
1049
+ Object.defineProperty(atomicEffect, 'name', { value: `atomic(${original.name})` })
1050
+ return batch(atomicEffect, 'immediate')
1093
1051
  }
1094
1052
  },
1095
1053
  default<Args extends any[], Return>(
1096
1054
  original: (...args: Args) => Return
1097
1055
  ): (...args: Args) => Return {
1098
1056
  return function (this: any, ...args: Args) {
1099
- return batch(
1100
- markWithRoot(() => original.apply(this, args), original),
1101
- 'immediate'
1102
- )
1057
+ const atomicEffect = () => original.apply(this, args)
1058
+ // Debug: helpful to have a name
1059
+ Object.defineProperty(atomicEffect, 'name', { value: `atomic(${original.name})` })
1060
+ return batch(atomicEffect, 'immediate')
1103
1061
  }
1104
1062
  },
1105
1063
  })
@@ -1120,10 +1078,10 @@ export function effect(
1120
1078
  //biome-ignore lint/suspicious/noConfusingVoidType: We have to
1121
1079
  fn: (access: DependencyAccess) => ScopedCallback | undefined | void | Promise<any>,
1122
1080
  effectOptions?: EffectOptions
1123
- ): ScopedCallback {
1124
- // Ensure zone is hooked if asyncZone option is enabled (lazy initialization)
1125
- // Inject batch function to allow atomic game loops in requestAnimationFrame
1126
- ensureZoneHooked(batch)
1081
+ ): ScopedCallback & {
1082
+ [stopped]: boolean
1083
+ [cleanupSymbol]: () => void
1084
+ } {
1127
1085
 
1128
1086
  // Use per-effect asyncMode or fall back to global option
1129
1087
  const asyncMode = effectOptions?.asyncMode ?? options.asyncMode ?? 'cancel'
@@ -1136,10 +1094,13 @@ export function effect(
1136
1094
  }
1137
1095
  }
1138
1096
  let cleanup: (() => void) | null = null
1139
- // capture the parent effect at creation time for ascend
1140
- const parentsForAscend = captureEffectStack()
1141
- const tracked = markWithRoot(<T>(cb: () => T) => withEffect(runEffect, cb), fn)
1142
- const ascend = <T>(cb: () => T) => withEffectStack(parentsForAscend, cb)
1097
+ const tracked = effectHistory.present.with(runEffect, ()=> effectAggregator.zoned)
1098
+ const ascend = effectHistory.zoned
1099
+ //const parent = effectHistory.present.active // TODO: Double-check parenting (untracked -> stop children) - use case or untracked(effect)
1100
+ let parent = effectHistory.present.active
1101
+ /*if (!parent) {
1102
+ for (const h of effectHistory.active.history) parent = h
1103
+ }*/
1143
1104
  let effectStopped = false
1144
1105
  let hasReacted = false
1145
1106
  let runningPromise: Promise<any> | null = null
@@ -1150,7 +1111,7 @@ export function effect(
1150
1111
  if (cleanup) {
1151
1112
  const prevCleanup = cleanup
1152
1113
  cleanup = null
1153
- withEffect(undefined, () => prevCleanup())
1114
+ untracked(() => prevCleanup())
1154
1115
  }
1155
1116
 
1156
1117
  // Handle async modes when effect is retriggered
@@ -1174,7 +1135,7 @@ export function effect(
1174
1135
  let reactionCleanup: ScopedCallback | undefined
1175
1136
  let result: any
1176
1137
  try {
1177
- result = withEffect(runEffect, () => fn({ tracked, ascend, reaction: hasReacted }))
1138
+ result = tracked(() => fn({ tracked, ascend, reaction: hasReacted }))
1178
1139
  if (
1179
1140
  result &&
1180
1141
  typeof result !== 'function' &&
@@ -1250,6 +1211,28 @@ export function effect(
1250
1211
  }
1251
1212
  // Mark the runEffect callback with the original function as its root
1252
1213
  markWithRoot(runEffect, fn)
1214
+ function augmentedRv(rv: ScopedCallback) {
1215
+ return Object.defineProperties(rv, {
1216
+ [stopped]: {
1217
+ get() {
1218
+ return effectStopped
1219
+ },
1220
+ },
1221
+ [cleanupSymbol]: {
1222
+ value: () => {
1223
+ if (cleanup) {
1224
+ const prevCleanup = cleanup
1225
+ cleanup = null
1226
+ untracked(() => prevCleanup())
1227
+ }
1228
+ },
1229
+ },
1230
+ }) as ScopedCallback & {
1231
+ [stopped]: boolean
1232
+ [cleanupSymbol]: () => void
1233
+ }
1234
+ }
1235
+ augmentedRv(runEffect)
1253
1236
 
1254
1237
  // Register strict mode if enabled
1255
1238
  if (effectOptions?.opaque) {
@@ -1262,7 +1245,6 @@ export function effect(
1262
1245
 
1263
1246
  batch(runEffect, 'immediate')
1264
1247
 
1265
- const parent = parentsForAscend[0]
1266
1248
  // Store parent relationship for hierarchy traversal
1267
1249
  effectParent.set(runEffect, parent)
1268
1250
  // Only ROOT effects are registered for GC cleanup and zone tracking
@@ -1283,7 +1265,7 @@ export function effect(
1283
1265
  fr.unregister(stopEffect)
1284
1266
  }
1285
1267
  if (isRootEffect) {
1286
- const callIfCollected = () => stopEffect()
1268
+ const callIfCollected = augmentedRv(() => stopEffect())
1287
1269
  fr.register(
1288
1270
  callIfCollected,
1289
1271
  () => {
@@ -1300,15 +1282,16 @@ export function effect(
1300
1282
  children = new Set()
1301
1283
  effectChildren.set(parent, children)
1302
1284
  }
1303
- const subEffectCleanup = (): void => {
1285
+ const subEffectCleanup = augmentedRv(() => {
1304
1286
  children.delete(subEffectCleanup)
1305
1287
  if (children.size === 0) {
1306
1288
  effectChildren.delete(parent)
1307
1289
  }
1308
1290
  // Execute this child effect cleanup (which triggers its own mainCleanup)
1309
1291
  stopEffect()
1310
- }
1292
+ })
1311
1293
  children.add(subEffectCleanup)
1294
+
1312
1295
  return subEffectCleanup
1313
1296
  }
1314
1297
 
@@ -1318,17 +1301,7 @@ export function effect(
1318
1301
  * @param fn - The function to execute
1319
1302
  */
1320
1303
  export function untracked<T>(fn: () => T): T {
1321
- // Store current tracking state and temporarily disable it
1322
- // This prevents the parent effect from tracking dependencies during fn execution
1323
- const wasTrackingDisabled = getTrackingDisabled()
1324
- setTrackingDisabled(true)
1325
-
1326
- try {
1327
- return fn()
1328
- } finally {
1329
- // Restore tracking state
1330
- setTrackingDisabled(wasTrackingDisabled)
1331
- }
1304
+ return effectHistory.present.root(fn)
1332
1305
  }
1333
1306
 
1334
1307
  /**
@@ -1337,11 +1310,7 @@ export function untracked<T>(fn: () => T): T {
1337
1310
  * @param fn - The function to execute
1338
1311
  */
1339
1312
  export function root<T>(fn: () => T): T {
1340
- let rv!: T
1341
- withEffect(undefined, () => {
1342
- rv = fn()
1343
- })
1344
- return rv
1313
+ return effectHistory.root(fn)
1345
1314
  }
1346
1315
 
1347
1316
  export { effectTrackers }
@@ -1397,18 +1366,17 @@ export function biDi<T>(
1397
1366
  set = get.set
1398
1367
  get = get.get
1399
1368
  }
1400
- const root = getRoot(received)
1369
+ let programatticallySetValue: any = Symbol()
1401
1370
  effect(
1402
1371
  markWithRoot(() => {
1403
- received(get())
1404
- }, root)
1372
+ const newValue = get()
1373
+ if (unwrap(newValue) !== programatticallySetValue) received(newValue)
1374
+ }, received)
1405
1375
  )
1406
- return atomic((value: T) => {
1407
- set!(value)
1408
- if (batchQueue?.all.has(root)) {
1409
- // Remove the effect from the batch queue so it doesn't execute
1410
- // This prevents circular updates in bidirectional bindings
1411
- batchQueue.all.delete(root)
1412
- }
1413
- })
1376
+ return set
1377
+ ? atomic((value: T) => {
1378
+ programatticallySetValue = unwrap(value)
1379
+ set(value)
1380
+ })
1381
+ : () => {}
1414
1382
  }
@@ -13,33 +13,30 @@ export { deepWatch } from './deep-watch'
13
13
  export {
14
14
  addBatchCleanup,
15
15
  atomic,
16
- batch, // TODO: Batch is now exported for testing purposes, though it shouldn't be - modify the tests to go through `atomic`
17
16
  biDi,
18
17
  defer,
19
18
  effect,
20
19
  getActivationLog,
21
- getActiveEffect,
22
20
  root,
23
21
  trackEffect,
24
22
  untracked,
25
23
  } from './effects'
26
24
  export { cleanedBy, cleanup, derived, unreactive, watch } from './interface'
27
- export { mapped, ReadOnlyError, reduced } from './mapped'
28
25
  export { type Memoizable, memoize } from './memoize'
29
26
  export { immutables, isNonReactive, registerNativeReactivity } from './non-reactive'
30
27
  export { getActiveProjection, project } from './project'
31
28
  export { isReactive, ReactiveBase, reactive, unwrap } from './proxy'
32
29
  export { organize, organized } from './record'
30
+ export { scan, type ScanResult } from './buffer'
33
31
  export { Register, register } from './register'
34
32
  export {
35
33
  type DependencyAccess,
36
- type DependencyFunction,
37
34
  type Evolution,
38
35
  options as reactiveOptions,
39
36
  ReactiveError,
37
+ ReactiveErrorCode,
40
38
  type ScopedCallback,
41
39
  } from './types'
42
- export { isZoneEnabled, setZoneEnabled } from './zone'
43
40
 
44
41
  import { ReactiveArray } from './array'
45
42
  import {
@@ -49,17 +46,17 @@ import {
49
46
  objectsWithDeepWatchers,
50
47
  } from './deep-watch'
51
48
  import { ReactiveMap, ReactiveWeakMap } from './map'
52
- import { nonReactiveObjects, registerNativeReactivity } from './non-reactive-state'
53
- import { objectToProxy, proxyToObject } from './proxy'
49
+ import { nonReactiveObjects } from './non-reactive-state'
50
+ import { metaProtos, objectToProxy, proxyToObject } from './proxy'
51
+ import { effectToReactiveObjects, watchers } from './registry'
54
52
  import { ReactiveSet, ReactiveWeakSet } from './set'
55
- import { effectToReactiveObjects, watchers } from './tracking'
56
53
 
57
54
  // Register native collection types to use specialized reactive wrappers
58
- registerNativeReactivity(WeakMap, ReactiveWeakMap)
59
- registerNativeReactivity(Map, ReactiveMap)
60
- registerNativeReactivity(WeakSet, ReactiveWeakSet)
61
- registerNativeReactivity(Set, ReactiveSet)
62
- registerNativeReactivity(Array, ReactiveArray)
55
+ metaProtos.set(Array, ReactiveArray.prototype)
56
+ metaProtos.set(Set, ReactiveSet.prototype)
57
+ metaProtos.set(WeakSet, ReactiveWeakSet.prototype)
58
+ metaProtos.set(Map, ReactiveMap.prototype)
59
+ metaProtos.set(WeakMap, ReactiveWeakMap.prototype)
63
60
 
64
61
  /**
65
62
  * Object containing internal reactive system state for debugging and profiling
@@ -1,10 +1,10 @@
1
1
  import { decorator, type GenericClassDecorator } from '../decorator'
2
2
  import { deepWatch } from './deep-watch'
3
- import { withEffect } from './effect-context'
4
- import { effect, getActiveEffect, untracked } from './effects'
3
+ import { effect, untracked } from './effects'
5
4
  import { isNonReactive, nonReactiveClass, nonReactiveObjects } from './non-reactive-state'
6
5
  import { unwrap } from './proxy-state'
7
- import { dependant, markWithRoot } from './tracking'
6
+ import { markWithRoot } from './registry'
7
+ import { dependant } from './tracking'
8
8
  import {
9
9
  type DependencyAccess,
10
10
  nonReactiveMark,
@@ -86,15 +86,12 @@ function watchObject(
86
86
  changed: (value: object) => void,
87
87
  { immediate = false, deep = false } = {}
88
88
  ): ScopedCallback {
89
- const myParentEffect = getActiveEffect()
90
89
  if (deep) return deepWatch(value, changed, { immediate })!
91
- return effect(
92
- markWithRoot(function watchObjectEffect() {
93
- dependant(value)
94
- if (immediate) withEffect(myParentEffect, () => changed(value))
95
- immediate = true
96
- }, changed)
97
- )
90
+ return effect(function watchObjectEffect() {
91
+ dependant(value)
92
+ if (immediate) changed(value)
93
+ immediate = true
94
+ })
98
95
  }
99
96
 
100
97
  function watchCallBack<T>(
@@ -102,29 +99,20 @@ function watchCallBack<T>(
102
99
  changed: (value: T, oldValue?: T) => void,
103
100
  { immediate = false, deep = false } = {}
104
101
  ): ScopedCallback {
105
- const myParentEffect = getActiveEffect()
106
102
  let oldValue: T | typeof unsetYet = unsetYet
107
103
  let deepCleanup: ScopedCallback | undefined
108
104
  const cbCleanup = effect(
109
105
  markWithRoot(function watchCallBackEffect(access) {
110
106
  const newValue = value(access)
111
107
  if (oldValue !== newValue)
112
- withEffect(
113
- myParentEffect,
114
- markWithRoot(() => {
115
- if (oldValue === unsetYet) {
116
- if (immediate) changed(newValue)
117
- } else changed(newValue, oldValue)
118
- oldValue = newValue
119
- if (deep) {
120
- if (deepCleanup) deepCleanup()
121
- deepCleanup = deepWatch(
122
- newValue as object,
123
- markWithRoot((value) => changed(value as T, value as T), changed)
124
- )
125
- }
126
- }, changed)
127
- )
108
+ if (oldValue === unsetYet) {
109
+ if (immediate) changed(newValue)
110
+ } else changed(newValue, oldValue)
111
+ oldValue = newValue
112
+ if (deep) {
113
+ if (deepCleanup) deepCleanup()
114
+ deepCleanup = deepWatch(newValue as object, (value) => changed(value as T, value as T))
115
+ }
128
116
  }, value)
129
117
  )
130
118
  return () => {
@@ -153,6 +141,7 @@ function deepNonReactive<T>(obj: T): T {
153
141
  })
154
142
  } catch {}
155
143
  if (!(nonReactiveMark in (obj as object))) nonReactiveObjects.add(obj as object)
144
+ // Finally, not deep
156
145
  //for (const key in obj) deepNonReactive(obj[key])
157
146
  return obj
158
147
  }
@@ -213,11 +202,9 @@ export function derived<T>(compute: (dep: DependencyAccess) => T): {
213
202
  return cleanedBy(
214
203
  rv,
215
204
  untracked(() =>
216
- effect(
217
- markWithRoot(function derivedEffect(access) {
218
- rv.value = compute(access)
219
- }, compute)
220
- )
205
+ effect(function derivedEffect(access) {
206
+ rv.value = compute(access)
207
+ })
221
208
  )
222
209
  )
223
210
  }