mutts 1.0.9 → 1.0.11
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 +60 -35
- package/dist/browser.cjs +1333 -1760
- package/dist/browser.cjs.map +1 -1
- package/dist/browser.d.ts +2 -1392
- package/dist/browser.dev.cjs +114 -0
- package/dist/browser.dev.cjs.map +1 -0
- package/dist/browser.dev.d.ts +2 -0
- package/dist/browser.dev.esm.js +5 -0
- package/dist/browser.dev.esm.js.map +1 -0
- package/dist/browser.esm.js +18 -97
- package/dist/browser.esm.js.map +1 -1
- package/dist/chunks/{async-browser-lvzLOCgk.cjs → async-browser-Dgr5CreQ.cjs} +16 -95
- package/dist/chunks/async-browser-Dgr5CreQ.cjs.map +1 -0
- package/dist/chunks/{async-node-C3DeIb0y.cjs → async-node-3PrbVAbB.cjs} +3 -1
- package/dist/chunks/async-node-3PrbVAbB.cjs.map +1 -0
- package/dist/chunks/index-Sf74wXTV.esm.js +2577 -0
- package/dist/chunks/index-Sf74wXTV.esm.js.map +1 -0
- package/dist/chunks/node-Bo7WU5S2.esm.js +96 -0
- package/dist/chunks/node-Bo7WU5S2.esm.js.map +1 -0
- package/dist/chunks/{index-VTO-b2vR.cjs → proxy-Cc79Lrzj.cjs} +2593 -3216
- package/dist/chunks/proxy-Cc79Lrzj.cjs.map +1 -0
- package/dist/chunks/{index-CtA2AWl3.esm.js → proxy-D2C49sXH.esm.js} +2570 -3174
- package/dist/chunks/proxy-D2C49sXH.esm.js.map +1 -0
- package/dist/debug.cjs +987 -28
- package/dist/debug.cjs.map +1 -1
- package/dist/debug.d.ts +37 -38
- package/dist/debug.esm.js +963 -1
- package/dist/debug.esm.js.map +1 -1
- package/dist/devtools/manifest.json +1 -1
- package/dist/devtools/panel.html +1 -1
- package/dist/devtools/panel.js +107 -94
- package/dist/devtools/panel.js.map +1 -1
- package/dist/index.d.ts +1322 -1
- package/dist/mutts.umd.js +6647 -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/node.cjs +69 -63
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.ts +2 -2
- package/dist/node.dev.cjs +114 -0
- package/dist/node.dev.cjs.map +1 -0
- package/dist/node.dev.d.ts +2 -0
- package/dist/node.dev.esm.js +6 -0
- package/dist/node.dev.esm.js.map +1 -0
- package/dist/node.esm.js +4 -97
- package/dist/node.esm.js.map +1 -1
- package/dist/{types-DaHFfhlN.d.ts → types-Bx2PhORg.d.ts} +134 -88
- package/docs/ai/api-reference.md +11 -32
- package/docs/ai/manual.md +297 -239
- package/docs/reactive/advanced.md +318 -9
- package/docs/reactive/attend.md +2 -4
- package/docs/reactive/collections.md +22 -187
- package/docs/reactive/core.md +223 -131
- package/docs/reactive/debugging.md +119 -12
- package/docs/reactive/error-handling.md +10 -10
- package/docs/reactive/resource.md +125 -0
- package/docs/reactive.md +3 -4
- package/docs/utils.md +70 -0
- package/docs/zone.md +1 -1
- package/package.json +76 -38
- package/dist/chunks/async-browser-lvzLOCgk.cjs.map +0 -1
- package/dist/chunks/async-node-C3DeIb0y.cjs.map +0 -1
- package/dist/chunks/index-2vea86wD.esm.js +0 -3011
- package/dist/chunks/index-2vea86wD.esm.js.map +0 -1
- package/dist/chunks/index-CtA2AWl3.esm.js.map +0 -1
- package/dist/chunks/index-VTO-b2vR.cjs.map +0 -1
- package/dist/debug/debug.d.ts +0 -122
- package/dist/debug/debug.d.ts.map +0 -1
- package/dist/debug/index.d.ts +0 -4
- package/dist/debug/index.d.ts.map +0 -1
- package/dist/debug/lineage-panel.d.ts +0 -5
- package/dist/debug/lineage-panel.d.ts.map +0 -1
- package/dist/debug/lineage.d.ts +0 -79
- package/dist/debug/lineage.d.ts.map +0 -1
- package/dist/src/async/browser.d.ts +0 -2
- package/dist/src/async/browser.d.ts.map +0 -1
- package/dist/src/async/index.d.ts +0 -19
- package/dist/src/async/index.d.ts.map +0 -1
- package/dist/src/async/node.d.ts +0 -2
- package/dist/src/async/node.d.ts.map +0 -1
- package/dist/src/decorator.d.ts +0 -106
- package/dist/src/decorator.d.ts.map +0 -1
- package/dist/src/destroyable.d.ts +0 -87
- package/dist/src/destroyable.d.ts.map +0 -1
- package/dist/src/entry-browser.d.ts +0 -3
- package/dist/src/entry-browser.d.ts.map +0 -1
- package/dist/src/entry-node.d.ts +0 -3
- package/dist/src/entry-node.d.ts.map +0 -1
- package/dist/src/eventful.d.ts +0 -20
- package/dist/src/eventful.d.ts.map +0 -1
- package/dist/src/flavored.d.ts +0 -33
- package/dist/src/flavored.d.ts.map +0 -1
- package/dist/src/index.d.ts +0 -14
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/indexable.d.ts +0 -243
- package/dist/src/indexable.d.ts.map +0 -1
- package/dist/src/introspection.d.ts +0 -27
- package/dist/src/introspection.d.ts.map +0 -1
- package/dist/src/iterableWeak.d.ts +0 -53
- package/dist/src/iterableWeak.d.ts.map +0 -1
- package/dist/src/mixins.d.ts +0 -25
- package/dist/src/mixins.d.ts.map +0 -1
- package/dist/src/promiseChain.d.ts +0 -20
- package/dist/src/promiseChain.d.ts.map +0 -1
- package/dist/src/reactive/array.d.ts +0 -48
- package/dist/src/reactive/array.d.ts.map +0 -1
- package/dist/src/reactive/buffer.d.ts +0 -120
- package/dist/src/reactive/buffer.d.ts.map +0 -1
- package/dist/src/reactive/change.d.ts +0 -29
- package/dist/src/reactive/change.d.ts.map +0 -1
- package/dist/src/reactive/deep-touch.d.ts +0 -28
- package/dist/src/reactive/deep-touch.d.ts.map +0 -1
- package/dist/src/reactive/deep-watch-state.d.ts +0 -25
- package/dist/src/reactive/deep-watch-state.d.ts.map +0 -1
- package/dist/src/reactive/deep-watch.d.ts +0 -20
- package/dist/src/reactive/deep-watch.d.ts.map +0 -1
- package/dist/src/reactive/describe.d.ts +0 -12
- package/dist/src/reactive/describe.d.ts.map +0 -1
- package/dist/src/reactive/effect-context.d.ts +0 -34
- package/dist/src/reactive/effect-context.d.ts.map +0 -1
- package/dist/src/reactive/effects.d.ts +0 -164
- package/dist/src/reactive/effects.d.ts.map +0 -1
- package/dist/src/reactive/index.d.ts +0 -19
- package/dist/src/reactive/index.d.ts.map +0 -1
- package/dist/src/reactive/map.d.ts +0 -28
- package/dist/src/reactive/map.d.ts.map +0 -1
- package/dist/src/reactive/memoize.d.ts +0 -28
- package/dist/src/reactive/memoize.d.ts.map +0 -1
- package/dist/src/reactive/non-reactive-state.d.ts +0 -9
- package/dist/src/reactive/non-reactive-state.d.ts.map +0 -1
- package/dist/src/reactive/non-reactive.d.ts +0 -11
- package/dist/src/reactive/non-reactive.d.ts.map +0 -1
- package/dist/src/reactive/project.d.ts +0 -40
- package/dist/src/reactive/project.d.ts.map +0 -1
- package/dist/src/reactive/proxy-state.d.ts +0 -8
- package/dist/src/reactive/proxy-state.d.ts.map +0 -1
- package/dist/src/reactive/proxy.d.ts +0 -23
- package/dist/src/reactive/proxy.d.ts.map +0 -1
- package/dist/src/reactive/record.d.ts +0 -115
- package/dist/src/reactive/record.d.ts.map +0 -1
- package/dist/src/reactive/register.d.ts +0 -125
- package/dist/src/reactive/register.d.ts.map +0 -1
- package/dist/src/reactive/registry.d.ts +0 -21
- package/dist/src/reactive/registry.d.ts.map +0 -1
- package/dist/src/reactive/set.d.ts +0 -26
- package/dist/src/reactive/set.d.ts.map +0 -1
- package/dist/src/reactive/tracking.d.ts +0 -7
- package/dist/src/reactive/tracking.d.ts.map +0 -1
- package/dist/src/reactive/types.d.ts +0 -424
- package/dist/src/reactive/types.d.ts.map +0 -1
- package/dist/src/reactive/watch.d.ts +0 -48
- package/dist/src/reactive/watch.d.ts.map +0 -1
- package/dist/src/std-decorators.d.ts +0 -45
- package/dist/src/std-decorators.d.ts.map +0 -1
- package/dist/src/utils.d.ts +0 -49
- package/dist/src/utils.d.ts.map +0 -1
- package/dist/src/zone.d.ts +0 -40
- package/dist/src/zone.d.ts.map +0 -1
- package/docs/reactive/describe.md +0 -85
- package/docs/reactive/project.md +0 -93
- package/docs/reactive/scan.md +0 -293
- package/src/async/browser.ts +0 -323
- package/src/async/index.ts +0 -27
- package/src/async/node.ts +0 -92
- package/src/decorator.ts +0 -272
- package/src/destroyable.ts +0 -199
- package/src/entry-browser.ts +0 -5
- package/src/entry-node.ts +0 -5
- package/src/eventful.ts +0 -110
- package/src/flavored.ts +0 -106
- package/src/index.d.ts +0 -12
- package/src/index.ts +0 -64
- package/src/indexable.ts +0 -526
- package/src/introspection.ts +0 -59
- package/src/iterableWeak.ts +0 -233
- package/src/mixins.ts +0 -123
- package/src/promiseChain.ts +0 -110
- package/src/reactive/array.ts +0 -500
- package/src/reactive/buffer.ts +0 -328
- package/src/reactive/change.ts +0 -131
- package/src/reactive/deep-touch.ts +0 -273
- package/src/reactive/deep-watch-state.ts +0 -82
- package/src/reactive/deep-watch.ts +0 -171
- package/src/reactive/describe.ts +0 -39
- package/src/reactive/effect-context.ts +0 -83
- package/src/reactive/effects.ts +0 -1434
- package/src/reactive/index.ts +0 -72
- package/src/reactive/map.ts +0 -142
- package/src/reactive/memoize.ts +0 -186
- package/src/reactive/non-reactive-state.ts +0 -49
- package/src/reactive/non-reactive.ts +0 -43
- package/src/reactive/project.md +0 -107
- package/src/reactive/project.ts +0 -430
- package/src/reactive/proxy-state.ts +0 -27
- package/src/reactive/proxy.ts +0 -282
- package/src/reactive/record.ts +0 -181
- package/src/reactive/register.ts +0 -538
- package/src/reactive/registry.ts +0 -72
- package/src/reactive/set.ts +0 -117
- package/src/reactive/tracking.ts +0 -41
- package/src/reactive/types.ts +0 -520
- package/src/reactive/watch.ts +0 -180
- package/src/std-decorators.ts +0 -256
- package/src/utils.ts +0 -300
- package/src/zone.ts +0 -142
|
@@ -7,10 +7,10 @@ The `mutts` reactive system provides several built-in tools to help track down s
|
|
|
7
7
|
|
|
8
8
|
## The `reactiveOptions` Reference
|
|
9
9
|
|
|
10
|
-
The `reactiveOptions` object (exported from `mutts
|
|
10
|
+
The `reactiveOptions` object (exported from `mutts`) allows you to hook into the reactive system's internals.
|
|
11
11
|
|
|
12
12
|
```typescript
|
|
13
|
-
import { reactiveOptions } from 'mutts
|
|
13
|
+
import { reactiveOptions } from 'mutts';
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
### Lifecycle Hooks
|
|
@@ -40,15 +40,40 @@ You can control how cycles are handled via `reactiveOptions.cycleHandling`:
|
|
|
40
40
|
- **`'debug'`**: Full diagnostic mode with transitive closures and topological sorting. Provides detailed cycle path reporting.
|
|
41
41
|
|
|
42
42
|
### Topological vs. Flat Mode Detection
|
|
43
|
+
- **Detection**: Cycles are detected when the execution depth exceeds `maxEffectChain` (default 100).
|
|
44
|
+
- **Diagnostics**: The resulting `ReactiveError` includes a `trace` property (the recent execution sequence) and attempts to identify a repeating `cycle`.
|
|
45
|
+
- **Recommendation**: Use this mode only for production to minimize performance overhead.
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
| :--- | :--- | :--- | :--- |
|
|
46
|
-
| **Debug** | `'debug'` or `'development'` | **Mathematical**: Analyzes the dependency graph. | `CYCLE_DETECTED` |
|
|
47
|
-
| **Production** | `'production'` (Default) | **Heuristic**: Counts executions per batch. | `MAX_REACTION_EXCEEDED` |
|
|
47
|
+
### Cycle Handling Modes
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
You can configure how the system handles cycles via `reactiveOptions.cycleHandling`:
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
| Mode | Detection Timing | Cycle Information | Performance |
|
|
52
|
+
|------|-----------------|-------------------|-------------|
|
|
53
|
+
| `'production'` | Late (Heuristic) | Trace of last N effects | Fastest |
|
|
54
|
+
| `'development'` | Eager (On edge) | Exact path (DFS) | Moderate |
|
|
55
|
+
| `'debug'` | Structural | Transitive closures | Slowest |
|
|
56
|
+
|
|
57
|
+
#### Finding Cycle Information
|
|
58
|
+
|
|
59
|
+
When a cycle is detected, a `ReactiveError` is thrown. You can find detailed path information in the error object:
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
try {
|
|
63
|
+
atom(() => { /* cycle logic */ });
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (error.code === 'Cycle detected') {
|
|
66
|
+
console.log('Cycle Path:', error.cycle.join(' → '));
|
|
67
|
+
console.log('Trigger Chain:', error.causalChain);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
- **`error.cycle`**: An array of effect names forming the cycle.
|
|
73
|
+
- **`error.causalChain`**: The sequence of triggers that led to the current effect.
|
|
74
|
+
- **`error.lineage`**: The creation stack of the effect (available in `development` or `debug` modes).
|
|
75
|
+
|
|
76
|
+
### Memoization Discrepancy Detection
|
|
52
77
|
|
|
53
78
|
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.
|
|
54
79
|
|
|
@@ -125,10 +150,12 @@ When the reactive system encounters a critical failure (like a cycle or max dept
|
|
|
125
150
|
### `ReactiveErrorCode`
|
|
126
151
|
|
|
127
152
|
Always check `error.debugInfo.code` to identify the failure type:
|
|
128
|
-
- `
|
|
129
|
-
- `
|
|
130
|
-
- `
|
|
131
|
-
- `
|
|
153
|
+
- `Cycle detected`: A circular dependency was found.
|
|
154
|
+
- `Max depth exceeded`: The synchronous effect chain reached `maxEffectChain`.
|
|
155
|
+
- `Max reaction exceeded`: An effect was triggered too many times in a single batch.
|
|
156
|
+
- `Write in computed`: An attempt was made to modify reactive state inside a `memoize` function.
|
|
157
|
+
- `Tracking error`: Internal inconsistency detected in the active dependency stack.
|
|
158
|
+
- `Broken effects`: The system has entered an unrecoverable "broken" state after a root-level panic. Call `reset()` to recover.
|
|
132
159
|
|
|
133
160
|
### Rich Debug Info
|
|
134
161
|
|
|
@@ -136,6 +163,7 @@ The `debugInfo` property on `ReactiveError` includes:
|
|
|
136
163
|
- **`causalChain`**: A string array describing the logical path of modifications leading to the error.
|
|
137
164
|
- **`creationStack`**: The stack trace of where the effect was originally created, helping you locate the source in your code.
|
|
138
165
|
- **`cycle`**: (For `CYCLE_DETECTED`) The names of the effects that form the loop.
|
|
166
|
+
- **`lineage`**: Detailed source-to-sink dependency traces for debugging (requires `lineages` introspection).
|
|
139
167
|
|
|
140
168
|
## Best Practices for Debugging
|
|
141
169
|
|
|
@@ -147,8 +175,15 @@ Always provide a name for your effects to make debug logs and error messages rea
|
|
|
147
175
|
effect(() => {
|
|
148
176
|
// ...
|
|
149
177
|
}, { name: 'UpdateSidebarCounter' });
|
|
178
|
+
|
|
179
|
+
// Or
|
|
180
|
+
|
|
181
|
+
effect.named('UpdateSidebarCounter')(() => {
|
|
182
|
+
// ...
|
|
183
|
+
});
|
|
150
184
|
```
|
|
151
185
|
|
|
186
|
+
|
|
152
187
|
### Activation & Deactivation
|
|
153
188
|
|
|
154
189
|
Since these are runtime options, you can toggle them based on your environment:
|
|
@@ -164,3 +199,75 @@ if (process.env.NODE_ENV === 'development') {
|
|
|
164
199
|
reactiveOptions.cycleHandling = 'production';
|
|
165
200
|
}
|
|
166
201
|
```
|
|
202
|
+
|
|
203
|
+
## Advanced Debugging (`mutts/debug`)
|
|
204
|
+
|
|
205
|
+
For a deeper look into the reactivity graph and execution flow, you can import the `mutts/debug` module. Simply importing it enables several background tracking features.
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
import 'mutts/debug';
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Reaction Reasons (`CleanupReason`)
|
|
212
|
+
|
|
213
|
+
When an effect or watcher re-runs, it receives a `reaction` property (in `EffectAccess`) that describes *why* it was triggered. This is also passed to the `cleanup` function.
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
effect(({ reaction }) => {
|
|
217
|
+
if (reaction && typeof reaction === 'object') {
|
|
218
|
+
if (reaction.type === 'propChange') {
|
|
219
|
+
console.log('Triggered by:', reaction.triggers.map(t => t.evolution.prop));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return (reason) => {
|
|
224
|
+
// reason is also a CleanupReason
|
|
225
|
+
if (reason?.type === 'stopped') console.log('Effect manually stopped');
|
|
226
|
+
};
|
|
227
|
+
});
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
#### `formatCleanupReason`
|
|
231
|
+
|
|
232
|
+
A built-in utility to turn a `CleanupReason` into an inspectable console log array.
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
import { formatCleanupReason } from 'mutts';
|
|
236
|
+
|
|
237
|
+
effect(()=> {
|
|
238
|
+
...
|
|
239
|
+
return (reason)=> {
|
|
240
|
+
if (reason) console.log(...formatCleanupReason(reason));
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Lineage Tracking
|
|
246
|
+
|
|
247
|
+
Lineage tracking allows you to see the "causal path" of an effect—not just the current stack trace, but the stack traces of all parent effects that created the current execution.
|
|
248
|
+
|
|
249
|
+
When an effect is created, it is assigned a `lineage` property that contains the stack traces of all parent effects that lead to its creation.
|
|
250
|
+
|
|
251
|
+
- **`logLineage()`**: Prints a formatted, interactive tree of the current effect's lineage to the console.
|
|
252
|
+
- **`captureLineage()`**: Captures the current lineage as a structured object.
|
|
253
|
+
|
|
254
|
+
#### Lineage Options
|
|
255
|
+
|
|
256
|
+
You can control how lineages are captured via `reactiveOptions.introspection.gatherReasons.lineages`:
|
|
257
|
+
- `'none'`: Disable lineage capture.
|
|
258
|
+
- `'touch'`: Capture lineage when a property is accessed (default).
|
|
259
|
+
- `'dependency'`: Capture lineage when a dependency is recorded.
|
|
260
|
+
- `'both'`: Capture both.
|
|
261
|
+
|
|
262
|
+
### The `__MUTTS_DEBUG__` Global
|
|
263
|
+
|
|
264
|
+
When `mutts/debug` is active (or after calling `enableDevTools()`), a global `__MUTTS_DEBUG__` object is exposed in the environment (Node.js `global` or Browser `window`).
|
|
265
|
+
|
|
266
|
+
This object provides low-level access to the graph, lineage capture, and renaming utilities:
|
|
267
|
+
- `__MUTTS_DEBUG__.getGraph()`: Returns the full reactivity graph.
|
|
268
|
+
- `__MUTTS_DEBUG__.logLineage()`: logs the current lineage.
|
|
269
|
+
- `__MUTTS_DEBUG__.browserLineage`: captures lineage for the DevTools panel.
|
|
270
|
+
|
|
271
|
+
### Custom DevTools Formatters
|
|
272
|
+
|
|
273
|
+
`mutts/debug` automatically registers [Custom Formatters](https://bit.ly/chrome-extension-custom-formatters) in Chrome. This makes lineage objects and reactive proxies appear as clean, structured trees in the console instead of opaque Proxy objects.
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
# Effect Error Handling
|
|
2
2
|
|
|
3
|
-
The `
|
|
3
|
+
The `caught` function allows you to catch and handle errors within reactive effects.
|
|
4
4
|
|
|
5
5
|
## Basic Usage
|
|
6
6
|
|
|
7
|
-
Register an error handler inside an effect using `
|
|
7
|
+
Register an error handler inside an effect using `caught`:
|
|
8
8
|
|
|
9
9
|
```typescript
|
|
10
|
-
import { effect,
|
|
10
|
+
import { effect, caught, reactive } from 'mutts'
|
|
11
11
|
|
|
12
12
|
const state = reactive({ value: 0 })
|
|
13
13
|
|
|
14
14
|
effect(() => {
|
|
15
|
-
|
|
15
|
+
caught((error) => {
|
|
16
16
|
console.error('Effect failed:', error)
|
|
17
17
|
})
|
|
18
18
|
|
|
@@ -28,7 +28,7 @@ You can register multiple handlers. They are tried in order until one succeeds:
|
|
|
28
28
|
```typescript
|
|
29
29
|
effect(() => {
|
|
30
30
|
// First handler - try to recover
|
|
31
|
-
|
|
31
|
+
caught((error) => {
|
|
32
32
|
if (error.message === 'Retryable') {
|
|
33
33
|
retryOperation()
|
|
34
34
|
return // Success - stops here
|
|
@@ -37,7 +37,7 @@ effect(() => {
|
|
|
37
37
|
})
|
|
38
38
|
|
|
39
39
|
// Second handler - log and continue
|
|
40
|
-
|
|
40
|
+
caught((error) => {
|
|
41
41
|
console.log('Operation failed:', error)
|
|
42
42
|
})
|
|
43
43
|
})
|
|
@@ -50,7 +50,7 @@ Errors in child effects propagate to parent effects:
|
|
|
50
50
|
```typescript
|
|
51
51
|
effect(() => {
|
|
52
52
|
// Parent catches child's error
|
|
53
|
-
|
|
53
|
+
caught((error) => {
|
|
54
54
|
console.log('Child failed:', error.message)
|
|
55
55
|
})
|
|
56
56
|
|
|
@@ -69,7 +69,7 @@ Handlers can return cleanup functions:
|
|
|
69
69
|
|
|
70
70
|
```typescript
|
|
71
71
|
effect(() => {
|
|
72
|
-
|
|
72
|
+
caught((error) => {
|
|
73
73
|
console.log('Handling error:', error)
|
|
74
74
|
|
|
75
75
|
return () => {
|
|
@@ -82,7 +82,7 @@ effect(() => {
|
|
|
82
82
|
|
|
83
83
|
## API
|
|
84
84
|
|
|
85
|
-
### `
|
|
85
|
+
### `caught(handler)`
|
|
86
86
|
|
|
87
87
|
Registers an error handler for the current effect.
|
|
88
88
|
|
|
@@ -118,4 +118,4 @@ Parent's handlers try
|
|
|
118
118
|
|
|
119
119
|
- Handlers must be registered **before** the code that might throw
|
|
120
120
|
- Handlers are cleared on each effect re-run (re-register if needed)
|
|
121
|
-
- Errors in async effects (Promises) are not caught by `
|
|
121
|
+
- Errors in async effects (Promises) are not caught by `caught` - use `.catch()` on the Promise
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Reactive Resource (`resource`)
|
|
2
|
+
|
|
3
|
+
The `resource` utility creates a reactive object that automatically tracks the state of an asynchronous operation (loading, value, error). It is designed to simplify data fetching and async state management in a reactive environment.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
`resource` wraps an async function (the "fetcher") and returns a reactive object that:
|
|
8
|
+
- **Tracks dependencies**: Automatically re-runs the fetcher when reactive dependencies accessed within it change.
|
|
9
|
+
- **Manages state**: Provides `loading`, `value`, `error`, and `latest` properties that update automatically.
|
|
10
|
+
- **Handles race conditions**: Ensures that only the result of the latest fetch is applied, discarding results from stale requests.
|
|
11
|
+
- **Supports manual reload**: Exposes a `reload()` method to force a refresh.
|
|
12
|
+
|
|
13
|
+
## API
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
function resource<T>(
|
|
17
|
+
fetcher: (dep: EffectAccess) => Promise<T> | T,
|
|
18
|
+
options?: { initialValue?: T }
|
|
19
|
+
): Resource<T>
|
|
20
|
+
|
|
21
|
+
interface Resource<T> {
|
|
22
|
+
value: T | undefined // The current successful value
|
|
23
|
+
loading: boolean // True if a fetch is in progress
|
|
24
|
+
error: any // Error from the last failed fetch
|
|
25
|
+
latest: T | undefined // The latest successful value (preserved during loading)
|
|
26
|
+
reload: () => void // Function to manually trigger a re-fetch
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Parameters
|
|
31
|
+
|
|
32
|
+
- **`fetcher`**: A function that returns a value `T` or a `Promise<T>`. It receives an `EffectAccess` object, allowing for dependency tracking control (e.g., `dep.tracked()`). Reactive properties accessed synchronously or within tracked scopes are dependencies.
|
|
33
|
+
- **`options`**: Optional configuration object.
|
|
34
|
+
- `initialValue`: Initial value for `value` and `latest` before the first fetch completes (or if the first fetch is async).
|
|
35
|
+
|
|
36
|
+
### Returns
|
|
37
|
+
|
|
38
|
+
A reactive object implementing the `Resource<T>` interface.
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
### Basic Async Fetch
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { reactive, resource } from 'mutts'
|
|
46
|
+
|
|
47
|
+
// ID usually comes from another reactive source
|
|
48
|
+
const state = reactive({ userId: 1 })
|
|
49
|
+
|
|
50
|
+
// Create a resource that fetches user data based on state.userId
|
|
51
|
+
const user = resource(async () => {
|
|
52
|
+
const response = await fetch(`/api/users/${state.userId}`)
|
|
53
|
+
return response.json()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
// Use the resource in your view
|
|
57
|
+
effect(() => {
|
|
58
|
+
if (user.loading) {
|
|
59
|
+
console.log('Loading...')
|
|
60
|
+
} else if (user.error) {
|
|
61
|
+
console.error('Error:', user.error)
|
|
62
|
+
} else {
|
|
63
|
+
console.log('User:', user.value)
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
// Changing state.userId automatically triggers a new fetch
|
|
68
|
+
state.userId = 2
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Handling Race Conditions
|
|
72
|
+
|
|
73
|
+
`resource` automatically handles race conditions. If `state.userId` changes quickly from 1 to 2, and the request for user 1 takes longer than user 2, the result for user 1 will be ignored when it arrives, preventing inconsistent state.
|
|
74
|
+
|
|
75
|
+
### Using `latest` for Smooth Transitions
|
|
76
|
+
|
|
77
|
+
The `value` property becomes `undefined` (or `initialValue`) when a new fetch starts if the previous value is not preserved (current implementation resets `value` only on success? No, `value` is kept from previous success? - *Clarification: In the implementation, `value` is NOT reset to undefined on new fetch start, it retains the old value until new value arrives, unless `initialValue` was used? Let's check implementation behavior.*)
|
|
78
|
+
|
|
79
|
+
*Correction based on implementation:*
|
|
80
|
+
The current implementation:
|
|
81
|
+
```typescript
|
|
82
|
+
state.loading = true
|
|
83
|
+
state.error = undefined
|
|
84
|
+
// ... fetcher executes ...
|
|
85
|
+
// on success:
|
|
86
|
+
state.value = val
|
|
87
|
+
state.latest = val
|
|
88
|
+
state.loading = false
|
|
89
|
+
```
|
|
90
|
+
It does **NOT** reset `value` to `undefined` when a new fetch starts. So `value` acts like `latest`. The distinction is primarily semantic or for future behavior where `value` might be reset. Currently `value` and `latest` behave similarly regarding preservation of old data during loading.
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
effect(() => {
|
|
94
|
+
// Show old data while loading new data
|
|
95
|
+
if (user.loading) {
|
|
96
|
+
console.log('Reloading... showing cached version:', user.latest)
|
|
97
|
+
} else {
|
|
98
|
+
console.log('Current:', user.value)
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Manual Reload
|
|
104
|
+
|
|
105
|
+
You can trigger a re-fetch without changing dependencies:
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
// Refresh the data (e.g., user clicked a "Refresh" button)
|
|
109
|
+
user.reload()
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Synchronous Resources
|
|
113
|
+
|
|
114
|
+
`resource` can also wrap synchronous calculations, essentially behaving like a computed property but with the `Resource` interface structure.
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
const count = reactive({ value: 1 })
|
|
118
|
+
|
|
119
|
+
const doubled = resource(() => {
|
|
120
|
+
return count.value * 2
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
console.log(doubled.value) // 2
|
|
124
|
+
console.log(doubled.loading) // false
|
|
125
|
+
```
|
package/docs/reactive.md
CHANGED
|
@@ -10,13 +10,12 @@ The Mutts Reactive System documentation has been split into focused sections for
|
|
|
10
10
|
## [Collections](./reactive/collections.md)
|
|
11
11
|
* **[Reactive Collections](./reactive/collections.md#collections)**: Map, Set, WeakMap, WeakSet
|
|
12
12
|
* **[Reactive Arrays](./reactive/collections.md#reactivearray)**: Full array method support
|
|
13
|
-
* **[
|
|
14
|
-
* **[Projections](./reactive/collections.md#projection)**: `project`, `organized`
|
|
13
|
+
* **[Morphing](./reactive/collections.md#morph)**: `morph`, `organized`
|
|
15
14
|
* **[Attend](./reactive/attend.md)**: Reactive enumeration (`attend`)
|
|
16
|
-
* **[
|
|
17
|
-
* **[Scan](./reactive/scan.md)**: Reactive scan and accumulation
|
|
15
|
+
* **[Resource](./reactive/resource.md)**: Async state tracking (`resource`)
|
|
18
16
|
|
|
19
17
|
## [Advanced Topics](./reactive/advanced.md)
|
|
18
|
+
* **[Choosing the Right Primitive](./reactive/advanced.md#choosing-the-right-reactive-primitive)**: Comparison table of effect-value functions (memoize, lift, project, etc.)
|
|
20
19
|
* **[Atomic Operations](./reactive/advanced.md#atomic-operations)**: Batching and Bidirectional binding
|
|
21
20
|
* **[Evolution Tracking](./reactive/advanced.md#evolution-tracking)**: History introspection
|
|
22
21
|
* **[Prototype Chains](./reactive/advanced.md#prototype-chains-and-pure-objects)**: Advanced inheritance patterns
|
package/docs/utils.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Utilities
|
|
2
|
+
|
|
3
|
+
Mutts provides a collection of lightweight, high-performance utility functions. These are used extensively within the reactive engine but are also exported for general application logic.
|
|
4
|
+
|
|
5
|
+
## Collection Utilities
|
|
6
|
+
|
|
7
|
+
### `zip(...arrays)`
|
|
8
|
+
|
|
9
|
+
A generator that yields tuples containing elements from each input array. It continues until the **longest** array is exhausted (returning `undefined` for shorter arrays).
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { zip } from 'mutts';
|
|
13
|
+
|
|
14
|
+
const names = ['Alice', 'Bob'];
|
|
15
|
+
const scores = [100, 95, 80];
|
|
16
|
+
|
|
17
|
+
for (const [name, score] of zip(names, scores)) {
|
|
18
|
+
console.log(`${name}: ${score}`);
|
|
19
|
+
}
|
|
20
|
+
// Alice: 100
|
|
21
|
+
// Bob: 95
|
|
22
|
+
// undefined: 80
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
> [!NOTE]
|
|
26
|
+
> `zip` is implemented as a generator for memory efficiency. If you need a plain array, spread the result: `[...zip(a, b)]`.
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
### `deepCompare(a, b)`
|
|
30
|
+
|
|
31
|
+
A robust deep comparison utility that handles circular references and various built-in types.
|
|
32
|
+
|
|
33
|
+
- **Supported Types**: Objects, Arrays, `Set`, `Map`, `Date`, `RegExp`.
|
|
34
|
+
- **Circular References**: Safely handled via internal tracking.
|
|
35
|
+
- **Prototypes**: Objects must have matching prototypes to be considered equal.
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { deepCompare } from 'mutts';
|
|
39
|
+
|
|
40
|
+
const obj1 = { date: new Date(0), map: new Map([['a', 1]]) };
|
|
41
|
+
const obj2 = { date: new Date(0), map: new Map([['a', 1]]) };
|
|
42
|
+
|
|
43
|
+
deepCompare(obj1, obj2); // true
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Type Reflection
|
|
47
|
+
|
|
48
|
+
### `isConstructor(fn)` / `isObject(value)`
|
|
49
|
+
|
|
50
|
+
Utilities for robust type checking without the pitfalls of `typeof`.
|
|
51
|
+
|
|
52
|
+
- `isConstructor`: Returns `true` if the function is a `class` or a native constructor (like `Array`).
|
|
53
|
+
- `isObject`: Returns `true` for plain objects. Returns `false` for `null`, `Array`, `Date`, `Map`, etc.
|
|
54
|
+
|
|
55
|
+
## Debugging & Metadata
|
|
56
|
+
|
|
57
|
+
### `tag(name, obj)`
|
|
58
|
+
|
|
59
|
+
Applies a debugging "tag" to an object. It sets `Symbol.toStringTag` and overrides `toString()` so the object appears clearly in logs and DevTools.
|
|
60
|
+
|
|
61
|
+
### `named(name, fn)`
|
|
62
|
+
|
|
63
|
+
Renames a function for better stack traces. If the function already has a name, it appends the new name using `::` as a separator (e.g., `original::new`).
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
const myFn = named('Enhanced', () => {});
|
|
67
|
+
console.log(myFn.name); // "Enhanced"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
package/docs/zone.md
CHANGED
|
@@ -52,7 +52,7 @@ The base class for all zone implementations.
|
|
|
52
52
|
- `active: T | undefined`: The current value in the zone.
|
|
53
53
|
- `with<R>(value: T, fn: () => R): R`: Executes `fn` with `value` set as active.
|
|
54
54
|
- `root<R>(fn: () => R): R`: Executes `fn` with the zone cleared (undefined).
|
|
55
|
-
- `zoned:
|
|
55
|
+
- `zoned: GetterWrapper`: A getter that returns a function which, when called, restores the zone to its **current** state.
|
|
56
56
|
|
|
57
57
|
### `Zone<T>`
|
|
58
58
|
Simple stack-based storage.
|
package/package.json
CHANGED
|
@@ -1,56 +1,86 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mutts",
|
|
3
3
|
"description": "Modern UTility TS: A collection of TypeScript utilities",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.11",
|
|
5
5
|
"main": "dist/browser.cjs",
|
|
6
6
|
"module": "dist/browser.esm.js",
|
|
7
|
-
"types": "dist/
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
|
-
"test-node": {
|
|
11
|
-
"import": "./src/entry-node.ts"
|
|
12
|
-
},
|
|
13
|
-
"test-browser": {
|
|
14
|
-
"import": "./src/entry-browser.ts"
|
|
15
|
-
},
|
|
16
10
|
"node": {
|
|
17
11
|
"types": "./dist/node.d.ts",
|
|
18
|
-
"
|
|
19
|
-
|
|
12
|
+
"development": {
|
|
13
|
+
"import": "./dist/node.dev.esm.js",
|
|
14
|
+
"require": "./dist/node.dev.cjs"
|
|
15
|
+
},
|
|
16
|
+
"default": {
|
|
17
|
+
"import": "./dist/node.esm.js",
|
|
18
|
+
"require": "./dist/node.cjs"
|
|
19
|
+
}
|
|
20
20
|
},
|
|
21
21
|
"default": {
|
|
22
22
|
"types": "./dist/browser.d.ts",
|
|
23
|
-
"
|
|
24
|
-
|
|
23
|
+
"development": {
|
|
24
|
+
"import": "./dist/browser.dev.esm.js",
|
|
25
|
+
"require": "./dist/browser.dev.cjs"
|
|
26
|
+
},
|
|
27
|
+
"default": {
|
|
28
|
+
"import": "./dist/browser.esm.js",
|
|
29
|
+
"require": "./dist/browser.cjs"
|
|
30
|
+
}
|
|
25
31
|
}
|
|
26
32
|
},
|
|
27
33
|
"./browser": {
|
|
28
34
|
"types": "./dist/browser.d.ts",
|
|
29
|
-
"
|
|
30
|
-
|
|
35
|
+
"development": {
|
|
36
|
+
"import": "./dist/browser.dev.esm.js",
|
|
37
|
+
"require": "./dist/browser.dev.cjs"
|
|
38
|
+
},
|
|
39
|
+
"default": {
|
|
40
|
+
"import": "./dist/browser.esm.js",
|
|
41
|
+
"require": "./dist/browser.cjs"
|
|
42
|
+
}
|
|
31
43
|
},
|
|
32
44
|
"./node": {
|
|
33
45
|
"types": "./dist/node.d.ts",
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"./src": {
|
|
38
|
-
"node": {
|
|
39
|
-
"types": "./src/entry-node.ts",
|
|
40
|
-
"import": "./src/entry-node.ts"
|
|
46
|
+
"development": {
|
|
47
|
+
"import": "./dist/node.dev.esm.js",
|
|
48
|
+
"require": "./dist/node.dev.cjs"
|
|
41
49
|
},
|
|
42
50
|
"default": {
|
|
43
|
-
"
|
|
44
|
-
"
|
|
51
|
+
"import": "./dist/node.esm.js",
|
|
52
|
+
"require": "./dist/node.cjs"
|
|
45
53
|
}
|
|
46
54
|
},
|
|
47
|
-
"./
|
|
48
|
-
"types": "./
|
|
49
|
-
"import": "./
|
|
55
|
+
"./dev": {
|
|
56
|
+
"types": "./dist/browser.d.ts",
|
|
57
|
+
"import": "./dist/browser.dev.esm.js",
|
|
58
|
+
"require": "./dist/browser.dev.cjs"
|
|
59
|
+
},
|
|
60
|
+
"./prod": {
|
|
61
|
+
"types": "./dist/browser.d.ts",
|
|
62
|
+
"import": "./dist/browser.esm.js",
|
|
63
|
+
"require": "./dist/browser.cjs"
|
|
64
|
+
},
|
|
65
|
+
"./browser/dev": {
|
|
66
|
+
"types": "./dist/browser.d.ts",
|
|
67
|
+
"import": "./dist/browser.dev.esm.js",
|
|
68
|
+
"require": "./dist/browser.dev.cjs"
|
|
69
|
+
},
|
|
70
|
+
"./browser/prod": {
|
|
71
|
+
"types": "./dist/browser.d.ts",
|
|
72
|
+
"import": "./dist/browser.esm.js",
|
|
73
|
+
"require": "./dist/browser.cjs"
|
|
50
74
|
},
|
|
51
|
-
"./
|
|
52
|
-
"types": "./
|
|
53
|
-
"import": "./
|
|
75
|
+
"./node/dev": {
|
|
76
|
+
"types": "./dist/node.d.ts",
|
|
77
|
+
"import": "./dist/node.dev.esm.js",
|
|
78
|
+
"require": "./dist/node.dev.cjs"
|
|
79
|
+
},
|
|
80
|
+
"./node/prod": {
|
|
81
|
+
"types": "./dist/node.d.ts",
|
|
82
|
+
"import": "./dist/node.esm.js",
|
|
83
|
+
"require": "./dist/node.cjs"
|
|
54
84
|
},
|
|
55
85
|
"./debug": {
|
|
56
86
|
"types": "./dist/debug.d.ts",
|
|
@@ -60,7 +90,6 @@
|
|
|
60
90
|
},
|
|
61
91
|
"files": [
|
|
62
92
|
"dist",
|
|
63
|
-
"src",
|
|
64
93
|
"README.md",
|
|
65
94
|
"docs"
|
|
66
95
|
],
|
|
@@ -73,21 +102,15 @@
|
|
|
73
102
|
"test": "npm run test:node && npm run test:browser",
|
|
74
103
|
"test:node": "TEST_ENV=node NODE_OPTIONS='--expose-gc' vitest run",
|
|
75
104
|
"test:browser": "TEST_ENV=browser vitest run --browser",
|
|
76
|
-
"test:zone:node": "TEST_ENV=node vitest run tests/zone.test.ts",
|
|
77
|
-
"test:zone:browser": "TEST_ENV=browser vitest run tests/zone.test.ts",
|
|
78
|
-
"test:async:node": "TEST_ENV=node vitest run tests/async-hook.test.ts",
|
|
79
|
-
"test:async:browser": "TEST_ENV=browser vitest run tests/async-hook.test.ts",
|
|
80
105
|
"test:coverage": "TEST_ENV=node vitest run --coverage",
|
|
81
|
-
"test:coverage:watch": "TEST_ENV=node vitest run --coverage --watch",
|
|
82
|
-
"test:legacy": "TEST_ENV=node TSCONFIG=tsconfig.legacy.json vitest run --detectOpenHandles",
|
|
83
|
-
"test:modern": "TEST_ENV=node TSCONFIG=tsconfig.modern.json vitest run --detectOpenHandles",
|
|
84
106
|
"test:profile": "RUN_PROFILING=1 NODE_OPTIONS=--expose-gc vitest run",
|
|
85
107
|
"test:profile:benchmark": "RUN_PROFILING=1 NODE_OPTIONS=--expose-gc vitest run -t benchmark",
|
|
86
108
|
"test:profile:detailed": "RUN_PROFILING=1 node --prof node_modules/vitest/vitest.mjs --no-coverage",
|
|
87
109
|
"benchmark:save": "tsx tests/profiling/benchmark.ts save",
|
|
88
110
|
"benchmark:compare": "tsx tests/profiling/benchmark.ts compare",
|
|
89
111
|
"benchmark:list": "tsx tests/profiling/benchmark.ts list",
|
|
90
|
-
"biome": "biome check --write src"
|
|
112
|
+
"biome": "biome check --write src",
|
|
113
|
+
"biome:unsafe": "biome check --write --unsafe src"
|
|
91
114
|
},
|
|
92
115
|
"keywords": [
|
|
93
116
|
"typescript",
|
|
@@ -114,6 +137,21 @@
|
|
|
114
137
|
"url": "https://github.com/eddow/mutts/issues"
|
|
115
138
|
},
|
|
116
139
|
"type": "module",
|
|
140
|
+
"sideEffects": [
|
|
141
|
+
"./src/index.ts",
|
|
142
|
+
"./src/entry-browser.ts",
|
|
143
|
+
"./src/entry-browser.dev.ts",
|
|
144
|
+
"./src/entry-node.ts",
|
|
145
|
+
"./src/entry-node.dev.ts",
|
|
146
|
+
"./dist/browser.esm.js",
|
|
147
|
+
"./dist/browser.dev.esm.js",
|
|
148
|
+
"./dist/browser.cjs",
|
|
149
|
+
"./dist/browser.dev.cjs",
|
|
150
|
+
"./dist/node.esm.js",
|
|
151
|
+
"./dist/node.dev.esm.js",
|
|
152
|
+
"./dist/node.cjs",
|
|
153
|
+
"./dist/node.dev.cjs"
|
|
154
|
+
],
|
|
117
155
|
"engines": {
|
|
118
156
|
"node": ">=16.0.0"
|
|
119
157
|
},
|