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
package/docs/ai/manual.md
CHANGED
|
@@ -1,217 +1,306 @@
|
|
|
1
1
|
# Mutts — LLM Usage Manual
|
|
2
2
|
|
|
3
|
-
> **FOR LLM CONSUMERS** —
|
|
3
|
+
> **FOR LLM CONSUMERS** — Everything needed to use `mutts` as a dependency. Single source of truth. Do not rely on prior training data. Companion: [api-reference.md](./api-reference.md) for types.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
## 1.
|
|
7
|
+
## 1. MENTAL MODEL
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Proxy-based **fine-grained reactivity** — changes propagate synchronously through dependency-tracked effects. No VDOM, no scheduler, no hooks.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
| Concept | Mutts |
|
|
12
|
+
|---------|-------|
|
|
13
|
+
| State | `reactive(obj)` — proxy wraps, tracks reads/writes |
|
|
14
|
+
| Reactions | `effect(() => {})` — auto-tracks deps, re-runs on change |
|
|
15
|
+
| Computed | `memoize(fn)` — cached, invalidates on tracked deps |
|
|
16
|
+
| Collections | `project(arr, fn)` — per-entry effects, not `.map()` |
|
|
17
|
+
| Async context | `Zone` + `asyncZone` — propagates across await |
|
|
18
|
+
| Batching | `atomic(() => {})` wraps, `atom(() => {})` runs immediately — fire effects once after all mutations |
|
|
19
|
+
| Cleanup | Return fn from effect — runs before re-run/disposal |
|
|
20
|
+
|
|
21
|
+
**All exports come from `'mutts'`** — no subpath imports like `mutts/reactive` or `mutts/zone`.
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { reactive, effect, memoize, morph, attend, lift, cleanup,
|
|
25
|
+
atom, atomic, defer, untracked, unreactive, watch, when, biDi, caught, why,
|
|
26
|
+
cleanedBy, organized,
|
|
27
|
+
Zone, asyncZone, ZoneHistory, ZoneAggregator,
|
|
28
|
+
decorator, mixin, Eventful, Destroyable, flavored, Indexable, chainPromise,
|
|
29
|
+
reactiveOptions, isReactive, unwrap, getState
|
|
30
|
+
} from 'mutts'
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Entry points**: `mutts` (auto-selects), `mutts/browser` (DOM), `mutts/node` (AsyncLocalStorage), `mutts/debug`.
|
|
34
|
+
|
|
35
|
+
**Singleton guard**: mutts throws if loaded twice (different bundles/versions). Ensure your bundler externalizes or aliases `mutts` to a single source.
|
|
12
36
|
|
|
13
37
|
---
|
|
14
38
|
|
|
15
|
-
## 2.
|
|
39
|
+
## 2. CRITICAL TRAPS
|
|
16
40
|
|
|
17
|
-
###
|
|
41
|
+
### TRAP 1: Memoize with primitives
|
|
42
|
+
`memoize` uses WeakMap — args MUST be objects/symbols, NOT primitives.
|
|
18
43
|
|
|
19
44
|
```ts
|
|
20
|
-
|
|
45
|
+
// BAD
|
|
46
|
+
const double = memoize((n: number) => n * 2) // WeakMap can't key on number
|
|
21
47
|
|
|
22
|
-
|
|
48
|
+
// GOOD
|
|
49
|
+
const double = memoize((obj: { n: number }) => obj.n * 2)
|
|
23
50
|
```
|
|
24
51
|
|
|
25
|
-
|
|
26
|
-
-
|
|
27
|
-
|
|
28
|
-
|
|
52
|
+
### TRAP 2: Reactive reads lose context after await
|
|
53
|
+
Effect tracking context is automatically propagated across async boundaries in Node.js (via `AsyncLocalStorage`). In browsers, monkey-patching is less robust — use `tracked` to be safe.
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
// Node.js — works automatically (effectHistory is pre-registered in asyncZone)
|
|
57
|
+
effect(async () => {
|
|
58
|
+
await fetch('/api')
|
|
59
|
+
console.log(state.count) // tracked in Node.js
|
|
60
|
+
})
|
|
29
61
|
|
|
30
|
-
|
|
62
|
+
// Browser or explicit — use access.tracked to restore context
|
|
63
|
+
effect(({ tracked }) => {
|
|
64
|
+
someCallback(() => {
|
|
65
|
+
tracked(() => console.log(state.count)) // restore tracking context
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### TRAP 3: Self-triggering effects (cycles)
|
|
71
|
+
Effect reads and writes same state → infinite loop.
|
|
31
72
|
|
|
32
73
|
```ts
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
74
|
+
// BAD — cycle
|
|
75
|
+
effect(() => {
|
|
76
|
+
state.count = state.count + 1 // reads count, writes count → re-triggers
|
|
36
77
|
})
|
|
37
78
|
|
|
38
|
-
|
|
39
|
-
|
|
79
|
+
// GOOD — defer mutation
|
|
80
|
+
effect(() => {
|
|
81
|
+
const val = state.count
|
|
82
|
+
defer(() => { state.processedCount = val }) // runs after batch
|
|
83
|
+
})
|
|
40
84
|
```
|
|
41
85
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
- **`reaction`**: `false` on first run, `true` on subsequent triggers.
|
|
45
|
-
- **`tracked(fn)`**: restores tracking context in async/unmanaged callbacks.
|
|
46
|
-
- **`ascend(fn)`**: tracks dependencies in the *parent* effect instead.
|
|
47
|
-
- **Parent-child**: effects created inside other effects are children; disposing a parent disposes all children.
|
|
48
|
-
- **GC**: unreferenced top-level effects may be garbage-collected. Store the cleanup reference to keep alive.
|
|
86
|
+
### TRAP 4: Using queueMicrotask for cycle avoidance
|
|
87
|
+
`defer()` is batch-aware and synchronous. `queueMicrotask` breaks batching.
|
|
49
88
|
|
|
50
|
-
**Modifiers** (chainable):
|
|
51
89
|
```ts
|
|
52
|
-
|
|
53
|
-
effect
|
|
54
|
-
|
|
90
|
+
// BAD
|
|
91
|
+
effect(() => {
|
|
92
|
+
queueMicrotask(() => state.x = state.y) // async, breaks atomic batches
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
// GOOD
|
|
96
|
+
effect(() => {
|
|
97
|
+
defer(() => state.x = state.y) // sync after batch completes
|
|
98
|
+
})
|
|
55
99
|
```
|
|
56
100
|
|
|
57
|
-
###
|
|
101
|
+
### TRAP 5: Array clearing
|
|
102
|
+
`array.length = 0` works (triggers all indices + length). `splice(0)` is equivalent.
|
|
58
103
|
|
|
59
104
|
```ts
|
|
60
|
-
|
|
61
|
-
//
|
|
62
|
-
// Cache invalidates when tracked reactive reads inside fn change.
|
|
63
|
-
// Same fn passed to memoize() multiple times returns same wrapper.
|
|
105
|
+
array.length = 0 // works — touches all affected indices
|
|
106
|
+
array.splice(0) // also works — explicit clear
|
|
64
107
|
```
|
|
65
108
|
|
|
66
|
-
|
|
109
|
+
### TRAP 6: Using .map() for reactive transforms
|
|
110
|
+
`.map()` is static — full rebuild on any change. Use `project()` for per-entry reactivity.
|
|
67
111
|
|
|
68
|
-
|
|
112
|
+
```ts
|
|
113
|
+
// BAD — full rebuild
|
|
114
|
+
const doubled = items.map(x => x.value * 2)
|
|
115
|
+
|
|
116
|
+
// GOOD — per-entry effects
|
|
117
|
+
const doubled = project(items, ({ get }) => get().value * 2)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### TRAP 7: Deep watch overhead
|
|
121
|
+
`watch(obj, fn, { deep: true })` tracks ALL nested properties. Prefer explicit reads + recursive touching (enabled by default).
|
|
69
122
|
|
|
70
123
|
```ts
|
|
71
|
-
|
|
72
|
-
|
|
124
|
+
// EXPENSIVE
|
|
125
|
+
watch(state, () => { ... }, { deep: true })
|
|
126
|
+
|
|
127
|
+
// BETTER — explicit property reads
|
|
128
|
+
effect(() => {
|
|
129
|
+
state.user.name // only tracks .user.name
|
|
73
130
|
})
|
|
74
131
|
```
|
|
75
132
|
|
|
76
|
-
###
|
|
133
|
+
### TRAP 8: _mutts_* properties are debug helpers
|
|
134
|
+
`_mutts_*` properties are for introspection/logging ONLY — not stable API.
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
// OK for debugging
|
|
138
|
+
console.log(obj._mutts_watchers)
|
|
139
|
+
|
|
140
|
+
// BAD — don't build logic on it
|
|
141
|
+
if (obj._mutts_isReactive) { ... }
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## 3. CORE REACTIVITY
|
|
147
|
+
|
|
148
|
+
### 3.1 reactive() — Trackable state
|
|
77
149
|
|
|
78
150
|
```ts
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
151
|
+
const state = reactive({ count: 0, items: [1, 2, 3] })
|
|
152
|
+
// Same object → same proxy. Works: objects, arrays, Map, Set, WeakMap, WeakSet
|
|
153
|
+
// unwrap(proxy) → original. isReactive(obj) → boolean
|
|
82
154
|
```
|
|
83
155
|
|
|
84
|
-
### 2
|
|
156
|
+
### 3.2 effect() — Auto-tracked reactions
|
|
85
157
|
|
|
86
158
|
```ts
|
|
87
|
-
const
|
|
88
|
-
state.
|
|
89
|
-
|
|
90
|
-
// Effects fire ONCE after both mutations, not twice
|
|
159
|
+
const stop = effect(({ reaction, tracked, ascend }) => {
|
|
160
|
+
console.log(state.count) // auto-tracked
|
|
161
|
+
return () => { /* cleanup: before re-run or disposal */ }
|
|
91
162
|
})
|
|
92
|
-
|
|
163
|
+
state.count++ // triggers
|
|
164
|
+
stop() // disposes
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
- **Access object**: `{ reaction, tracked(fn), ascend(fn) }` — `reaction` = false on first run, true after. `tracked` restores context in async. `ascend` tracks in parent effect.
|
|
168
|
+
- **Parent-child**: effects inside effects are children — parent disposal cascades.
|
|
169
|
+
- **GC**: unreferenced top-level effects may GC — store `stop` to keep alive.
|
|
170
|
+
- **Modifiers**: `effect.opaque(() => {})` (identity-only), `effect.named('x')(() => {})` (debug label). Chainable.
|
|
171
|
+
|
|
172
|
+
### 3.3 memoize() — Cached computed
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
const doubled = memoize((obj: { value: number }) => obj.value * 2)
|
|
176
|
+
// Args MUST be objects/symbols (WeakMap keys). Cache invalidates on tracked deps.
|
|
177
|
+
// Decorator: @memoize on getters (per-instance) or methods (per-instance+args).
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### 3.4 untracked() — Escape tracking
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
untracked(() => { state.count }) // NOT tracked
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### 3.5 unreactive() — Opt out
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
unreactive(obj) // mark non-reactive
|
|
190
|
+
unreactive(MyClass) // entire class
|
|
191
|
+
@unreactive('id', 'meta') // specific props on @reactive class
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### 3.6 atomic() — Batch mutations
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
const updateBoth = atomic((a, b) => { state.a = a; state.b = b }) // effects fire once
|
|
198
|
+
// Decorator: @atomic on methods
|
|
93
199
|
```
|
|
94
200
|
|
|
95
|
-
|
|
201
|
+
### 3.7 atom() — Immediate atomic execution
|
|
96
202
|
|
|
97
|
-
|
|
203
|
+
```ts
|
|
204
|
+
atom(() => { state.a = 1; state.b = 2 }) // runs now, effects fire once
|
|
205
|
+
// Unlike atomic() which wraps for later, atom() executes immediately
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### 3.8 defer() — Avoid cycles
|
|
98
209
|
|
|
99
|
-
When an effect needs to mutate state it reads, defer the mutation:
|
|
100
210
|
```ts
|
|
101
211
|
effect(() => {
|
|
102
212
|
const len = state.items.length
|
|
103
|
-
defer(() => { state.processedCount = len }) //
|
|
213
|
+
defer(() => { state.processedCount = len }) // sync after batch, FIFO
|
|
104
214
|
})
|
|
215
|
+
// Outside batch: immediate. NO microtask delay.
|
|
105
216
|
```
|
|
106
|
-
- Runs synchronously after outermost batch. FIFO order. No microtask delay.
|
|
107
|
-
- Outside a batch: executes immediately.
|
|
108
217
|
|
|
109
|
-
###
|
|
218
|
+
### 3.9 biDi() — Bidirectional binding
|
|
110
219
|
|
|
111
|
-
Bridges reactive state ↔ external (DOM, third-party). Prevents infinite loops automatically.
|
|
112
220
|
```ts
|
|
113
221
|
const provide = biDi(
|
|
114
|
-
(v) =>
|
|
222
|
+
(v) => input.value = v, // external setter
|
|
115
223
|
{ get: () => model.value, set: (v) => model.value = v }
|
|
116
224
|
)
|
|
117
|
-
|
|
225
|
+
input.addEventListener('input', () => provide(input.value))
|
|
226
|
+
// Prevents infinite loops automatically
|
|
118
227
|
```
|
|
119
228
|
|
|
120
|
-
###
|
|
229
|
+
### 3.10 watch() — Observe changes
|
|
121
230
|
|
|
122
231
|
```ts
|
|
123
|
-
|
|
124
|
-
watch(
|
|
232
|
+
watch(() => state.count, (newVal, oldVal) => {}) // specific derivation
|
|
233
|
+
watch(state, () => {}) // any property
|
|
234
|
+
watch(state, () => {}, { deep: true }) // deep (expensive)
|
|
235
|
+
```
|
|
125
236
|
|
|
126
|
-
|
|
127
|
-
watch(state, () => { ... })
|
|
237
|
+
### 3.10 cleanedBy() — Attach cleanup to objects
|
|
128
238
|
|
|
129
|
-
|
|
130
|
-
|
|
239
|
+
```ts
|
|
240
|
+
const obj = cleanedBy({ data: [] }, () => console.log('cleaned up'))
|
|
241
|
+
obj[cleanup]() // triggers cleanup
|
|
242
|
+
// Chains: if obj already has a cleanup, both run
|
|
131
243
|
```
|
|
132
244
|
|
|
133
|
-
|
|
245
|
+
### 3.11 Lazy computed values
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
// Lazy + trackable (preferred):
|
|
249
|
+
const total = memoize(() => state.a + state.b)
|
|
250
|
+
effect(() => console.log(total())) // recomputes only on read, dependency tracked
|
|
251
|
+
|
|
252
|
+
// Eager + trackable (stable reactive proxy):
|
|
253
|
+
const d = lift(() => ({ value: state.a + state.b }))
|
|
254
|
+
effect(() => console.log(d.value)) // recomputes immediately, .value is reactive
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### 3.12 when() — Promise-based reactive wait
|
|
134
258
|
|
|
135
|
-
|
|
259
|
+
```ts
|
|
260
|
+
await when(() => state.loaded) // resolves when truthy
|
|
261
|
+
await when(() => state.ready, { timeout: 5000 }) // rejects after 5s
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
---
|
|
136
265
|
|
|
137
|
-
|
|
266
|
+
## 4. COLLECTIONS
|
|
138
267
|
|
|
139
268
|
| Type | Tracking |
|
|
140
269
|
|------|----------|
|
|
141
|
-
| `reactive([])` | Per-index, `.length`, iteration
|
|
270
|
+
| `reactive([])` | Per-index, `.length`, iteration |
|
|
142
271
|
| `reactive(new Map())` | Per-key, `.size`, iteration |
|
|
143
272
|
| `reactive(new Set())` | Per-value, `.size`, iteration |
|
|
144
|
-
| `reactive(new WeakMap())` | Per-key only |
|
|
145
|
-
| `reactive(new WeakSet())` | Per-value only |
|
|
273
|
+
| `reactive(new WeakMap/Set())` | Per-key/value only |
|
|
146
274
|
|
|
147
275
|
Array methods (`push`, `splice`, `sort`, etc.) all trigger reactivity.
|
|
148
276
|
|
|
149
|
-
|
|
150
|
-
### 3.1 Register — Keyed ordered collection
|
|
151
|
-
|
|
152
|
-
```ts
|
|
153
|
-
import { Register } from 'mutts/reactive'
|
|
154
|
-
|
|
155
|
-
const list = new Register(item => item.id, [
|
|
156
|
-
{ id: 1, label: 'Alpha' },
|
|
157
|
-
{ id: 2, label: 'Bravo' },
|
|
158
|
-
])
|
|
159
|
-
|
|
160
|
-
list.push({ id: 3, label: 'Charlie' })
|
|
161
|
-
list.get(2) // lookup by key, O(1)
|
|
162
|
-
list.set(2, newItem) // update by key
|
|
163
|
-
list.remove(2) // remove by key
|
|
164
|
-
list.keep(item => item.active) // filter in-place
|
|
165
|
-
list.upsert(v => list.push(v), ...items) // update or insert
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
Full array surface (`map`, `filter`, `reduce`, `sort`, etc.) + CRUD events (`on('add', ...)`, `on('delete', ...)`, `on('update', ...)`, `on('rekey', ...)`).
|
|
169
|
-
|
|
170
277
|
---
|
|
171
278
|
|
|
172
|
-
##
|
|
279
|
+
## 5. COLLECTION TRANSFORMS
|
|
173
280
|
|
|
174
|
-
|
|
281
|
+
All transforms return reactive results. Cleanup via `result[cleanup]()`.
|
|
175
282
|
|
|
176
|
-
|
|
283
|
+
### 5.1 project() — Per-entry reactive map
|
|
177
284
|
|
|
178
285
|
```ts
|
|
179
|
-
import { project, cleanup } from 'mutts'
|
|
180
|
-
|
|
181
|
-
// Array
|
|
182
286
|
const names = project(users, ({ get }) => get().name.toUpperCase())
|
|
183
|
-
// names[0] recomputes
|
|
184
|
-
|
|
185
|
-
// Record
|
|
186
|
-
const grades = project.record(scores, ({ get }) => get() >= 90 ? 'A' : 'B')
|
|
187
|
-
|
|
188
|
-
// Map
|
|
189
|
-
const totals = project.map(inventory, ({ get }) => get().count)
|
|
190
|
-
|
|
191
|
-
// Auto-dispatch
|
|
192
|
-
const doubled = project(source, ({ get }) => get() * 2)
|
|
193
|
-
|
|
194
|
-
// Cleanup
|
|
195
|
-
names[cleanup]()
|
|
287
|
+
// names[0] recomputes ONLY when users[0] changes
|
|
288
|
+
// Variants: project.record(), project.map() — auto-dispatches by source type
|
|
196
289
|
```
|
|
197
290
|
|
|
198
291
|
**Access object**: `{ get(), set(v), key, source, old, value }`
|
|
199
292
|
|
|
200
|
-
###
|
|
201
|
-
|
|
202
|
-
Creates inner effect per key. Disposed when key disappears.
|
|
293
|
+
### 5.2 attend() — Per-key lifecycle
|
|
203
294
|
|
|
204
295
|
```ts
|
|
205
|
-
import { attend } from 'mutts'
|
|
206
|
-
|
|
207
296
|
attend(reactiveRecord, (key) => {
|
|
208
297
|
console.log(`${key} = ${reactiveRecord[key]}`)
|
|
209
|
-
return () => console.log(`cleanup: ${key}`)
|
|
298
|
+
return () => console.log(`cleanup: ${key}`) // disposed when key disappears
|
|
210
299
|
})
|
|
211
|
-
//
|
|
300
|
+
// Works with: arrays, Maps, Sets, or raw () => Iterable<Key>
|
|
212
301
|
```
|
|
213
302
|
|
|
214
|
-
###
|
|
303
|
+
### 5.3 organized() — Per-key record transform
|
|
215
304
|
|
|
216
305
|
```ts
|
|
217
306
|
const doubled = organized(source, (access, target) => {
|
|
@@ -220,41 +309,22 @@ const doubled = organized(source, (access, target) => {
|
|
|
220
309
|
})
|
|
221
310
|
```
|
|
222
311
|
|
|
223
|
-
###
|
|
224
|
-
|
|
225
|
-
```ts
|
|
226
|
-
const descriptors = reactive({ foo: { value: 1, enumerable: true } })
|
|
227
|
-
const target = describe(descriptors) // target.foo === 1
|
|
228
|
-
descriptors.bar = { get: () => 42, enumerable: true } // target.bar appears
|
|
229
|
-
delete descriptors.foo // target.foo disappears
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
### 4.5 scan() — Reactive accumulation
|
|
312
|
+
### 5.6 lift() — Sync a computed array/object
|
|
233
313
|
|
|
234
314
|
```ts
|
|
235
|
-
const
|
|
236
|
-
//
|
|
237
|
-
// Changing source[1].val only recomputes from index 1 onward
|
|
238
|
-
result[cleanup]()
|
|
315
|
+
const filtered = lift(() => items.filter(x => x.active))
|
|
316
|
+
// Element-wise diff — only changed elements sync, not full rebuild
|
|
239
317
|
```
|
|
240
318
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
### 4.6 lift() — Sync a computed array/object
|
|
319
|
+
**lift vs deep touching**: Deep touching handles `state.items = newArray` (replacement diffs). `lift` is for **derived collections** (filter/map/reshape) where there's no single assignment — the output is recomputed from scratch.
|
|
244
320
|
|
|
245
|
-
|
|
246
|
-
const filtered = lift(() => items.filter(x => x.active))
|
|
247
|
-
// Only changed elements sync — element-wise diff, not full rebuild
|
|
248
|
-
filtered[cleanup]()
|
|
249
|
-
```
|
|
321
|
+
**lift vs memoize**: `memoize` is lazy (invalidate → recompute on next read), returns raw values, keyed by args. `lift` is eager (recompute immediately), returns a **stable reactive proxy** with per-element diffing. Use `lift` for derived collections consumed by `project()`/effects; use `memoize` for parameterized caching or lazy evaluation.
|
|
250
322
|
|
|
251
323
|
---
|
|
252
324
|
|
|
253
|
-
##
|
|
325
|
+
## 6. EVOLUTION TRACKING
|
|
254
326
|
|
|
255
327
|
```ts
|
|
256
|
-
import { getState } from 'mutts/reactive'
|
|
257
|
-
|
|
258
328
|
let state = getState(obj)
|
|
259
329
|
effect(() => {
|
|
260
330
|
while ('evolution' in state) {
|
|
@@ -264,68 +334,68 @@ effect(() => {
|
|
|
264
334
|
})
|
|
265
335
|
```
|
|
266
336
|
|
|
267
|
-
**Recursive touching**: replacing an object with another of the same prototype triggers fine-grained per-property diffs, not
|
|
337
|
+
**Recursive touching**: replacing an object with another of the same prototype triggers fine-grained per-property diffs, not wholesale notification. Disable: `reactiveOptions.recursiveTouching = false`.
|
|
268
338
|
|
|
269
339
|
---
|
|
270
340
|
|
|
271
|
-
##
|
|
341
|
+
## 7. ERROR HANDLING
|
|
272
342
|
|
|
273
343
|
```ts
|
|
274
|
-
import { onEffectThrow } from 'mutts'
|
|
275
|
-
|
|
276
344
|
effect(() => {
|
|
277
|
-
|
|
278
|
-
console.error('Caught:', error)
|
|
345
|
+
caught((error) => {
|
|
279
346
|
// return without throwing = handled
|
|
280
347
|
// throw = try next handler
|
|
281
|
-
// return function = cleanup on
|
|
348
|
+
// return function = cleanup on disposal
|
|
282
349
|
})
|
|
283
350
|
// ... code that might throw
|
|
284
351
|
})
|
|
285
352
|
```
|
|
286
353
|
|
|
287
|
-
- Multiple handlers tried in order. Unhandled
|
|
288
|
-
- **Must register before throwing code** (
|
|
289
|
-
- Does **not** catch async errors — use `.catch()
|
|
354
|
+
- Multiple handlers, tried in order. Unhandled → propagate to parent effect.
|
|
355
|
+
- **Must register before throwing code** (cleared on re-run).
|
|
356
|
+
- Does **not** catch async errors — use `.catch()`.
|
|
290
357
|
|
|
291
358
|
---
|
|
292
359
|
|
|
293
|
-
##
|
|
360
|
+
## 8. DEBUGGING & OPTIONS
|
|
294
361
|
|
|
295
362
|
```ts
|
|
296
|
-
import { reactiveOptions } from 'mutts
|
|
363
|
+
import { reactiveOptions } from 'mutts'
|
|
297
364
|
|
|
298
|
-
// Cycle detection
|
|
299
|
-
reactiveOptions.cycleHandling = 'development' // default
|
|
365
|
+
// Cycle detection
|
|
366
|
+
reactiveOptions.cycleHandling = 'development' // default: graph-based, throws
|
|
300
367
|
reactiveOptions.cycleHandling = 'debug' // full transitive closure, detailed paths
|
|
301
|
-
reactiveOptions.cycleHandling = 'production' // heuristic via maxTriggerPerBatch
|
|
302
|
-
|
|
303
|
-
// Memoization discrepancy (double-run detection)
|
|
304
|
-
reactiveOptions.onMemoizationDiscrepancy = (cached, fresh, fn, args, cause) => {
|
|
305
|
-
throw new Error(`Discrepancy in ${fn.name}: ${cause}`)
|
|
306
|
-
}
|
|
368
|
+
reactiveOptions.cycleHandling = 'production' // heuristic via maxTriggerPerBatch
|
|
307
369
|
|
|
308
|
-
// Lifecycle hooks
|
|
309
|
-
reactiveOptions.enter = (effect) => {
|
|
310
|
-
reactiveOptions.leave = (effect) => {
|
|
311
|
-
reactiveOptions.touched = (obj, evolution) => {
|
|
370
|
+
// Lifecycle hooks (all wrapped via optionCall for safety)
|
|
371
|
+
reactiveOptions.enter = (effect) => {} // before effect runs
|
|
372
|
+
reactiveOptions.leave = (effect) => {} // after effect runs
|
|
373
|
+
reactiveOptions.touched = (obj, evolution, props, effects) => {}
|
|
374
|
+
reactiveOptions.beginChain = (roots) => {} // before batch
|
|
375
|
+
reactiveOptions.garbageCollected = (fn) => {} // effect GC'd
|
|
376
|
+
reactiveOptions.skipRunningEffect = (fn) => {} // effect skipped (already running)
|
|
377
|
+
reactiveOptions.onMemoizationDiscrepancy = (cached, fresh, fn, args, cause) => {}
|
|
312
378
|
|
|
313
379
|
// Introspection (memory-intensive, dev only)
|
|
314
|
-
import {
|
|
315
|
-
|
|
380
|
+
import { reactiveOptions } from 'mutts'
|
|
381
|
+
import { buildReactivityGraph, getMutationHistory } from 'mutts/debug'
|
|
382
|
+
|
|
383
|
+
reactiveOptions.introspection = { enableHistory: true, historySize: 100 }
|
|
384
|
+
const history = getMutationHistory()
|
|
385
|
+
const graph = buildReactivityGraph()
|
|
316
386
|
```
|
|
317
387
|
|
|
318
|
-
**ReactiveError.debugInfo
|
|
388
|
+
**ReactiveError.debugInfo**: `causalChain`, `creationStack`, `cycle` (for CYCLE_DETECTED).
|
|
319
389
|
|
|
320
390
|
---
|
|
321
391
|
|
|
322
|
-
##
|
|
392
|
+
## 9. ZONES (Async Context)
|
|
323
393
|
|
|
324
394
|
```ts
|
|
325
|
-
import { Zone, asyncZone } from 'mutts
|
|
395
|
+
import { Zone, asyncZone } from 'mutts'
|
|
326
396
|
|
|
327
397
|
const requestId = new Zone<string>()
|
|
328
|
-
asyncZone.add(requestId)
|
|
398
|
+
asyncZone.add(requestId) // register for async propagation
|
|
329
399
|
|
|
330
400
|
requestId.with('req-123', async () => {
|
|
331
401
|
await somePromise()
|
|
@@ -333,118 +403,106 @@ requestId.with('req-123', async () => {
|
|
|
333
403
|
})
|
|
334
404
|
```
|
|
335
405
|
|
|
336
|
-
-
|
|
337
|
-
-
|
|
338
|
-
-
|
|
339
|
-
-
|
|
340
|
-
- Reactivity uses zones internally (`effectHistory`
|
|
406
|
+
- **`Zone<T>`**: stack-based. `.with(value, fn)`, `.active`, `.root(fn)`.
|
|
407
|
+
- **`ZoneHistory<T>`**: extends Zone, tracks history set for cycle detection.
|
|
408
|
+
- **`ZoneAggregator`**: combines zones. `asyncZone` is the global one.
|
|
409
|
+
- **`.zoned`**: snapshot context for manual bridging into unmanaged callbacks.
|
|
410
|
+
- Reactivity uses zones internally (`effectHistory` tracks active effect across await).
|
|
411
|
+
- **Node**: uses `AsyncLocalStorage`. **Browser**: monkey-patches Promise/setTimeout (less robust).
|
|
341
412
|
|
|
342
413
|
---
|
|
343
414
|
|
|
344
|
-
##
|
|
415
|
+
## 10. OTHER MODULES
|
|
345
416
|
|
|
346
|
-
###
|
|
417
|
+
### 10.1 Decorators
|
|
347
418
|
|
|
348
|
-
Unified system for legacy (`experimentalDecorators`) and modern (Stage 3):
|
|
349
419
|
```ts
|
|
350
|
-
import { decorator } from 'mutts/decorator'
|
|
351
420
|
const myDec = decorator({ method(original, name) { ... }, class(target) { ... } })
|
|
421
|
+
// Works with both legacy (experimentalDecorators) and Stage 3 decorators
|
|
422
|
+
// Built-in: @cached, @debounce(ms), @throttle(ms), @deprecated(msg)
|
|
352
423
|
```
|
|
353
424
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
### 9.2 Mixin (`mutts/mixin`)
|
|
425
|
+
### 10.2 Mixin
|
|
357
426
|
|
|
358
427
|
```ts
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
class A extends Countable { } // as base class
|
|
363
|
-
class B extends Countable(OtherBase) { } // as mixin
|
|
364
|
-
// Cached: same base always returns same mixed class
|
|
428
|
+
const Countable = mixin((base) => class extends base { count = 0 })
|
|
429
|
+
class A extends Countable { } // as base
|
|
430
|
+
class B extends Countable(Other) { } // as mixin (cached per base)
|
|
365
431
|
```
|
|
366
432
|
|
|
367
|
-
###
|
|
433
|
+
### 10.3 Eventful
|
|
368
434
|
|
|
369
|
-
Type-safe event system:
|
|
370
435
|
```ts
|
|
371
|
-
import { Eventful } from 'mutts/eventful'
|
|
372
436
|
interface MyEvents { click: (x: number, y: number) => void }
|
|
373
437
|
class Button extends Eventful<MyEvents> { }
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
btn.
|
|
377
|
-
btn.
|
|
378
|
-
btn.off.click()
|
|
379
|
-
const unhook = btn.hook((event, ...args) => { ... }) // global listener
|
|
438
|
+
btn.on.click((x, y) => {}) // dot notation subscribe
|
|
439
|
+
btn.emit.click(100, 200) // emit
|
|
440
|
+
btn.off.click() // unsubscribe all
|
|
441
|
+
btn.hook((event, ...args) => {}) // global listener
|
|
380
442
|
```
|
|
381
443
|
|
|
382
|
-
###
|
|
444
|
+
### 10.4 Destroyable
|
|
383
445
|
|
|
384
|
-
Resource management with `FinalizationRegistry` and `Symbol.dispose`:
|
|
385
446
|
```ts
|
|
386
|
-
import { Destroyable, allocated, destructor } from 'mutts/destroyable'
|
|
387
|
-
|
|
388
447
|
class FileHandler extends Destroyable() {
|
|
389
448
|
@allocated accessor filePath: string
|
|
390
449
|
[destructor](alloc) { console.log(`Closing: ${alloc.filePath}`) }
|
|
391
450
|
}
|
|
392
|
-
// Destroyed objects throw DestructionError on access
|
|
393
|
-
//
|
|
451
|
+
// Destroyed objects throw DestructionError on access
|
|
452
|
+
// Destroyable.destroy(instance) or `using` statement
|
|
394
453
|
```
|
|
395
454
|
|
|
396
|
-
###
|
|
455
|
+
### 10.5 Flavored Functions
|
|
397
456
|
|
|
398
|
-
Chainable property modifiers on functions:
|
|
399
457
|
```ts
|
|
400
|
-
import { flavored, flavorOptions } from 'mutts'
|
|
401
458
|
const greet = flavored(
|
|
402
459
|
(name: string, opts?: { loud?: boolean }) => opts?.loud ? name.toUpperCase() : name,
|
|
403
460
|
{ get loud() { return flavorOptions(this, { loud: true }) } }
|
|
404
461
|
)
|
|
405
|
-
greet.loud('hi') // "HI"
|
|
462
|
+
greet.loud('hi') // "HI" — chainable property modifiers
|
|
406
463
|
```
|
|
407
464
|
|
|
408
|
-
###
|
|
465
|
+
### 10.6 Indexable
|
|
409
466
|
|
|
410
|
-
Numeric index access (`obj[0]`) on custom classes via Proxy:
|
|
411
467
|
```ts
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
set(index, v) { this.data[index] = v }
|
|
468
|
+
const MyCol = Indexable(Base, {
|
|
469
|
+
get(i) { return this.data[i] },
|
|
470
|
+
set(i, v) { this.data[i] = v }
|
|
416
471
|
})
|
|
472
|
+
// Enables obj[0] numeric index access on custom classes via Proxy
|
|
417
473
|
```
|
|
418
474
|
|
|
419
|
-
###
|
|
475
|
+
### 10.7 PromiseChain
|
|
420
476
|
|
|
421
|
-
Fluent chaining on Promises without intermediate `await`:
|
|
422
477
|
```ts
|
|
423
|
-
import { chainPromise } from 'mutts/promiseChain'
|
|
424
478
|
const theme = await chainPromise(api.getUser('123')).getProfile().getSettings().theme
|
|
479
|
+
// Fluent chaining on Promises without intermediate await
|
|
425
480
|
```
|
|
426
481
|
|
|
427
482
|
---
|
|
428
483
|
|
|
429
|
-
##
|
|
430
|
-
|
|
431
|
-
### Affirmative State
|
|
432
|
-
Declare `Y = f(X)`. Do NOT say "when X changes, update Y". The system ensures consistency.
|
|
484
|
+
## 11. PHILOSOPHY
|
|
433
485
|
|
|
434
|
-
|
|
435
|
-
|
|
486
|
+
- **Affirmative state**: Declare `Y = f(X)`. Don't say "when X changes, update Y".
|
|
487
|
+
- **Events are legacy**: Use reactive derivations (`effect`, `memoize`, `project`) for internal logic. Events only for DOM/external APIs.
|
|
488
|
+
- **Cleanup ≠ undo**: Cleanup releases subscriptions, does NOT undo side effects.
|
|
436
489
|
|
|
437
|
-
|
|
438
|
-
Cleanup functions release reactive subscriptions, NOT undo side effects. If an element is being removed, there's no point resetting its attributes.
|
|
439
|
-
|
|
440
|
-
### Key Traps
|
|
441
|
-
|
|
442
|
-
| Trap | Fix |
|
|
443
|
-
|------|-----|
|
|
444
|
-
| `memoize` with primitive args | Args must be objects/symbols (WeakMap keys) |
|
|
445
|
-
| Bare reactive reads lose context after `await` | Register zones in `asyncZone` or use `tracked(() => ...)` |
|
|
446
|
-
| Self-triggering effects | Use `defer()` / `addBatchCleanup()` |
|
|
447
|
-
| `queueMicrotask` for cycle avoidance | Use `defer()` instead (synchronous, batch-aware) |
|
|
448
|
-
| Deep watch overhead | Prefer explicit property reads + recursive touching |
|
|
449
|
-
| `_mutts_*` properties are debug/introspection helpers | Use them for logging, tracing, and profiling only — not as stable API |
|
|
490
|
+
---
|
|
450
491
|
|
|
492
|
+
## 12. QUICK REFERENCE — DO vs DON'T
|
|
493
|
+
|
|
494
|
+
| DO | DON'T |
|
|
495
|
+
|----|-------|
|
|
496
|
+
| `morph(arr, fn)` | `arr.map(fn)` for reactive transforms |
|
|
497
|
+
| `effect(() => { state.x })` | bare `state.x` outside effect |
|
|
498
|
+
| `defer(() => state.y = val)` | `queueMicrotask(() => state.y = val)` |
|
|
499
|
+
| `memoize((obj) => obj.n * 2)` | `memoize((n: number) => n * 2)` |
|
|
500
|
+
| `atom(() => { a=1; b=2 })` | sequential mutations (2 effect runs) |
|
|
501
|
+
| `untracked(() => state.x)` | reading state you don't want tracked |
|
|
502
|
+
| `watch(() => state.x, cb)` | manual dirty-checking |
|
|
503
|
+
| `arr.splice(0)` or `arr.length = 0` | manual loop to clear arrays |
|
|
504
|
+
| `isReactive(obj)` to check | `obj._mutts_isReactive` |
|
|
505
|
+
| Store `stop = effect(...)` | letting effect GC unintentionally |
|
|
506
|
+
| `import { x } from 'mutts'` | `import { x } from 'mutts/reactive'` (no subpaths) |
|
|
507
|
+
| `cleanedBy(obj, fn)` for cleanup | manual `obj[cleanup] = fn` |
|
|
508
|
+
| `memoize(() => a + b)` for computed | `effect` + manual state sync |
|