mutts 1.0.5 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +1 -0
  2. package/dist/chunks/{_tslib-Mzh1rNsX.esm.js → _tslib-MCKDzsSq.esm.js} +2 -2
  3. package/dist/chunks/_tslib-MCKDzsSq.esm.js.map +1 -0
  4. package/dist/chunks/decorator-BGILvPtN.esm.js +627 -0
  5. package/dist/chunks/decorator-BGILvPtN.esm.js.map +1 -0
  6. package/dist/chunks/decorator-BQ2eBTCj.js +651 -0
  7. package/dist/chunks/decorator-BQ2eBTCj.js.map +1 -0
  8. package/dist/chunks/{index-Cvxdw6Ax.js → index-CDCOjzTy.js} +396 -500
  9. package/dist/chunks/index-CDCOjzTy.js.map +1 -0
  10. package/dist/chunks/{index-qiWwozOc.esm.js → index-DiP0RXoZ.esm.js} +301 -403
  11. package/dist/chunks/index-DiP0RXoZ.esm.js.map +1 -0
  12. package/dist/decorator.d.ts +3 -3
  13. package/dist/decorator.esm.js +1 -1
  14. package/dist/decorator.js +1 -1
  15. package/dist/destroyable.esm.js +4 -4
  16. package/dist/destroyable.esm.js.map +1 -1
  17. package/dist/destroyable.js +4 -4
  18. package/dist/destroyable.js.map +1 -1
  19. package/dist/devtools/panel.js.map +1 -1
  20. package/dist/eventful.esm.js +1 -1
  21. package/dist/index.esm.js +48 -3
  22. package/dist/index.esm.js.map +1 -1
  23. package/dist/index.js +48 -4
  24. package/dist/index.js.map +1 -1
  25. package/dist/mutts.umd.js +1 -1
  26. package/dist/mutts.umd.js.map +1 -1
  27. package/dist/mutts.umd.min.js +1 -1
  28. package/dist/mutts.umd.min.js.map +1 -1
  29. package/dist/reactive.d.ts +25 -0
  30. package/dist/reactive.esm.js +3 -3
  31. package/dist/reactive.js +4 -4
  32. package/dist/std-decorators.d.ts +1 -1
  33. package/dist/std-decorators.esm.js +10 -10
  34. package/dist/std-decorators.esm.js.map +1 -1
  35. package/dist/std-decorators.js +10 -10
  36. package/dist/std-decorators.js.map +1 -1
  37. package/docs/ai/manual.md +14 -95
  38. package/docs/reactive/advanced.md +6 -107
  39. package/docs/reactive/debugging.md +158 -0
  40. package/docs/reactive.md +6 -5
  41. package/package.json +16 -66
  42. package/src/decorator.ts +11 -9
  43. package/src/destroyable.ts +3 -3
  44. package/src/index.ts +46 -0
  45. package/src/reactive/change.ts +1 -1
  46. package/src/reactive/debug.ts +1 -1
  47. package/src/reactive/deep-touch.ts +1 -1
  48. package/src/reactive/deep-watch.ts +1 -1
  49. package/src/reactive/effect-context.ts +2 -2
  50. package/src/reactive/effects.ts +44 -16
  51. package/src/reactive/index.ts +1 -1
  52. package/src/reactive/interface.ts +9 -8
  53. package/src/reactive/memoize.ts +77 -31
  54. package/src/reactive/proxy.ts +4 -4
  55. package/src/reactive/registry.ts +67 -0
  56. package/src/reactive/tracking.ts +12 -41
  57. package/src/reactive/types.ts +37 -0
  58. package/src/std-decorators.ts +9 -9
  59. package/src/utils.ts +141 -0
  60. package/dist/chunks/_tslib-Mzh1rNsX.esm.js.map +0 -1
  61. package/dist/chunks/decorator-DLvrD0UF.js +0 -265
  62. package/dist/chunks/decorator-DLvrD0UF.js.map +0 -1
  63. package/dist/chunks/decorator-DqiszP7i.esm.js +0 -253
  64. package/dist/chunks/decorator-DqiszP7i.esm.js.map +0 -1
  65. package/dist/chunks/index-Cvxdw6Ax.js.map +0 -1
  66. package/dist/chunks/index-qiWwozOc.esm.js.map +0 -1
@@ -0,0 +1,158 @@
1
+ # Debugging Tools
2
+
3
+ The `mutts` reactive system provides several built-in tools to help track down synchronization issues, dependency leaks, and infinite loops. These tools are primarily exposed through the `reactiveOptions` object.
4
+
5
+ > [!WARNING]
6
+ > **Performance Cost**: Most debugging tools incur a significant performance overhead. They should be enabled only during development or within test environments. Re-running computations for discrepancy detection, in particular, effectively doubles the cost of reactive updates.
7
+
8
+ ## The `reactiveOptions` Reference
9
+
10
+ The `reactiveOptions` object (exported from `mutts/reactive`) allows you to hook into the reactive system's internals.
11
+
12
+ ```typescript
13
+ import { reactiveOptions } from 'mutts/reactive';
14
+ ```
15
+
16
+ ### Lifecycle Hooks
17
+
18
+ These hooks are called during the execution of effects and computed values.
19
+
20
+ - **`enter(effect: Function)`**: Called when an effect or memoized function starts executing.
21
+ - **`leave(effect: Function)`**: Called when an effect or memoized function finishes.
22
+ - **`touched(obj: any, evolution: Evolution)`**: Called whenever a reactive object is "touched" (accessed or modified). This is the lowest-level hook for observing system activity.
23
+
24
+ ### Effect Chaining & Batching
25
+
26
+ - **`beginChain(targets: Function[]) / endChain()`**: Called when a batch of effects starts and ends its execution.
27
+ - **`maxEffectChain`**: (Default: `100`) Limits the depth of synchronous effect triggering to prevent stack overflows.
28
+ - **`maxTriggerPerBatch`**: (Default: `10`) Limits how many times a single effect can be triggered within the same batch. Useful for detecting aggressive re-computation.
29
+
30
+ ## Cycle Detection
31
+
32
+ `mutts` automatically detects circular dependencies (e.g., Effect A updates State X, which triggers Effect B, which updates State Y, which triggers Effect A).
33
+
34
+ ### Configuration
35
+
36
+ You can control how cycles are handled via `reactiveOptions.cycleHandling`:
37
+
38
+ - **`'throw'`** (Default): Throws a `ReactiveError` with a detailed path.
39
+ - **`'warn'`**: Logs a warning but breaks the cycle to allow the application to continue.
40
+ - **`'break'`**: Silently breaks the cycle.
41
+ - **`'strict'`**: Performs a graph check *before* execution to prevent cycles from even starting. This has the highest overhead.
42
+
43
+ ## Memoization Discrepancy Detection
44
+
45
+ The most powerful debugging tool in `mutts` is the **Discrepancy Detector**. It helps identify "missing dependencies"—reactive values used inside a computation that the system isn't tracking.
46
+
47
+ ### How it Works
48
+
49
+ When `reactiveOptions.onMemoizationDiscrepancy` is set:
50
+ 1. Every time a `memoize()` function is called, it checks its cache.
51
+ 2. If a cached value exists, the function is immediately **executed a second time** in a completely untracked context.
52
+ 3. If the results differ, your callback is triggered.
53
+
54
+ ### Usage
55
+
56
+ ```typescript
57
+ reactiveOptions.onMemoizationDiscrepancy = (cached, fresh, fn, args, cause) => {
58
+ console.error(`Discrepancy in ${fn.name || 'anonymous'}!`, {
59
+ cached,
60
+ fresh,
61
+ cause // 'calculation' or 'comparison'
62
+ });
63
+ throw new Error('Memoization discrepancy detected');
64
+ };
65
+ ```
66
+
67
+ If the cause is 'comparison', it means that, at first computation, calling the function twice gives different results.
68
+
69
+ If the cause is 'calculation', it means that, when asked the value while the cache was still valid, the function returned a different result.
70
+
71
+ ### `isVerificationRun`
72
+
73
+ During the "second run" of a discrepancy check, `reactiveOptions.isVerificationRun` is set to `true`. You can use this flag in your own code to avoid side effects (like incrementing counters) that should only happen once during the primary execution.
74
+
75
+ > [!IMPORTANT]
76
+ > Do not modify `isVerificationRun` manually; it is managed by the engine.
77
+
78
+ ## Introspection API
79
+
80
+ For programmatic analysis of the reactive system, `mutts` provides a dedicated introspection module. This is particularly useful for AI agents and developer tools.
81
+
82
+ ```typescript
83
+ import { enableIntrospection, getDependencyGraph, getMutationHistory, snapshot } from 'mutts/introspection';
84
+ ```
85
+
86
+ ### Enabling Introspection
87
+
88
+ Introspection features (like history tracking) are memory-intensive and disabled by default.
89
+
90
+ ```typescript
91
+ // Enable history tracking (default size: 50)
92
+ enableIntrospection({ historySize: 100 });
93
+ ```
94
+
95
+ ### Dependency Graph
96
+
97
+ You can retrieve the full dependency graph to understand how objects and effects are linked.
98
+
99
+ ```typescript
100
+ const graph = getDependencyGraph();
101
+ // Returns: { nodes: Array<EffectNode | ObjectNode>, edges: Array<GraphEdge> }
102
+ ```
103
+
104
+ ### Mutation History
105
+
106
+ If `enableHistory` is on, you can inspect the sequence of mutations that have occurred.
107
+
108
+ ```typescript
109
+ const history = getMutationHistory();
110
+ // Each record contains type, property, old/new values, and causal source.
111
+ ```
112
+
113
+ ## Structured Error Handling
114
+
115
+ When the reactive system encounters a critical failure (like a cycle or max depth exceeded), it throws a `ReactiveError` containing rich diagnostic information.
116
+
117
+ ### `ReactiveErrorCode`
118
+
119
+ Always check `error.debugInfo.code` to identify the failure type:
120
+ - `CYCLE_DETECTED`: A circular dependency was found.
121
+ - `MAX_DEPTH_EXCEEDED`: The synchronous effect chain reached `maxEffectChain`.
122
+ - `MAX_REACTION_EXCEEDED`: An effect was triggered too many times in a single batch.
123
+ - `WRITE_IN_COMPUTED`: An attempt was made to modify reactive state inside a `memoize` or `derived` function.
124
+
125
+ ### Rich Debug Info
126
+
127
+ The `debugInfo` property on `ReactiveError` includes:
128
+ - **`causalChain`**: A string array describing the logical path of modifications leading to the error.
129
+ - **`creationStack`**: The stack trace of where the effect was originally created, helping you locate the source in your code.
130
+ - **`cycle`**: (For `CYCLE_DETECTED`) The names of the effects that form the loop.
131
+
132
+ ## Best Practices for Debugging
133
+
134
+ ### Naming Effects
135
+
136
+ Always provide a name for your effects to make debug logs and error messages readable:
137
+
138
+ ```typescript
139
+ effect(() => {
140
+ // ...
141
+ }, { name: 'UpdateSidebarCounter' });
142
+ ```
143
+
144
+ ### Activation & Deactivation
145
+
146
+ Since these are runtime options, you can toggle them based on your environment:
147
+
148
+ ```typescript
149
+ if (process.env.NODE_ENV === 'development') {
150
+ reactiveOptions.cycleHandling = 'throw';
151
+ reactiveOptions.onMemoizationDiscrepancy = myHandler;
152
+ enableIntrospection();
153
+ } else {
154
+ // Ensure they are off in production for performance
155
+ reactiveOptions.onMemoizationDiscrepancy = undefined;
156
+ reactiveOptions.cycleHandling = 'break';
157
+ }
158
+ ```
package/docs/reactive.md CHANGED
@@ -20,9 +20,10 @@ The Mutts Reactive System documentation has been split into focused sections for
20
20
  * **[Memoization](./reactive/advanced.md#memoization)**: Caching strategies
21
21
  * **[Debugging](./reactive/advanced.md#debugging-and-development)**: Cycle detection and troubleshooting
22
22
 
23
- ## Debugging tools
23
+ ## [Debugging and Troubleshooting](./reactive/debugging.md)
24
+ * **[Reactive Options](./reactive/debugging.md#the-reactiveoptions-reference)**: Global debug hooks and configuration
25
+ * **[Cycle Detection](./reactive/debugging.md#cycle-detection)**: Configuration and troubleshooting circular dependencies
26
+ * **[Memoization Discrepancy](./reactive/debugging.md#memoization-discrepancy-detection)**: Identifying missing dependencies
27
+ * **[Introspection API](./reactive/debugging.md#introspection-api)**: Programmatic analysis and dependency graphs
24
28
 
25
- The reactive system is currently in a gamma state. The interface is quite fixed, the debugging tools are in place but :
26
- - still unfinished
27
- - not deactivatable
28
- - harming the performances of the application
29
+ * **[Performance](./reactive/debugging.md#performance-cost)**: Understanding the cost of debugging tools
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "mutts",
3
3
  "description": "Modern UTility TS: A collection of TypeScript utilities",
4
- "version": "1.0.5",
5
- "main": "dist/index.js",
6
- "module": "dist/index.esm.js",
7
- "types": "dist/index.d.ts",
4
+ "version": "1.0.6",
5
+ "main": "src/index.ts",
6
+ "module": "src/index.ts",
7
+ "types": "src/index.ts",
8
8
  "exports": {
9
9
  ".": {
10
10
  "types": "./dist/index.d.ts",
@@ -13,60 +13,7 @@
13
13
  "require": "./dist/index.js",
14
14
  "script": "./dist/mutts.umd.min.js"
15
15
  },
16
- "./decorator": {
17
- "types": "./dist/decorator.d.ts",
18
- "source": "./src/decorator.ts",
19
- "import": "./dist/decorator.esm.js",
20
- "require": "./dist/decorator.js"
21
- },
22
- "./reactive": {
23
- "types": "./dist/reactive.d.ts",
24
- "source": "./src/reactive/index.ts",
25
- "import": "./dist/reactive.esm.js",
26
- "require": "./dist/reactive.js"
27
- },
28
- "./eventful": {
29
- "types": "./dist/eventful.d.ts",
30
- "source": "./src/eventful.ts",
31
- "import": "./dist/eventful.esm.js",
32
- "require": "./dist/eventful.js"
33
- },
34
- "./indexable": {
35
- "types": "./dist/indexable.d.ts",
36
- "source": "./src/indexable.ts",
37
- "import": "./dist/indexable.esm.js",
38
- "require": "./dist/indexable.js"
39
- },
40
- "./promiseChain": {
41
- "types": "./dist/promiseChain.d.ts",
42
- "source": "./src/promiseChain.ts",
43
- "import": "./dist/promiseChain.esm.js",
44
- "require": "./dist/promiseChain.js"
45
- },
46
- "./destroyable": {
47
- "types": "./dist/destroyable.d.ts",
48
- "source": "./src/destroyable.ts",
49
- "import": "./dist/destroyable.esm.js",
50
- "require": "./dist/destroyable.js"
51
- },
52
- "./std-decorators": {
53
- "types": "./dist/std-decorators.d.ts",
54
- "source": "./src/std-decorators.ts",
55
- "import": "./dist/std-decorators.esm.js",
56
- "require": "./dist/std-decorators.js"
57
- },
58
- "./src": {
59
- "import": "./src/index.ts"
60
- },
61
- "./src/*": {
62
- "import": "./src/*"
63
- },
64
- "./umd": {
65
- "browser": "./dist/mutts.umd.js"
66
- },
67
- "./umd.min": {
68
- "browser": "./dist/mutts.umd.min.js"
69
- }
16
+ "./src/*": "./src/*"
70
17
  },
71
18
  "files": [
72
19
  "dist",
@@ -80,14 +27,14 @@
80
27
  "build": "npm run build:js && npm run build:devtools",
81
28
  "build:watch": "rollup -c --watch",
82
29
  "prepublishOnly": "npm run build",
83
- "test": "node --expose-gc node_modules/.bin/jest",
84
- "test:coverage": "node --expose-gc node_modules/.bin/jest --coverage",
85
- "test:coverage:watch": "node --expose-gc node_modules/.bin/jest --coverage --watch",
86
- "test:legacy": "TSCONFIG=tsconfig.legacy.json node node_modules/.bin/jest --detectOpenHandles --testPathPatterns=decorator",
87
- "test:modern": "TSCONFIG=tsconfig.modern.json node node_modules/.bin/jest --detectOpenHandles --testPathPatterns=decorator",
88
- "test:profile": "RUN_PROFILING=1 node --expose-gc node_modules/.bin/jest --testPathPatterns=profiling",
89
- "test:profile:benchmark": "RUN_PROFILING=1 node --expose-gc node_modules/.bin/jest --testPathPatterns=profiling --testNamePattern=benchmark",
90
- "test:profile:detailed": "RUN_PROFILING=1 node --prof node_modules/.bin/jest --testPathPatterns=profiling --no-coverage",
30
+ "test": "NODE_OPTIONS=--expose-gc jest",
31
+ "test:coverage": "NODE_OPTIONS=--expose-gc jest --coverage",
32
+ "test:coverage:watch": "NODE_OPTIONS=--expose-gc jest --coverage --watch",
33
+ "test:legacy": "TSCONFIG=tsconfig.legacy.json jest --detectOpenHandles --testPathPatterns=decorator",
34
+ "test:modern": "TSCONFIG=tsconfig.modern.json jest --detectOpenHandles --testPathPatterns=decorator",
35
+ "test:profile": "RUN_PROFILING=1 NODE_OPTIONS=--expose-gc jest --testPathPatterns=profiling",
36
+ "test:profile:benchmark": "RUN_PROFILING=1 NODE_OPTIONS=--expose-gc jest --testPathPatterns=profiling --testNamePattern=benchmark",
37
+ "test:profile:detailed": "RUN_PROFILING=1 node --prof node_modules/jest/bin/jest.js --testPathPatterns=profiling --no-coverage",
91
38
  "benchmark:save": "tsx tests/profiling/benchmark.ts save",
92
39
  "benchmark:compare": "tsx tests/profiling/benchmark.ts compare",
93
40
  "benchmark:list": "tsx tests/profiling/benchmark.ts list",
@@ -123,11 +70,14 @@
123
70
  },
124
71
  "devDependencies": {
125
72
  "@biomejs/biome": "^2.0.6",
73
+ "@jest/globals": "^30.2.0",
126
74
  "@rollup/plugin-commonjs": "^28.0.6",
75
+ "@rollup/plugin-json": "^6.1.0",
127
76
  "@rollup/plugin-node-resolve": "^16.0.1",
128
77
  "@rollup/plugin-terser": "^0.4.4",
129
78
  "@rollup/plugin-typescript": "^12.1.4",
130
79
  "@types/jest": "^30.0.0",
80
+ "@types/node": "^22.10.10",
131
81
  "jest": "^30.0.4",
132
82
  "rollup": "^4.52.2",
133
83
  "rollup-plugin-copy": "^3.5.0",
package/src/decorator.ts CHANGED
@@ -58,13 +58,15 @@ export type ModernClassDecorator<T> = (target: T, context: ClassDecoratorContext
58
58
 
59
59
  type DDMethod<T> = (
60
60
  original: (this: T, ...args: any[]) => any,
61
+ target: any,
61
62
  name: PropertyKey
62
63
  ) => ((this: T, ...args: any[]) => any) | void
63
64
 
64
- type DDGetter<T> = (original: (this: T) => any, name: PropertyKey) => ((this: T) => any) | void
65
+ type DDGetter<T> = (original: (this: T) => any, target: any, name: PropertyKey) => ((this: T) => any) | void
65
66
 
66
67
  type DDSetter<T> = (
67
68
  original: (this: T, value: any) => void,
69
+ target: any,
68
70
  name: PropertyKey
69
71
  ) => ((this: T, value: any) => void) | void
70
72
 
@@ -153,17 +155,17 @@ export function legacyDecorator<T = any>(description: DecoratorDescription<T>):
153
155
  if (!('getter' in description || 'setter' in description))
154
156
  throw new Error('Decorator cannot be applied to a getter or setter')
155
157
  if ('getter' in description) {
156
- const newGetter = description.getter!(descriptor.get as any, propertyKey)
158
+ const newGetter = description.getter!(descriptor.get as any, target, propertyKey)
157
159
  if (newGetter) descriptor.get = newGetter
158
160
  }
159
161
  if ('setter' in description) {
160
- const newSetter = description.setter!(descriptor.set as any, propertyKey)
162
+ const newSetter = description.setter!(descriptor.set as any, target, propertyKey)
161
163
  if (newSetter) descriptor.set = newSetter
162
164
  }
163
165
  return descriptor
164
166
  } else if (typeof descriptor.value === 'function') {
165
167
  if (!('method' in description)) throw new Error('Decorator cannot be applied to a method')
166
- const newMethod = description.method!(descriptor.value, propertyKey)
168
+ const newMethod = description.method!(descriptor.value, target, propertyKey)
167
169
  if (newMethod) descriptor.value = newMethod
168
170
  return descriptor
169
171
  }
@@ -196,23 +198,23 @@ export function modernDecorator<T = any>(description: DecoratorDescription<T>):
196
198
  throw new Error('Decorator cannot be applied to a field')
197
199
  case 'getter':
198
200
  if (!('getter' in description)) throw new Error('Decorator cannot be applied to a getter')
199
- return description.getter!(target, context.name)
201
+ return description.getter!(target, target, context.name)
200
202
  case 'setter':
201
203
  if (!('setter' in description)) throw new Error('Decorator cannot be applied to a setter')
202
- return description.setter!(target, context.name)
204
+ return description.setter!(target, target, context.name)
203
205
  case 'method':
204
206
  if (!('method' in description)) throw new Error('Decorator cannot be applied to a method')
205
- return description.method!(target, context.name)
207
+ return description.method!(target, target, context.name)
206
208
  case 'accessor': {
207
209
  if (!('getter' in description || 'setter' in description))
208
210
  throw new Error('Decorator cannot be applied to a getter or setter')
209
211
  const rv: Partial<ClassAccessorDecoratorResult<any, any>> = {}
210
212
  if ('getter' in description) {
211
- const newGetter = description.getter!(target.get, context.name)
213
+ const newGetter = description.getter!(target.get, target, context.name)
212
214
  if (newGetter) rv.get = newGetter
213
215
  }
214
216
  if ('setter' in description) {
215
- const newSetter = description.setter!(target.set, context.name)
217
+ const newSetter = description.setter!(target.set, target, context.name)
216
218
  if (newSetter) rv.set = newSetter
217
219
  }
218
220
  return rv
@@ -125,7 +125,7 @@ export function Destroyable<
125
125
  static destroy(obj: Destroyable) {
126
126
  const destructor = Destroyable.destructors.get(obj)
127
127
  if (!destructor) return false
128
- fr.unregister(obj)
128
+ fr.unregister(obj[allocatedValues])
129
129
  Destroyable.destructors.delete(obj)
130
130
  Object.setPrototypeOf(obj, new Proxy({}, destroyedHandler))
131
131
  // Clear all own properties
@@ -154,7 +154,7 @@ export function Destroyable<
154
154
  myDestructor(allocated)
155
155
  }
156
156
  Destroyable.destructors.set(this, destruction)
157
- fr.register(this, destruction, this)
157
+ fr.register(this, destruction, allocated)
158
158
  }
159
159
  }
160
160
  }
@@ -165,7 +165,7 @@ const forwardProperties = Symbol('forwardProperties')
165
165
  * Use with accessor properties or explicit get/set pairs
166
166
  */
167
167
  export const allocated = decorator({
168
- setter(original, propertyKey) {
168
+ setter(original, _target, propertyKey) {
169
169
  return function (value) {
170
170
  this[allocatedValues][propertyKey] = value
171
171
  return original.call(this, value)
package/src/index.ts CHANGED
@@ -7,3 +7,49 @@ export * from './mixins'
7
7
  export * from './reactive'
8
8
  export * from './std-decorators'
9
9
  export * from './utils'
10
+
11
+ import pkg from '../package.json'
12
+ const { version } = pkg
13
+
14
+ // Singleton verification
15
+ const GLOBAL_MUTTS_KEY = '__MUTTS_INSTANCE__'
16
+ const globalScope =
17
+ (typeof globalThis !== 'undefined' ? globalThis :
18
+ (typeof window !== 'undefined' ? window :
19
+ (typeof global !== 'undefined' ? global : false))) as any
20
+ if(globalScope) {
21
+ // Detect the source of this instance safely across different environments
22
+ let source = 'mutts/index'
23
+ try {
24
+ // @ts-ignore
25
+ if (typeof __filename !== 'undefined') source = __filename
26
+ // @ts-ignore
27
+ else {
28
+ // Using eval to avoid SyntaxError in CJS environments where import.meta is not allowed
29
+ const meta = eval('import.meta')
30
+ if (meta && meta.url) source = meta.url
31
+ }
32
+ } catch (e) {
33
+ // Fallback for environments where neither is available or accessible
34
+ }
35
+
36
+ const currentSourceInfo = {
37
+ version,
38
+ source,
39
+ timestamp: Date.now()
40
+ }
41
+
42
+ if (globalScope[GLOBAL_MUTTS_KEY]) {
43
+ const existing = globalScope[GLOBAL_MUTTS_KEY]
44
+ throw new Error(
45
+ `[Mutts] Multiple instances detected!\n` +
46
+ `Existing instance: ${JSON.stringify(existing, null, 2)}\n` +
47
+ `New instance: ${JSON.stringify(currentSourceInfo, null, 2)}\n` +
48
+ `This usually happens when 'mutts' is both installed as a dependency and bundled, ` +
49
+ `or when different versions are loaded. ` +
50
+ `Please check your build configuration (aliases, externals) to ensure a single source of truth.`
51
+ )
52
+ }
53
+
54
+ globalScope[GLOBAL_MUTTS_KEY] = currentSourceInfo
55
+ }
@@ -3,7 +3,7 @@ import { bubbleUpChange, objectsWithDeepWatchers } from './deep-watch-state'
3
3
  import { getActiveEffect, isRunning } from './effect-context'
4
4
  import { batch, effectTrackers, hasBatched, opaqueEffects, recordActivation } from './effects'
5
5
  import { unwrap } from './proxy-state'
6
- import { watchers } from './tracking'
6
+ import { watchers } from './registry'
7
7
  import { allProps, type Evolution, options, type ScopedCallback, type State } from './types'
8
8
 
9
9
  const states = new WeakMap<object, State>()
@@ -5,7 +5,7 @@
5
5
  * - Provides graph data for tooling (DevTools panel, etc.)
6
6
  */
7
7
 
8
- import { effectParent, effectToReactiveObjects, getRoot } from './tracking'
8
+ import { effectParent, effectToReactiveObjects, getRoot } from './registry'
9
9
  import { allProps, type Evolution, options, type ScopedCallback } from './types'
10
10
 
11
11
  const EXTERNAL_SOURCE = Symbol('external-source')
@@ -3,7 +3,7 @@ import { bubbleUpChange, objectsWithDeepWatchers } from './deep-watch-state'
3
3
  import { batch } from './effects'
4
4
  import { isNonReactive } from './non-reactive-state'
5
5
  import { unwrap } from './proxy-state'
6
- import { effectParent, watchers } from './tracking'
6
+ import { effectParent, watchers } from './registry'
7
7
  import { allProps, type Evolution, options, type ScopedCallback } from './types'
8
8
 
9
9
  function isObject(value: any): value is object {
@@ -6,7 +6,7 @@ import {
6
6
  import { effect } from './effects'
7
7
  import { isNonReactive } from './non-reactive-state'
8
8
  import { reactive, unwrap } from './proxy'
9
- import { markWithRoot } from './tracking'
9
+ import { markWithRoot } from './registry'
10
10
  import { options, type ScopedCallback } from './types'
11
11
 
12
12
  function isObject(value: any): value is object {
@@ -1,4 +1,4 @@
1
- import { effectParent, getRoot } from './tracking'
1
+ import { effectParent, getRoot } from './registry'
2
2
  import { ReactiveError, type ScopedCallback } from './types'
3
3
 
4
4
  /**
@@ -77,7 +77,7 @@ export function getActiveEffect() {
77
77
  * @returns The result of the function
78
78
  */
79
79
  export function withEffect<T>(effect: ScopedCallback | undefined, fn: () => T): T {
80
- // console.log('[Mutts] withEffect', effect ? 'Active' : 'NULL');
80
+
81
81
  if (getRoot(effect) === getRoot(getActiveEffect())) return fn()
82
82
  stack.unshift(effect)
83
83
  try {
@@ -8,16 +8,18 @@ import {
8
8
  withEffect,
9
9
  withEffectStack,
10
10
  } from './effect-context'
11
+ import {
12
+ getTrackingDisabled,
13
+ setTrackingDisabled,
14
+ } from './tracking'
11
15
  import {
12
16
  effectChildren,
13
17
  effectParent,
14
18
  effectToReactiveObjects,
15
19
  getRoot,
16
- getTrackingDisabled,
17
20
  markWithRoot,
18
- setTrackingDisabled,
19
21
  watchers,
20
- } from './tracking'
22
+ } from './registry'
21
23
  import {
22
24
  type DependencyAccess,
23
25
  type EffectOptions,
@@ -27,6 +29,8 @@ import {
27
29
  ReactiveErrorCode,
28
30
  // type AsyncExecutionMode,
29
31
  type ScopedCallback,
32
+ cleanup as cleanupSymbol,
33
+ stopped,
30
34
  } from './types'
31
35
 
32
36
  /**
@@ -297,6 +301,7 @@ function addGraphEdge(callerRoot: Function, targetRoot: Function) {
297
301
  * @param end - Target node
298
302
  * @param exclude - Node to exclude from the path
299
303
  * @returns true if a path exists without going through the excluded node
304
+ * @todo Can be REALLY costly - optimise or make optional or ...
300
305
  */
301
306
  function hasPathExcluding(start: Function, end: Function, exclude: Function): boolean {
302
307
  if (start === end) return true
@@ -621,6 +626,11 @@ 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
+ const cleanupFn = (effect as any)[cleanupSymbol]
630
+ if (cleanupFn) cleanupFn()
631
+ // If the effect was stopped during cleanup (e.g. lazy memoization), don't add it to the batch
632
+ if ((effect as any)[stopped]) return
633
+
624
634
  if (!batchQueue) return
625
635
 
626
636
  const root = getRoot(effect)
@@ -1085,21 +1095,21 @@ export function batch(effect: ScopedCallback | ScopedCallback[], immediate?: 'im
1085
1095
  */
1086
1096
  export const atomic = decorator({
1087
1097
  method(original) {
1088
- return function (...args: any[]) {
1089
- return batch(
1090
- markWithRoot(() => original.apply(this, args), original),
1091
- 'immediate'
1092
- )
1098
+ return function (this: any, ...args: any[]) {
1099
+ const atomicEffect = () => original.apply(this, args)
1100
+ // Debug: helpful to have a name
1101
+ Object.defineProperty(atomicEffect, 'name', { value: `atomic(${original.name})` })
1102
+ return batch(atomicEffect, 'immediate')
1093
1103
  }
1094
1104
  },
1095
1105
  default<Args extends any[], Return>(
1096
1106
  original: (...args: Args) => Return
1097
1107
  ): (...args: Args) => Return {
1098
1108
  return function (this: any, ...args: Args) {
1099
- return batch(
1100
- markWithRoot(() => original.apply(this, args), original),
1101
- 'immediate'
1102
- )
1109
+ const atomicEffect = () => original.apply(this, args)
1110
+ // Debug: helpful to have a name
1111
+ Object.defineProperty(atomicEffect, 'name', { value: `atomic(${original.name})` })
1112
+ return batch(atomicEffect, 'immediate')
1103
1113
  }
1104
1114
  },
1105
1115
  })
@@ -1138,7 +1148,7 @@ export function effect(
1138
1148
  let cleanup: (() => void) | null = null
1139
1149
  // capture the parent effect at creation time for ascend
1140
1150
  const parentsForAscend = captureEffectStack()
1141
- const tracked = markWithRoot(<T>(cb: () => T) => withEffect(runEffect, cb), fn)
1151
+ const tracked = <T>(cb: () => T) => withEffect(runEffect, cb)
1142
1152
  const ascend = <T>(cb: () => T) => withEffectStack(parentsForAscend, cb)
1143
1153
  let effectStopped = false
1144
1154
  let hasReacted = false
@@ -1282,8 +1292,25 @@ export function effect(
1282
1292
  cleanupEffectFromGraph(runEffect)
1283
1293
  fr.unregister(stopEffect)
1284
1294
  }
1295
+ function augmentedRv(rv: ScopedCallback): ScopedCallback {
1296
+ Object.defineProperty(rv, stopped, {
1297
+ get() {
1298
+ return effectStopped
1299
+ },
1300
+ })
1301
+ Object.defineProperty(rv, cleanupSymbol, {
1302
+ value: () => {
1303
+ if (cleanup) {
1304
+ const prevCleanup = cleanup
1305
+ cleanup = null
1306
+ withEffect(undefined, () => prevCleanup())
1307
+ }
1308
+ },
1309
+ })
1310
+ return rv
1311
+ }
1285
1312
  if (isRootEffect) {
1286
- const callIfCollected = () => stopEffect()
1313
+ const callIfCollected = augmentedRv(() => stopEffect())
1287
1314
  fr.register(
1288
1315
  callIfCollected,
1289
1316
  () => {
@@ -1300,15 +1327,16 @@ export function effect(
1300
1327
  children = new Set()
1301
1328
  effectChildren.set(parent, children)
1302
1329
  }
1303
- const subEffectCleanup = (): void => {
1330
+ const subEffectCleanup = augmentedRv(() => {
1304
1331
  children.delete(subEffectCleanup)
1305
1332
  if (children.size === 0) {
1306
1333
  effectChildren.delete(parent)
1307
1334
  }
1308
1335
  // Execute this child effect cleanup (which triggers its own mainCleanup)
1309
1336
  stopEffect()
1310
- }
1337
+ })
1311
1338
  children.add(subEffectCleanup)
1339
+
1312
1340
  return subEffectCleanup
1313
1341
  }
1314
1342
 
@@ -52,7 +52,7 @@ import { ReactiveMap, ReactiveWeakMap } from './map'
52
52
  import { nonReactiveObjects, registerNativeReactivity } from './non-reactive-state'
53
53
  import { objectToProxy, proxyToObject } from './proxy'
54
54
  import { ReactiveSet, ReactiveWeakSet } from './set'
55
- import { effectToReactiveObjects, watchers } from './tracking'
55
+ import { effectToReactiveObjects, watchers } from './registry'
56
56
 
57
57
  // Register native collection types to use specialized reactive wrappers
58
58
  registerNativeReactivity(WeakMap, ReactiveWeakMap)