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.
- package/README.md +2 -1
- 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-GRBSx0mB.js → index-CDCOjzTy.js} +543 -495
- package/dist/chunks/index-CDCOjzTy.js.map +1 -0
- package/dist/chunks/{index-79Kk8D6e.esm.js → index-DiP0RXoZ.esm.js} +452 -404
- 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 +50 -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 +54 -1
- package/dist/reactive.esm.js +3 -3
- package/dist/reactive.js +6 -4
- package/dist/reactive.js.map +1 -1
- 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/core.md +16 -16
- package/docs/reactive/debugging.md +158 -0
- package/docs/reactive.md +8 -0
- package/package.json +16 -66
- package/src/decorator.ts +11 -9
- package/src/destroyable.ts +5 -5
- package/src/index.ts +46 -0
- package/src/reactive/array.ts +3 -5
- package/src/reactive/change.ts +7 -3
- 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 +114 -17
- package/src/reactive/index.ts +3 -2
- package/src/reactive/interface.ts +10 -9
- package/src/reactive/map.ts +6 -6
- package/src/reactive/mapped.ts +2 -3
- package/src/reactive/memoize.ts +77 -31
- package/src/reactive/project.ts +103 -6
- package/src/reactive/proxy.ts +4 -4
- package/src/reactive/registry.ts +67 -0
- package/src/reactive/set.ts +6 -6
- package/src/reactive/tracking.ts +12 -41
- package/src/reactive/types.ts +59 -0
- package/src/reactive/zone.ts +1 -1
- package/src/std-decorators.ts +10 -10
- 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-79Kk8D6e.esm.js.map +0 -1
- package/dist/chunks/index-GRBSx0mB.js.map +0 -1
- /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
|
-
|
|
1170
|
+
The `mutts` reactive system includes built-in tools for troubleshooting complex dependency graphs and identifying performance bottlenecks.
|
|
1171
1171
|
|
|
1172
|
-
|
|
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
|
-
|
|
1174
|
+
### Quick Summary
|
|
1175
1175
|
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
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
|
|
package/docs/reactive/core.md
CHANGED
|
@@ -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.
|
|
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
|
@@ -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
|
-
|
|
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,
|
|
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/array.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
368
|
-
|
|
365
|
+
length: true,
|
|
366
|
+
})
|
|
369
367
|
)
|
|
370
368
|
}
|
|
371
369
|
}
|