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.
- package/README.md +1 -0
- package/dist/chunks/{_tslib-Mzh1rNsX.esm.js → _tslib-MCKDzsSq.esm.js} +2 -2
- package/dist/chunks/_tslib-MCKDzsSq.esm.js.map +1 -0
- package/dist/chunks/decorator-BGILvPtN.esm.js +627 -0
- package/dist/chunks/decorator-BGILvPtN.esm.js.map +1 -0
- package/dist/chunks/decorator-BQ2eBTCj.js +651 -0
- package/dist/chunks/decorator-BQ2eBTCj.js.map +1 -0
- package/dist/chunks/{index-Cvxdw6Ax.js → index-CDCOjzTy.js} +396 -500
- package/dist/chunks/index-CDCOjzTy.js.map +1 -0
- package/dist/chunks/{index-qiWwozOc.esm.js → index-DiP0RXoZ.esm.js} +301 -403
- package/dist/chunks/index-DiP0RXoZ.esm.js.map +1 -0
- package/dist/decorator.d.ts +3 -3
- package/dist/decorator.esm.js +1 -1
- package/dist/decorator.js +1 -1
- package/dist/destroyable.esm.js +4 -4
- package/dist/destroyable.esm.js.map +1 -1
- package/dist/destroyable.js +4 -4
- package/dist/destroyable.js.map +1 -1
- package/dist/devtools/panel.js.map +1 -1
- package/dist/eventful.esm.js +1 -1
- package/dist/index.esm.js +48 -3
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +48 -4
- package/dist/index.js.map +1 -1
- package/dist/mutts.umd.js +1 -1
- package/dist/mutts.umd.js.map +1 -1
- package/dist/mutts.umd.min.js +1 -1
- package/dist/mutts.umd.min.js.map +1 -1
- package/dist/reactive.d.ts +25 -0
- package/dist/reactive.esm.js +3 -3
- package/dist/reactive.js +4 -4
- package/dist/std-decorators.d.ts +1 -1
- package/dist/std-decorators.esm.js +10 -10
- package/dist/std-decorators.esm.js.map +1 -1
- package/dist/std-decorators.js +10 -10
- package/dist/std-decorators.js.map +1 -1
- package/docs/ai/manual.md +14 -95
- package/docs/reactive/advanced.md +6 -107
- package/docs/reactive/debugging.md +158 -0
- package/docs/reactive.md +6 -5
- package/package.json +16 -66
- package/src/decorator.ts +11 -9
- package/src/destroyable.ts +3 -3
- package/src/index.ts +46 -0
- package/src/reactive/change.ts +1 -1
- package/src/reactive/debug.ts +1 -1
- package/src/reactive/deep-touch.ts +1 -1
- package/src/reactive/deep-watch.ts +1 -1
- package/src/reactive/effect-context.ts +2 -2
- package/src/reactive/effects.ts +44 -16
- package/src/reactive/index.ts +1 -1
- package/src/reactive/interface.ts +9 -8
- package/src/reactive/memoize.ts +77 -31
- package/src/reactive/proxy.ts +4 -4
- package/src/reactive/registry.ts +67 -0
- package/src/reactive/tracking.ts +12 -41
- package/src/reactive/types.ts +37 -0
- package/src/std-decorators.ts +9 -9
- package/src/utils.ts +141 -0
- package/dist/chunks/_tslib-Mzh1rNsX.esm.js.map +0 -1
- package/dist/chunks/decorator-DLvrD0UF.js +0 -265
- package/dist/chunks/decorator-DLvrD0UF.js.map +0 -1
- package/dist/chunks/decorator-DqiszP7i.esm.js +0 -253
- package/dist/chunks/decorator-DqiszP7i.esm.js.map +0 -1
- package/dist/chunks/index-Cvxdw6Ax.js.map +0 -1
- 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
|
|
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
|
-
|
|
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
|
-
"main": "
|
|
6
|
-
"module": "
|
|
7
|
-
"types": "
|
|
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
|
-
"./
|
|
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": "
|
|
84
|
-
"test:coverage": "
|
|
85
|
-
"test:coverage:watch": "
|
|
86
|
-
"test:legacy": "TSCONFIG=tsconfig.legacy.json
|
|
87
|
-
"test:modern": "TSCONFIG=tsconfig.modern.json
|
|
88
|
-
"test:profile": "RUN_PROFILING=1
|
|
89
|
-
"test:profile:benchmark": "RUN_PROFILING=1
|
|
90
|
-
"test:profile:detailed": "RUN_PROFILING=1 node --prof node_modules
|
|
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
|
package/src/destroyable.ts
CHANGED
|
@@ -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,
|
|
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
|
+
}
|
package/src/reactive/change.ts
CHANGED
|
@@ -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 './
|
|
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>()
|
package/src/reactive/debug.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - Provides graph data for tooling (DevTools panel, etc.)
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { effectParent, effectToReactiveObjects, getRoot } from './
|
|
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 './
|
|
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 './
|
|
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 './
|
|
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
|
-
|
|
80
|
+
|
|
81
81
|
if (getRoot(effect) === getRoot(getActiveEffect())) return fn()
|
|
82
82
|
stack.unshift(effect)
|
|
83
83
|
try {
|
package/src/reactive/effects.ts
CHANGED
|
@@ -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 './
|
|
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
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
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
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
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 =
|
|
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 = ()
|
|
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
|
|
package/src/reactive/index.ts
CHANGED
|
@@ -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 './
|
|
55
|
+
import { effectToReactiveObjects, watchers } from './registry'
|
|
56
56
|
|
|
57
57
|
// Register native collection types to use specialized reactive wrappers
|
|
58
58
|
registerNativeReactivity(WeakMap, ReactiveWeakMap)
|