mutts 1.0.9 → 1.0.10
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 +58 -33
- package/dist/browser.cjs +1371 -1206
- package/dist/browser.cjs.map +1 -1
- package/dist/browser.d.ts +2 -1392
- package/dist/browser.dev.cjs +110 -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 +6 -87
- package/dist/browser.esm.js.map +1 -1
- package/dist/chunks/{async-browser-lvzLOCgk.cjs → async-browser-BU_IfxYD.cjs} +4 -85
- package/dist/chunks/async-browser-BU_IfxYD.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-2vea86wD.esm.js → index-CaaQQlPJ.esm.js} +1116 -953
- package/dist/chunks/index-CaaQQlPJ.esm.js.map +1 -0
- package/dist/chunks/node-nKJBk8iJ.esm.js +96 -0
- package/dist/chunks/node-nKJBk8iJ.esm.js.map +1 -0
- package/dist/chunks/{index-VTO-b2vR.cjs → proxy-Dtg-bJ3T.cjs} +2285 -2983
- package/dist/chunks/proxy-Dtg-bJ3T.cjs.map +1 -0
- package/dist/chunks/{index-CtA2AWl3.esm.js → proxy-r7lARftl.esm.js} +2273 -2948
- package/dist/chunks/proxy-r7lARftl.esm.js.map +1 -0
- package/dist/debug.cjs +973 -28
- package/dist/debug.cjs.map +1 -1
- package/dist/debug.d.ts +36 -37
- package/dist/debug.esm.js +949 -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 +1447 -1
- package/dist/mutts.umd.js +7167 -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 +62 -60
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.ts +2 -2
- package/dist/node.dev.cjs +110 -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-W5vD6m2n.d.ts} +99 -44
- package/docs/ai/api-reference.md +11 -18
- package/docs/ai/manual.md +308 -230
- package/docs/reactive/advanced.md +312 -2
- package/docs/reactive/attend.md +1 -2
- package/docs/reactive/collections.md +21 -39
- package/docs/reactive/core.md +7 -37
- package/docs/reactive/debugging.md +117 -10
- package/docs/reactive/error-handling.md +10 -10
- package/docs/reactive/resource.md +125 -0
- package/docs/reactive/scan.md +32 -1
- package/docs/reactive.md +3 -2
- package/docs/utils.md +70 -0
- package/package.json +59 -30
- 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.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/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,318 @@
|
|
|
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, scan, cleanup,
|
|
25
|
+
atom, atomic, defer, untracked, unreactive, watch, when, biDi, caught, why,
|
|
26
|
+
cleanedBy, organized, Register,
|
|
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
|
+
})
|
|
61
|
+
|
|
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
|
+
```
|
|
29
69
|
|
|
30
|
-
###
|
|
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.
|
|
111
|
+
|
|
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
|
+
```
|
|
67
119
|
|
|
68
|
-
###
|
|
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.
|
|
77
135
|
|
|
78
136
|
```ts
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
137
|
+
// OK for debugging
|
|
138
|
+
console.log(obj._mutts_watchers)
|
|
139
|
+
|
|
140
|
+
// BAD — don't build logic on it
|
|
141
|
+
if (obj._mutts_isReactive) { ... }
|
|
82
142
|
```
|
|
83
143
|
|
|
84
|
-
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## 3. CORE REACTIVITY
|
|
147
|
+
|
|
148
|
+
### 3.1 reactive() — Trackable state
|
|
149
|
+
|
|
150
|
+
```ts
|
|
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
|
|
154
|
+
```
|
|
155
|
+
|
|
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
|
|
93
192
|
```
|
|
94
193
|
|
|
95
|
-
|
|
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
|
|
199
|
+
```
|
|
96
200
|
|
|
97
|
-
###
|
|
201
|
+
### 3.7 atom() — Immediate atomic execution
|
|
202
|
+
|
|
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
|
+
```
|
|
134
256
|
|
|
135
|
-
|
|
257
|
+
### 3.12 when() — Promise-based reactive wait
|
|
136
258
|
|
|
137
|
-
|
|
259
|
+
```ts
|
|
260
|
+
await when(() => state.loaded) // resolves when truthy
|
|
261
|
+
await when(() => state.ready, { timeout: 5000 }) // rejects after 5s
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
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
|
|
277
|
+
### 4.1 Register — Keyed ordered collection
|
|
151
278
|
|
|
152
279
|
```ts
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
280
|
+
const list = new Register(item => item.id, [{ id: 1, label: 'A' }])
|
|
281
|
+
list.get(1) // O(1) lookup by key
|
|
282
|
+
list.set(1, newItem) // update by key
|
|
283
|
+
list.remove(1) // remove by key
|
|
284
|
+
list.keep(x => x.active) // filter in-place
|
|
165
285
|
list.upsert(v => list.push(v), ...items) // update or insert
|
|
286
|
+
// Full array surface + CRUD events: on('add'|'delete'|'update'|'rekey', ...)
|
|
166
287
|
```
|
|
167
288
|
|
|
168
|
-
Full array surface (`map`, `filter`, `reduce`, `sort`, etc.) + CRUD events (`on('add', ...)`, `on('delete', ...)`, `on('update', ...)`, `on('rekey', ...)`).
|
|
169
|
-
|
|
170
289
|
---
|
|
171
290
|
|
|
172
|
-
##
|
|
291
|
+
## 5. COLLECTION TRANSFORMS
|
|
173
292
|
|
|
174
|
-
|
|
293
|
+
All transforms return reactive results. Cleanup via `result[cleanup]()`.
|
|
175
294
|
|
|
176
|
-
|
|
295
|
+
### 5.1 project() — Per-entry reactive map
|
|
177
296
|
|
|
178
297
|
```ts
|
|
179
|
-
import { project, cleanup } from 'mutts'
|
|
180
|
-
|
|
181
|
-
// Array
|
|
182
298
|
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]()
|
|
299
|
+
// names[0] recomputes ONLY when users[0] changes
|
|
300
|
+
// Variants: project.record(), project.map() — auto-dispatches by source type
|
|
196
301
|
```
|
|
197
302
|
|
|
198
303
|
**Access object**: `{ get(), set(v), key, source, old, value }`
|
|
199
304
|
|
|
200
|
-
###
|
|
201
|
-
|
|
202
|
-
Creates inner effect per key. Disposed when key disappears.
|
|
305
|
+
### 5.2 attend() — Per-key lifecycle
|
|
203
306
|
|
|
204
307
|
```ts
|
|
205
|
-
import { attend } from 'mutts'
|
|
206
|
-
|
|
207
308
|
attend(reactiveRecord, (key) => {
|
|
208
309
|
console.log(`${key} = ${reactiveRecord[key]}`)
|
|
209
|
-
return () => console.log(`cleanup: ${key}`)
|
|
310
|
+
return () => console.log(`cleanup: ${key}`) // disposed when key disappears
|
|
210
311
|
})
|
|
211
|
-
//
|
|
312
|
+
// Works with: arrays, Maps, Sets, or raw () => Iterable<Key>
|
|
212
313
|
```
|
|
213
314
|
|
|
214
|
-
###
|
|
315
|
+
### 5.3 organized() — Per-key record transform
|
|
215
316
|
|
|
216
317
|
```ts
|
|
217
318
|
const doubled = organized(source, (access, target) => {
|
|
@@ -220,41 +321,30 @@ const doubled = organized(source, (access, target) => {
|
|
|
220
321
|
})
|
|
221
322
|
```
|
|
222
323
|
|
|
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
|
|
324
|
+
### 5.4 scan() — Reactive accumulation
|
|
233
325
|
|
|
234
326
|
```ts
|
|
235
327
|
const result = scan(source, (acc, item) => acc + item.val, 0)
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
result[cleanup]()
|
|
328
|
+
// [1, 3, 6] — changing source[1] recomputes from index 1 onward
|
|
329
|
+
// Items must be objects (WeakMap keys). Move-optimized.
|
|
239
330
|
```
|
|
240
331
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
### 4.6 lift() — Sync a computed array/object
|
|
332
|
+
### 5.6 lift() — Sync a computed array/object
|
|
244
333
|
|
|
245
334
|
```ts
|
|
246
335
|
const filtered = lift(() => items.filter(x => x.active))
|
|
247
|
-
//
|
|
248
|
-
filtered[cleanup]()
|
|
336
|
+
// Element-wise diff — only changed elements sync, not full rebuild
|
|
249
337
|
```
|
|
250
338
|
|
|
339
|
+
**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.
|
|
340
|
+
|
|
341
|
+
**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.
|
|
342
|
+
|
|
251
343
|
---
|
|
252
344
|
|
|
253
|
-
##
|
|
345
|
+
## 6. EVOLUTION TRACKING
|
|
254
346
|
|
|
255
347
|
```ts
|
|
256
|
-
import { getState } from 'mutts/reactive'
|
|
257
|
-
|
|
258
348
|
let state = getState(obj)
|
|
259
349
|
effect(() => {
|
|
260
350
|
while ('evolution' in state) {
|
|
@@ -264,68 +354,68 @@ effect(() => {
|
|
|
264
354
|
})
|
|
265
355
|
```
|
|
266
356
|
|
|
267
|
-
**Recursive touching**: replacing an object with another of the same prototype triggers fine-grained per-property diffs, not
|
|
357
|
+
**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
358
|
|
|
269
359
|
---
|
|
270
360
|
|
|
271
|
-
##
|
|
361
|
+
## 7. ERROR HANDLING
|
|
272
362
|
|
|
273
363
|
```ts
|
|
274
|
-
import { onEffectThrow } from 'mutts'
|
|
275
|
-
|
|
276
364
|
effect(() => {
|
|
277
|
-
|
|
278
|
-
console.error('Caught:', error)
|
|
365
|
+
caught((error) => {
|
|
279
366
|
// return without throwing = handled
|
|
280
367
|
// throw = try next handler
|
|
281
|
-
// return function = cleanup on
|
|
368
|
+
// return function = cleanup on disposal
|
|
282
369
|
})
|
|
283
370
|
// ... code that might throw
|
|
284
371
|
})
|
|
285
372
|
```
|
|
286
373
|
|
|
287
|
-
- Multiple handlers tried in order. Unhandled
|
|
288
|
-
- **Must register before throwing code** (
|
|
289
|
-
- Does **not** catch async errors — use `.catch()
|
|
374
|
+
- Multiple handlers, tried in order. Unhandled → propagate to parent effect.
|
|
375
|
+
- **Must register before throwing code** (cleared on re-run).
|
|
376
|
+
- Does **not** catch async errors — use `.catch()`.
|
|
290
377
|
|
|
291
378
|
---
|
|
292
379
|
|
|
293
|
-
##
|
|
380
|
+
## 8. DEBUGGING & OPTIONS
|
|
294
381
|
|
|
295
382
|
```ts
|
|
296
|
-
import { reactiveOptions } from 'mutts
|
|
383
|
+
import { reactiveOptions } from 'mutts'
|
|
297
384
|
|
|
298
|
-
// Cycle detection
|
|
299
|
-
reactiveOptions.cycleHandling = 'development' // default
|
|
385
|
+
// Cycle detection
|
|
386
|
+
reactiveOptions.cycleHandling = 'development' // default: graph-based, throws
|
|
300
387
|
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
|
-
}
|
|
388
|
+
reactiveOptions.cycleHandling = 'production' // heuristic via maxTriggerPerBatch
|
|
307
389
|
|
|
308
|
-
// Lifecycle hooks
|
|
309
|
-
reactiveOptions.enter = (effect) => {
|
|
310
|
-
reactiveOptions.leave = (effect) => {
|
|
311
|
-
reactiveOptions.touched = (obj, evolution) => {
|
|
390
|
+
// Lifecycle hooks (all wrapped via optionCall for safety)
|
|
391
|
+
reactiveOptions.enter = (effect) => {} // before effect runs
|
|
392
|
+
reactiveOptions.leave = (effect) => {} // after effect runs
|
|
393
|
+
reactiveOptions.touched = (obj, evolution, props, effects) => {}
|
|
394
|
+
reactiveOptions.beginChain = (roots) => {} // before batch
|
|
395
|
+
reactiveOptions.garbageCollected = (fn) => {} // effect GC'd
|
|
396
|
+
reactiveOptions.skipRunningEffect = (fn) => {} // effect skipped (already running)
|
|
397
|
+
reactiveOptions.onMemoizationDiscrepancy = (cached, fresh, fn, args, cause) => {}
|
|
312
398
|
|
|
313
399
|
// Introspection (memory-intensive, dev only)
|
|
314
|
-
import {
|
|
315
|
-
|
|
400
|
+
import { reactiveOptions } from 'mutts'
|
|
401
|
+
import { buildReactivityGraph, getMutationHistory } from 'mutts/debug'
|
|
402
|
+
|
|
403
|
+
reactiveOptions.introspection = { enableHistory: true, historySize: 100 }
|
|
404
|
+
const history = getMutationHistory()
|
|
405
|
+
const graph = buildReactivityGraph()
|
|
316
406
|
```
|
|
317
407
|
|
|
318
|
-
**ReactiveError.debugInfo
|
|
408
|
+
**ReactiveError.debugInfo**: `causalChain`, `creationStack`, `cycle` (for CYCLE_DETECTED).
|
|
319
409
|
|
|
320
410
|
---
|
|
321
411
|
|
|
322
|
-
##
|
|
412
|
+
## 9. ZONES (Async Context)
|
|
323
413
|
|
|
324
414
|
```ts
|
|
325
|
-
import { Zone, asyncZone } from 'mutts
|
|
415
|
+
import { Zone, asyncZone } from 'mutts'
|
|
326
416
|
|
|
327
417
|
const requestId = new Zone<string>()
|
|
328
|
-
asyncZone.add(requestId)
|
|
418
|
+
asyncZone.add(requestId) // register for async propagation
|
|
329
419
|
|
|
330
420
|
requestId.with('req-123', async () => {
|
|
331
421
|
await somePromise()
|
|
@@ -333,118 +423,106 @@ requestId.with('req-123', async () => {
|
|
|
333
423
|
})
|
|
334
424
|
```
|
|
335
425
|
|
|
336
|
-
-
|
|
337
|
-
-
|
|
338
|
-
-
|
|
339
|
-
-
|
|
340
|
-
- Reactivity uses zones internally (`effectHistory`
|
|
426
|
+
- **`Zone<T>`**: stack-based. `.with(value, fn)`, `.active`, `.root(fn)`.
|
|
427
|
+
- **`ZoneHistory<T>`**: extends Zone, tracks history set for cycle detection.
|
|
428
|
+
- **`ZoneAggregator`**: combines zones. `asyncZone` is the global one.
|
|
429
|
+
- **`.zoned`**: snapshot context for manual bridging into unmanaged callbacks.
|
|
430
|
+
- Reactivity uses zones internally (`effectHistory` tracks active effect across await).
|
|
431
|
+
- **Node**: uses `AsyncLocalStorage`. **Browser**: monkey-patches Promise/setTimeout (less robust).
|
|
341
432
|
|
|
342
433
|
---
|
|
343
434
|
|
|
344
|
-
##
|
|
435
|
+
## 10. OTHER MODULES
|
|
345
436
|
|
|
346
|
-
###
|
|
437
|
+
### 10.1 Decorators
|
|
347
438
|
|
|
348
|
-
Unified system for legacy (`experimentalDecorators`) and modern (Stage 3):
|
|
349
439
|
```ts
|
|
350
|
-
import { decorator } from 'mutts/decorator'
|
|
351
440
|
const myDec = decorator({ method(original, name) { ... }, class(target) { ... } })
|
|
441
|
+
// Works with both legacy (experimentalDecorators) and Stage 3 decorators
|
|
442
|
+
// Built-in: @cached, @debounce(ms), @throttle(ms), @deprecated(msg)
|
|
352
443
|
```
|
|
353
444
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
### 9.2 Mixin (`mutts/mixin`)
|
|
445
|
+
### 10.2 Mixin
|
|
357
446
|
|
|
358
447
|
```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
|
|
448
|
+
const Countable = mixin((base) => class extends base { count = 0 })
|
|
449
|
+
class A extends Countable { } // as base
|
|
450
|
+
class B extends Countable(Other) { } // as mixin (cached per base)
|
|
365
451
|
```
|
|
366
452
|
|
|
367
|
-
###
|
|
453
|
+
### 10.3 Eventful
|
|
368
454
|
|
|
369
|
-
Type-safe event system:
|
|
370
455
|
```ts
|
|
371
|
-
import { Eventful } from 'mutts/eventful'
|
|
372
456
|
interface MyEvents { click: (x: number, y: number) => void }
|
|
373
457
|
class Button extends Eventful<MyEvents> { }
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
btn.
|
|
377
|
-
btn.
|
|
378
|
-
btn.off.click()
|
|
379
|
-
const unhook = btn.hook((event, ...args) => { ... }) // global listener
|
|
458
|
+
btn.on.click((x, y) => {}) // dot notation subscribe
|
|
459
|
+
btn.emit.click(100, 200) // emit
|
|
460
|
+
btn.off.click() // unsubscribe all
|
|
461
|
+
btn.hook((event, ...args) => {}) // global listener
|
|
380
462
|
```
|
|
381
463
|
|
|
382
|
-
###
|
|
464
|
+
### 10.4 Destroyable
|
|
383
465
|
|
|
384
|
-
Resource management with `FinalizationRegistry` and `Symbol.dispose`:
|
|
385
466
|
```ts
|
|
386
|
-
import { Destroyable, allocated, destructor } from 'mutts/destroyable'
|
|
387
|
-
|
|
388
467
|
class FileHandler extends Destroyable() {
|
|
389
468
|
@allocated accessor filePath: string
|
|
390
469
|
[destructor](alloc) { console.log(`Closing: ${alloc.filePath}`) }
|
|
391
470
|
}
|
|
392
|
-
// Destroyed objects throw DestructionError on access
|
|
393
|
-
//
|
|
471
|
+
// Destroyed objects throw DestructionError on access
|
|
472
|
+
// Destroyable.destroy(instance) or `using` statement
|
|
394
473
|
```
|
|
395
474
|
|
|
396
|
-
###
|
|
475
|
+
### 10.5 Flavored Functions
|
|
397
476
|
|
|
398
|
-
Chainable property modifiers on functions:
|
|
399
477
|
```ts
|
|
400
|
-
import { flavored, flavorOptions } from 'mutts'
|
|
401
478
|
const greet = flavored(
|
|
402
479
|
(name: string, opts?: { loud?: boolean }) => opts?.loud ? name.toUpperCase() : name,
|
|
403
480
|
{ get loud() { return flavorOptions(this, { loud: true }) } }
|
|
404
481
|
)
|
|
405
|
-
greet.loud('hi') // "HI"
|
|
482
|
+
greet.loud('hi') // "HI" — chainable property modifiers
|
|
406
483
|
```
|
|
407
484
|
|
|
408
|
-
###
|
|
485
|
+
### 10.6 Indexable
|
|
409
486
|
|
|
410
|
-
Numeric index access (`obj[0]`) on custom classes via Proxy:
|
|
411
487
|
```ts
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
set(index, v) { this.data[index] = v }
|
|
488
|
+
const MyCol = Indexable(Base, {
|
|
489
|
+
get(i) { return this.data[i] },
|
|
490
|
+
set(i, v) { this.data[i] = v }
|
|
416
491
|
})
|
|
492
|
+
// Enables obj[0] numeric index access on custom classes via Proxy
|
|
417
493
|
```
|
|
418
494
|
|
|
419
|
-
###
|
|
495
|
+
### 10.7 PromiseChain
|
|
420
496
|
|
|
421
|
-
Fluent chaining on Promises without intermediate `await`:
|
|
422
497
|
```ts
|
|
423
|
-
import { chainPromise } from 'mutts/promiseChain'
|
|
424
498
|
const theme = await chainPromise(api.getUser('123')).getProfile().getSettings().theme
|
|
499
|
+
// Fluent chaining on Promises without intermediate await
|
|
425
500
|
```
|
|
426
501
|
|
|
427
502
|
---
|
|
428
503
|
|
|
429
|
-
##
|
|
430
|
-
|
|
431
|
-
### Affirmative State
|
|
432
|
-
Declare `Y = f(X)`. Do NOT say "when X changes, update Y". The system ensures consistency.
|
|
504
|
+
## 11. PHILOSOPHY
|
|
433
505
|
|
|
434
|
-
|
|
435
|
-
|
|
506
|
+
- **Affirmative state**: Declare `Y = f(X)`. Don't say "when X changes, update Y".
|
|
507
|
+
- **Events are legacy**: Use reactive derivations (`effect`, `memoize`, `project`) for internal logic. Events only for DOM/external APIs.
|
|
508
|
+
- **Cleanup ≠ undo**: Cleanup releases subscriptions, does NOT undo side effects.
|
|
436
509
|
|
|
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 |
|
|
510
|
+
---
|
|
450
511
|
|
|
512
|
+
## 12. QUICK REFERENCE — DO vs DON'T
|
|
513
|
+
|
|
514
|
+
| DO | DON'T |
|
|
515
|
+
|----|-------|
|
|
516
|
+
| `morph(arr, fn)` | `arr.map(fn)` for reactive transforms |
|
|
517
|
+
| `effect(() => { state.x })` | bare `state.x` outside effect |
|
|
518
|
+
| `defer(() => state.y = val)` | `queueMicrotask(() => state.y = val)` |
|
|
519
|
+
| `memoize((obj) => obj.n * 2)` | `memoize((n: number) => n * 2)` |
|
|
520
|
+
| `atom(() => { a=1; b=2 })` | sequential mutations (2 effect runs) |
|
|
521
|
+
| `untracked(() => state.x)` | reading state you don't want tracked |
|
|
522
|
+
| `watch(() => state.x, cb)` | manual dirty-checking |
|
|
523
|
+
| `arr.splice(0)` or `arr.length = 0` | manual loop to clear arrays |
|
|
524
|
+
| `isReactive(obj)` to check | `obj._mutts_isReactive` |
|
|
525
|
+
| Store `stop = effect(...)` | letting effect GC unintentionally |
|
|
526
|
+
| `import { x } from 'mutts'` | `import { x } from 'mutts/reactive'` (no subpaths) |
|
|
527
|
+
| `cleanedBy(obj, fn)` for cleanup | manual `obj[cleanup] = fn` |
|
|
528
|
+
| `memoize(() => a + b)` for computed | `effect` + manual state sync |
|