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
@@ -1167,114 +1167,13 @@ Pair `memoize()` with [`mapped()`](#mapped) to keep derived array elements stabl
1167
1167
 
1168
1168
  ## Debugging and Development
1169
1169
 
1170
- ### Cycle Detection
1170
+ The `mutts` reactive system includes built-in tools for troubleshooting complex dependency graphs and identifying performance bottlenecks.
1171
1171
 
1172
- The reactive system automatically detects circular dependencies between effects. When one effect triggers another effect that eventually triggers the first effect again, a cycle is detected.
1172
+ For a full guide on debugging, including cycle detection and memoization discrepancy tools, see the dedicated **[Debugging Tools](./debugging.md)** documentation.
1173
1173
 
1174
- #### Cycle Detection Behavior
1174
+ ### Quick Summary
1175
1175
 
1176
- By default, cycles are detected and an error is thrown. You can configure the behavior using `reactiveOptions.cycleHandling`:
1177
-
1178
- ```typescript
1179
- import { reactiveOptions } from 'mutts/reactive'
1180
-
1181
- // Options: 'throw' (default), 'warn', or 'break'
1182
- reactiveOptions.cycleHandling = 'warn' // Warn instead of throwing
1183
- reactiveOptions.cycleHandling = 'break' // Silently break the cycle
1184
- ```
1185
-
1186
- **Cycle handling modes:**
1187
-
1188
- - **`'throw'`** (default): Throws a `ReactiveError` with a detailed cycle path when a cycle is detected
1189
- - **`'warn'`**: Logs a warning message with the cycle path but continues execution (breaks the cycle)
1190
- - **`'break'`**: Silently breaks the cycle without any message
1191
- - **`'strict'`**: Checks the graph BEFORE execution. Prevents infinite loops at the source (higher overhead).
1192
-
1193
- #### Cycle Error Messages
1194
-
1195
- When a cycle is detected, the error message includes the full cycle path showing which effects form the cycle:
1196
-
1197
- ```typescript
1198
- const state = reactive({ a: 0, b: 0, c: 0 })
1199
-
1200
- effect(() => {
1201
- state.b = state.a + 1 // Effect A
1202
- })
1203
-
1204
- effect(() => {
1205
- state.c = state.b + 1 // Effect B
1206
- })
1207
-
1208
- // This will throw with a detailed cycle path
1209
- try {
1210
- effect(() => {
1211
- state.a = state.c + 1 // Effect C - creates cycle: A → B → C → A
1212
- })
1213
- } catch (e) {
1214
- console.error(e.message)
1215
- // "[reactive] Cycle detected: effectA → effectB → effectC → effectA"
1216
- }
1217
- ```
1218
-
1219
- The cycle path shows the sequence of effects that form the circular dependency, making it easier to identify and fix the issue.
1220
-
1221
- #### Common Cycle Patterns
1222
-
1223
- **Direct cycle:**
1224
- ```typescript
1225
- const state = reactive({ a: 0, b: 0 })
1226
-
1227
- effect(() => {
1228
- state.b = state.a + 1 // Effect A
1229
- })
1230
-
1231
- effect(() => {
1232
- state.a = state.b + 1 // Effect B - creates cycle: A → B → A
1233
- })
1234
- ```
1235
-
1236
- **Indirect cycle:**
1237
- ```typescript
1238
- const state = reactive({ a: 0, b: 0, c: 0 })
1239
-
1240
- effect(() => {
1241
- state.b = state.a + 1 // Effect A
1242
- })
1243
-
1244
- effect(() => {
1245
- state.c = state.b + 1 // Effect B
1246
- })
1247
-
1248
- effect(() => {
1249
- state.a = state.c + 1 // Effect C - creates cycle: A → B → C → A
1250
- })
1251
- ```
1252
-
1253
- #### Preventing Cycles
1254
-
1255
- To avoid cycles, consider:
1256
-
1257
- 1. **Separate read and write effects**: Don't have an effect that both reads and writes the same reactive properties
1258
- 2. **Use `untracked()`**: For operations that shouldn't create dependencies
1259
- 3. **Use `atomic()`**: To batch operations and prevent intermediate triggers
1260
- 4. **Restructure logic**: Break circular dependencies by introducing intermediate state or computed values
1261
-
1262
- **Example - Using `untracked()` to break cycles:**
1263
- ```typescript
1264
- const state = reactive({ count: 0 })
1265
-
1266
- effect(() => {
1267
- // Read count
1268
- const current = state.count
1269
-
1270
- // Write to count without creating a dependency cycle
1271
- untracked(() => {
1272
- if (current < 10) {
1273
- state.count = current + 1
1274
- }
1275
- })
1276
- })
1277
- ```
1278
-
1279
- **Note:** Self-loops (an effect reading and writing the same property, like `obj.prop++`) are automatically ignored and do not create dependency relationships or cycles.
1176
+ - **Cycle Detection**: Automatically catch circular dependencies via `reactiveOptions.cycleHandling`.
1177
+ - **Memoization Discrepancy**: Detect "missing dependencies" by running computations twice during development using `reactiveOptions.onMemoizationDiscrepancy`.
1178
+ - **Global Hooks**: Use `reactiveOptions.touched`, `enter`, and `leave` to observe system activity.
1280
1179
 
@@ -7,22 +7,22 @@
7
7
  - [5-Minute Quick Start](#5-minute-quick-start)
8
8
  - [Core API](#core-api)
9
9
  - [Effect System](#effect-system)
10
- - [Atomic Operations](#atomic-operations)
11
- - [Advanced Effects](#advanced-effects)
12
- - [Evolution Tracking](#evolution-tracking)
13
- - [Prototype Chains and Pure Objects](#prototype-chains-and-pure-objects)
14
- - [Recursive Touching](#recursive-touching)
15
- - [Why Not Deep Watching?](#why-not-deep-watching)
16
- - [Collections](#collections)
17
- - [Register](#register)
18
- - [Class Reactivity](#class-reactivity)
19
- - [Non-Reactive System](#non-reactive-system)
20
- - [Array Mapping](#array-mapping)
21
- - [Projection](#projection)
22
- - [Record Organization](#record-organization)
23
- - [Memoization](#memoization)
24
- - [Debugging and Development](#debugging-and-development)
25
- - [Cycle Detection](#cycle-detection)
10
+ - [Atomic Operations](./advanced.md#atomic-operations)
11
+ - [Advanced Effects](./advanced.md#advanced-effects)
12
+ - [Evolution Tracking](./advanced.md#evolution-tracking)
13
+ - [Prototype Chains and Pure Objects](./advanced.md#prototype-chains-and-pure-objects)
14
+ - [Recursive Touching](./advanced.md#recursive-touching)
15
+ - [Why Not Deep Watching?](./advanced.md#why-not-deep-watching)
16
+ - [Collections](./advanced.md#collections)
17
+ - [Register](./advanced.md#register)
18
+ - [Class Reactivity](./advanced.md#class-reactivity)
19
+ - [Non-Reactive System](./advanced.md#non-reactive-system)
20
+ - [Array Mapping](./advanced.md#array-mapping)
21
+ - [Projection](./advanced.md#projection)
22
+ - [Record Organization](./advanced.md#record-organization)
23
+ - [Memoization](./advanced.md#memoization)
24
+ - [Debugging and Development](./advanced.md#debugging-and-development)
25
+ - [Cycle Detection](./advanced.md#cycle-detection)
26
26
 
27
27
  ## Introduction
28
28
 
@@ -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
@@ -19,3 +19,11 @@ The Mutts Reactive System documentation has been split into focused sections for
19
19
  * **[Prototype Chains](./reactive/advanced.md#prototype-chains-and-pure-objects)**: Advanced inheritance patterns
20
20
  * **[Memoization](./reactive/advanced.md#memoization)**: Caching strategies
21
21
  * **[Debugging](./reactive/advanced.md#debugging-and-development)**: Cycle detection and troubleshooting
22
+
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
28
+
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.4",
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
@@ -117,7 +117,7 @@ export function Destroyable<
117
117
  base = undefined
118
118
  }
119
119
  if (!base) {
120
- base = class {} as T
120
+ base = class { } as T
121
121
  }
122
122
 
123
123
  return class Destroyable extends (base as T) {
@@ -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
@@ -139,7 +139,7 @@ export function Destroyable<
139
139
  return Destroyable.destructors.has(obj)
140
140
  }
141
141
 
142
- declare [forwardProperties]: PropertyKey[]
142
+ [forwardProperties]!: PropertyKey[]
143
143
  readonly [allocatedValues]: Allocated
144
144
  constructor(...args: any[]) {
145
145
  super(...args)
@@ -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
+ }
@@ -10,13 +10,12 @@ export const native = Symbol('native')
10
10
  const isArray = Array.isArray
11
11
  Array.isArray = ((value: any) =>
12
12
  isArray(value) ||
13
- // biome-ignore lint/suspicious/useIsArray: We are defining it
14
13
  (value &&
15
14
  typeof value === 'object' &&
16
15
  prototypeForwarding in value &&
17
16
  Array.isArray(value[prototypeForwarding]))) as any
18
17
  export class ReactiveBaseArray {
19
- declare readonly [native]: any[]
18
+ readonly [native]!: any[]
20
19
 
21
20
  // Safe array access with negative indices
22
21
  at(index: number): any {
@@ -293,7 +292,6 @@ export class ReactiveArray extends Indexable(ReactiveBaseArray, {
293
292
  }
294
293
  },
295
294
  }) {
296
- declare length: number
297
295
  constructor(original: any[]) {
298
296
  super()
299
297
  Object.defineProperties(this, {
@@ -364,8 +362,8 @@ export class ReactiveArray extends Indexable(ReactiveBaseArray, {
364
362
  deleteCount === items.length
365
363
  ? range(start, start + deleteCount)
366
364
  : range(start, oldLength + Math.max(items.length - deleteCount, 0), {
367
- length: true,
368
- })
365
+ length: true,
366
+ })
369
367
  )
370
368
  }
371
369
  }