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.
Files changed (204) hide show
  1. package/README.md +58 -33
  2. package/dist/browser.cjs +1371 -1206
  3. package/dist/browser.cjs.map +1 -1
  4. package/dist/browser.d.ts +2 -1392
  5. package/dist/browser.dev.cjs +110 -0
  6. package/dist/browser.dev.cjs.map +1 -0
  7. package/dist/browser.dev.d.ts +2 -0
  8. package/dist/browser.dev.esm.js +5 -0
  9. package/dist/browser.dev.esm.js.map +1 -0
  10. package/dist/browser.esm.js +6 -87
  11. package/dist/browser.esm.js.map +1 -1
  12. package/dist/chunks/{async-browser-lvzLOCgk.cjs → async-browser-BU_IfxYD.cjs} +4 -85
  13. package/dist/chunks/async-browser-BU_IfxYD.cjs.map +1 -0
  14. package/dist/chunks/{async-node-C3DeIb0y.cjs → async-node-3PrbVAbB.cjs} +3 -1
  15. package/dist/chunks/async-node-3PrbVAbB.cjs.map +1 -0
  16. package/dist/chunks/{index-2vea86wD.esm.js → index-CaaQQlPJ.esm.js} +1116 -953
  17. package/dist/chunks/index-CaaQQlPJ.esm.js.map +1 -0
  18. package/dist/chunks/node-nKJBk8iJ.esm.js +96 -0
  19. package/dist/chunks/node-nKJBk8iJ.esm.js.map +1 -0
  20. package/dist/chunks/{index-VTO-b2vR.cjs → proxy-Dtg-bJ3T.cjs} +2285 -2983
  21. package/dist/chunks/proxy-Dtg-bJ3T.cjs.map +1 -0
  22. package/dist/chunks/{index-CtA2AWl3.esm.js → proxy-r7lARftl.esm.js} +2273 -2948
  23. package/dist/chunks/proxy-r7lARftl.esm.js.map +1 -0
  24. package/dist/debug.cjs +973 -28
  25. package/dist/debug.cjs.map +1 -1
  26. package/dist/debug.d.ts +36 -37
  27. package/dist/debug.esm.js +949 -1
  28. package/dist/debug.esm.js.map +1 -1
  29. package/dist/devtools/manifest.json +1 -1
  30. package/dist/devtools/panel.html +1 -1
  31. package/dist/devtools/panel.js +107 -94
  32. package/dist/devtools/panel.js.map +1 -1
  33. package/dist/index.d.ts +1447 -1
  34. package/dist/mutts.umd.js +7167 -1
  35. package/dist/mutts.umd.js.map +1 -1
  36. package/dist/mutts.umd.min.js +1 -1
  37. package/dist/mutts.umd.min.js.map +1 -1
  38. package/dist/node.cjs +62 -60
  39. package/dist/node.cjs.map +1 -1
  40. package/dist/node.d.ts +2 -2
  41. package/dist/node.dev.cjs +110 -0
  42. package/dist/node.dev.cjs.map +1 -0
  43. package/dist/node.dev.d.ts +2 -0
  44. package/dist/node.dev.esm.js +6 -0
  45. package/dist/node.dev.esm.js.map +1 -0
  46. package/dist/node.esm.js +4 -97
  47. package/dist/node.esm.js.map +1 -1
  48. package/dist/{types-DaHFfhlN.d.ts → types-W5vD6m2n.d.ts} +99 -44
  49. package/docs/ai/api-reference.md +11 -18
  50. package/docs/ai/manual.md +308 -230
  51. package/docs/reactive/advanced.md +312 -2
  52. package/docs/reactive/attend.md +1 -2
  53. package/docs/reactive/collections.md +21 -39
  54. package/docs/reactive/core.md +7 -37
  55. package/docs/reactive/debugging.md +117 -10
  56. package/docs/reactive/error-handling.md +10 -10
  57. package/docs/reactive/resource.md +125 -0
  58. package/docs/reactive/scan.md +32 -1
  59. package/docs/reactive.md +3 -2
  60. package/docs/utils.md +70 -0
  61. package/package.json +59 -30
  62. package/dist/chunks/async-browser-lvzLOCgk.cjs.map +0 -1
  63. package/dist/chunks/async-node-C3DeIb0y.cjs.map +0 -1
  64. package/dist/chunks/index-2vea86wD.esm.js.map +0 -1
  65. package/dist/chunks/index-CtA2AWl3.esm.js.map +0 -1
  66. package/dist/chunks/index-VTO-b2vR.cjs.map +0 -1
  67. package/dist/debug/debug.d.ts +0 -122
  68. package/dist/debug/debug.d.ts.map +0 -1
  69. package/dist/debug/index.d.ts +0 -4
  70. package/dist/debug/index.d.ts.map +0 -1
  71. package/dist/debug/lineage-panel.d.ts +0 -5
  72. package/dist/debug/lineage-panel.d.ts.map +0 -1
  73. package/dist/debug/lineage.d.ts +0 -79
  74. package/dist/debug/lineage.d.ts.map +0 -1
  75. package/dist/src/async/browser.d.ts +0 -2
  76. package/dist/src/async/browser.d.ts.map +0 -1
  77. package/dist/src/async/index.d.ts +0 -19
  78. package/dist/src/async/index.d.ts.map +0 -1
  79. package/dist/src/async/node.d.ts +0 -2
  80. package/dist/src/async/node.d.ts.map +0 -1
  81. package/dist/src/decorator.d.ts +0 -106
  82. package/dist/src/decorator.d.ts.map +0 -1
  83. package/dist/src/destroyable.d.ts +0 -87
  84. package/dist/src/destroyable.d.ts.map +0 -1
  85. package/dist/src/entry-browser.d.ts +0 -3
  86. package/dist/src/entry-browser.d.ts.map +0 -1
  87. package/dist/src/entry-node.d.ts +0 -3
  88. package/dist/src/entry-node.d.ts.map +0 -1
  89. package/dist/src/eventful.d.ts +0 -20
  90. package/dist/src/eventful.d.ts.map +0 -1
  91. package/dist/src/flavored.d.ts +0 -33
  92. package/dist/src/flavored.d.ts.map +0 -1
  93. package/dist/src/index.d.ts +0 -14
  94. package/dist/src/index.d.ts.map +0 -1
  95. package/dist/src/indexable.d.ts +0 -243
  96. package/dist/src/indexable.d.ts.map +0 -1
  97. package/dist/src/introspection.d.ts +0 -27
  98. package/dist/src/introspection.d.ts.map +0 -1
  99. package/dist/src/iterableWeak.d.ts +0 -53
  100. package/dist/src/iterableWeak.d.ts.map +0 -1
  101. package/dist/src/mixins.d.ts +0 -25
  102. package/dist/src/mixins.d.ts.map +0 -1
  103. package/dist/src/promiseChain.d.ts +0 -20
  104. package/dist/src/promiseChain.d.ts.map +0 -1
  105. package/dist/src/reactive/array.d.ts +0 -48
  106. package/dist/src/reactive/array.d.ts.map +0 -1
  107. package/dist/src/reactive/buffer.d.ts +0 -120
  108. package/dist/src/reactive/buffer.d.ts.map +0 -1
  109. package/dist/src/reactive/change.d.ts +0 -29
  110. package/dist/src/reactive/change.d.ts.map +0 -1
  111. package/dist/src/reactive/deep-touch.d.ts +0 -28
  112. package/dist/src/reactive/deep-touch.d.ts.map +0 -1
  113. package/dist/src/reactive/deep-watch-state.d.ts +0 -25
  114. package/dist/src/reactive/deep-watch-state.d.ts.map +0 -1
  115. package/dist/src/reactive/deep-watch.d.ts +0 -20
  116. package/dist/src/reactive/deep-watch.d.ts.map +0 -1
  117. package/dist/src/reactive/describe.d.ts +0 -12
  118. package/dist/src/reactive/describe.d.ts.map +0 -1
  119. package/dist/src/reactive/effect-context.d.ts +0 -34
  120. package/dist/src/reactive/effect-context.d.ts.map +0 -1
  121. package/dist/src/reactive/effects.d.ts +0 -164
  122. package/dist/src/reactive/effects.d.ts.map +0 -1
  123. package/dist/src/reactive/index.d.ts +0 -19
  124. package/dist/src/reactive/index.d.ts.map +0 -1
  125. package/dist/src/reactive/map.d.ts +0 -28
  126. package/dist/src/reactive/map.d.ts.map +0 -1
  127. package/dist/src/reactive/memoize.d.ts +0 -28
  128. package/dist/src/reactive/memoize.d.ts.map +0 -1
  129. package/dist/src/reactive/non-reactive-state.d.ts +0 -9
  130. package/dist/src/reactive/non-reactive-state.d.ts.map +0 -1
  131. package/dist/src/reactive/non-reactive.d.ts +0 -11
  132. package/dist/src/reactive/non-reactive.d.ts.map +0 -1
  133. package/dist/src/reactive/project.d.ts +0 -40
  134. package/dist/src/reactive/project.d.ts.map +0 -1
  135. package/dist/src/reactive/proxy-state.d.ts +0 -8
  136. package/dist/src/reactive/proxy-state.d.ts.map +0 -1
  137. package/dist/src/reactive/proxy.d.ts +0 -23
  138. package/dist/src/reactive/proxy.d.ts.map +0 -1
  139. package/dist/src/reactive/record.d.ts +0 -115
  140. package/dist/src/reactive/record.d.ts.map +0 -1
  141. package/dist/src/reactive/register.d.ts +0 -125
  142. package/dist/src/reactive/register.d.ts.map +0 -1
  143. package/dist/src/reactive/registry.d.ts +0 -21
  144. package/dist/src/reactive/registry.d.ts.map +0 -1
  145. package/dist/src/reactive/set.d.ts +0 -26
  146. package/dist/src/reactive/set.d.ts.map +0 -1
  147. package/dist/src/reactive/tracking.d.ts +0 -7
  148. package/dist/src/reactive/tracking.d.ts.map +0 -1
  149. package/dist/src/reactive/types.d.ts +0 -424
  150. package/dist/src/reactive/types.d.ts.map +0 -1
  151. package/dist/src/reactive/watch.d.ts +0 -48
  152. package/dist/src/reactive/watch.d.ts.map +0 -1
  153. package/dist/src/std-decorators.d.ts +0 -45
  154. package/dist/src/std-decorators.d.ts.map +0 -1
  155. package/dist/src/utils.d.ts +0 -49
  156. package/dist/src/utils.d.ts.map +0 -1
  157. package/dist/src/zone.d.ts +0 -40
  158. package/dist/src/zone.d.ts.map +0 -1
  159. package/docs/reactive/describe.md +0 -85
  160. package/docs/reactive/project.md +0 -93
  161. package/src/async/browser.ts +0 -323
  162. package/src/async/index.ts +0 -27
  163. package/src/async/node.ts +0 -92
  164. package/src/decorator.ts +0 -272
  165. package/src/destroyable.ts +0 -199
  166. package/src/entry-browser.ts +0 -5
  167. package/src/entry-node.ts +0 -5
  168. package/src/eventful.ts +0 -110
  169. package/src/flavored.ts +0 -106
  170. package/src/index.d.ts +0 -12
  171. package/src/index.ts +0 -64
  172. package/src/indexable.ts +0 -526
  173. package/src/introspection.ts +0 -59
  174. package/src/iterableWeak.ts +0 -233
  175. package/src/mixins.ts +0 -123
  176. package/src/promiseChain.ts +0 -110
  177. package/src/reactive/array.ts +0 -500
  178. package/src/reactive/buffer.ts +0 -328
  179. package/src/reactive/change.ts +0 -131
  180. package/src/reactive/deep-touch.ts +0 -273
  181. package/src/reactive/deep-watch-state.ts +0 -82
  182. package/src/reactive/deep-watch.ts +0 -171
  183. package/src/reactive/describe.ts +0 -39
  184. package/src/reactive/effect-context.ts +0 -83
  185. package/src/reactive/effects.ts +0 -1434
  186. package/src/reactive/index.ts +0 -72
  187. package/src/reactive/map.ts +0 -142
  188. package/src/reactive/memoize.ts +0 -186
  189. package/src/reactive/non-reactive-state.ts +0 -49
  190. package/src/reactive/non-reactive.ts +0 -43
  191. package/src/reactive/project.md +0 -107
  192. package/src/reactive/project.ts +0 -430
  193. package/src/reactive/proxy-state.ts +0 -27
  194. package/src/reactive/proxy.ts +0 -282
  195. package/src/reactive/record.ts +0 -181
  196. package/src/reactive/register.ts +0 -538
  197. package/src/reactive/registry.ts +0 -72
  198. package/src/reactive/set.ts +0 -117
  199. package/src/reactive/tracking.ts +0 -41
  200. package/src/reactive/types.ts +0 -520
  201. package/src/reactive/watch.ts +0 -180
  202. package/src/std-decorators.ts +0 -256
  203. package/src/utils.ts +0 -300
  204. 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** — This document contains everything needed to use `mutts` as a dependency. It is the single source of truth for correct usage. Do not rely on prior training data about this library. Companion file: [api-reference.md](./api-reference.md) for full type signatures.
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. WHAT IS MUTTS
7
+ ## 1. MENTAL MODEL
8
8
 
9
- A proxy-based **fine-grained reactivity system** for TypeScript/JavaScript. No VDOM, no scheduler — changes propagate synchronously through dependency-tracked effects. Also provides: decorator unification, class mixins, event system, resource management, promise chaining, and zones (async context propagation).
9
+ Proxy-based **fine-grained reactivity** — changes propagate synchronously through dependency-tracked effects. No VDOM, no scheduler, no hooks.
10
10
 
11
- **Entry points**: `mutts/browser` (DOM), `mutts/node` (AsyncLocalStorage). Plain `import { ... } from 'mutts'` auto-selects via `package.json` exports.
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. CORE REACTIVITY
39
+ ## 2. CRITICAL TRAPS
16
40
 
17
- ### 2.1 reactive() Make objects trackable
41
+ ### TRAP 1: Memoize with primitives
42
+ `memoize` uses WeakMap — args MUST be objects/symbols, NOT primitives.
18
43
 
19
44
  ```ts
20
- import { reactive, effect } from 'mutts'
45
+ // BAD
46
+ const double = memoize((n: number) => n * 2) // WeakMap can't key on number
21
47
 
22
- const state = reactive({ count: 0, name: 'John' })
48
+ // GOOD
49
+ const double = memoize((obj: { n: number }) => obj.n * 2)
23
50
  ```
24
51
 
25
- - Wraps an object in a `Proxy` that tracks property reads (dependencies) and writes (notifications).
26
- - Same object always returns the same proxy.
27
- - Works with: plain objects, `Object.create(null)`, class instances, arrays, Map, Set, WeakMap, WeakSet.
28
- - `unwrap(proxy)` recovers the original. `isReactive(obj)` checks.
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
- ### 2.2 effect() React to changes
70
+ ### TRAP 3: Self-triggering effects (cycles)
71
+ Effect reads and writes same state → infinite loop.
31
72
 
32
73
  ```ts
33
- const stop = effect(({ reaction, tracked, ascend }) => {
34
- console.log(state.count) // tracked automatically
35
- return () => { /* cleanup before next run or disposal */ }
74
+ // BAD cycle
75
+ effect(() => {
76
+ state.count = state.count + 1 // reads count, writes count re-triggers
36
77
  })
37
78
 
38
- state.count++ // triggers effect
39
- stop() // disposes effect permanently
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
- - **Auto-tracks**: any reactive property read inside the callback becomes a dependency.
43
- - **Cleanup**: return a function called before each re-run and on disposal.
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
- effect.opaque(() => { ... }) // identity-only tracking (no deep touch)
53
- effect.named('label')(() => { ... }) // named for debugging
54
- effect.opaque.named('x')(() => { ... })
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
- ### 2.3 memoize() Cached computed values
101
+ ### TRAP 5: Array clearing
102
+ `array.length = 0` works (triggers all indices + length). `splice(0)` is equivalent.
58
103
 
59
104
  ```ts
60
- const doubled = memoize((obj: { value: number }) => obj.value * 2)
61
- // Arguments must be WeakMap-compatible (objects/symbols). No primitives.
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
- As decorator: `@memoize` on getters (per-instance cache) or methods (per-instance+args).
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
- ### 2.4 untracked() Escape tracking
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
- untracked(() => {
72
- // Reactive reads here are NOT tracked by the enclosing effect
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
- ### 2.5 unreactive() Opt out
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
- unreactive(obj) // mark object as non-reactive
80
- unreactive(MyClass) // mark entire class
81
- @unreactive('id', 'meta') // mark specific properties on a @reactive class
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
- ### 2.6 atomic() — Batch mutations
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 updateBoth = atomic((a, b) => {
88
- state.a = a
89
- state.b = b
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
- updateBoth(10, 20)
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
- Also works as `@atomic` decorator on class methods.
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
- ### 2.7 defer() / addBatchCleanup() Avoid cycles
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 }) // runs after batch completes
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
- ### 2.8 biDi() — Bidirectional binding
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) => inputElement.value = v, // external setter
222
+ (v) => input.value = v, // external setter
115
223
  { get: () => model.value, set: (v) => model.value = v }
116
224
  )
117
- inputElement.addEventListener('input', () => provide(inputElement.value))
225
+ input.addEventListener('input', () => provide(input.value))
226
+ // Prevents infinite loops automatically
118
227
  ```
119
228
 
120
- ### 2.9 watch() — Observe changes
229
+ ### 3.10 watch() — Observe changes
121
230
 
122
231
  ```ts
123
- // Watch a specific derivation
124
- watch(() => state.count, (newVal, oldVal) => { ... })
232
+ watch(() => state.count, (newVal, oldVal) => {}) // specific derivation
233
+ watch(state, () => {}) // any property
234
+ watch(state, () => {}, { deep: true }) // deep (expensive)
235
+ ```
125
236
 
126
- // Watch any property on an object
127
- watch(state, () => { ... })
237
+ ### 3.10 cleanedBy() Attach cleanup to objects
128
238
 
129
- // Deep watch (higher overhead)
130
- watch(state, () => { ... }, { deep: true })
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
- ## 3. COLLECTIONS
257
+ ### 3.12 when() — Promise-based reactive wait
136
258
 
137
- All standard collections get reactive wrappers via `reactive()`:
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 (allProps) |
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
- 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
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
- ## 4. COLLECTION TRANSFORMS
291
+ ## 5. COLLECTION TRANSFORMS
173
292
 
174
- ### 4.1 project() Per-entry reactive map
293
+ All transforms return reactive results. Cleanup via `result[cleanup]()`.
175
294
 
176
- The reactive replacement for `.map()`. Each entry gets its own effect — only changed entries recompute.
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 only when users[0] changes
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
- ### 4.2 attend() — Per-key lifecycle
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
- // Also works with: arrays, Maps, Sets, or raw () => Iterable<Key>
312
+ // Works with: arrays, Maps, Sets, or raw () => Iterable<Key>
212
313
  ```
213
314
 
214
- ### 4.3 organized() — Per-key record transform with side effects
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
- ### 4.4 describe() — Reactive Object.defineProperties
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
- // result is reactive array of intermediates: [1, 3, 6]
237
- // Changing source[1].val only recomputes from index 1 onward
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
- Items must be objects (WeakMap keys). Move-optimized (reorders reuse cached intermediates).
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
- // Only changed elements sync — element-wise diff, not full rebuild
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
- ## 5. EVOLUTION TRACKING
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 a wholesale parent notification. Disable with `reactiveOptions.recursiveTouching = false`.
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
- ## 6. ERROR HANDLING
361
+ ## 7. ERROR HANDLING
272
362
 
273
363
  ```ts
274
- import { onEffectThrow } from 'mutts'
275
-
276
364
  effect(() => {
277
- onEffectThrow((error) => {
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 effect disposal
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 errors propagate to parent effect chain.
288
- - **Must register before throwing code** (handlers cleared on re-run).
289
- - Does **not** catch async errors — use `.catch()` on Promises.
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
- ## 7. DEBUGGING
380
+ ## 8. DEBUGGING & OPTIONS
294
381
 
295
382
  ```ts
296
- import { reactiveOptions } from 'mutts/reactive'
383
+ import { reactiveOptions } from 'mutts'
297
384
 
298
- // Cycle detection modes
299
- reactiveOptions.cycleHandling = 'development' // default graph-based, throws immediately
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 (fastest)
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 { enableIntrospection, getDependencyGraph, getMutationHistory } from 'mutts/introspection'
315
- enableIntrospection({ historySize: 100 })
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** contains: `causalChain`, `creationStack`, `cycle` (for CYCLE_DETECTED).
408
+ **ReactiveError.debugInfo**: `causalChain`, `creationStack`, `cycle` (for CYCLE_DETECTED).
319
409
 
320
410
  ---
321
411
 
322
- ## 8. ZONES (Async Context)
412
+ ## 9. ZONES (Async Context)
323
413
 
324
414
  ```ts
325
- import { Zone, asyncZone } from 'mutts/zone'
415
+ import { Zone, asyncZone } from 'mutts'
326
416
 
327
417
  const requestId = new Zone<string>()
328
- asyncZone.add(requestId) // register for async propagation
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
- - `Zone<T>`: stack-based storage. `.with(value, fn)`, `.active`, `.root(fn)`.
337
- - `ZoneHistory<T>`: tracks history for cycle detection.
338
- - `ZoneAggregator`: combines multiple zones. `asyncZone` is the global one.
339
- - `.zoned`: snapshot current context for manual bridging into unmanaged callbacks.
340
- - Reactivity uses zones internally (`effectHistory` zone tracks active effect across await).
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
- ## 9. OTHER MODULES
435
+ ## 10. OTHER MODULES
345
436
 
346
- ### 9.1 Decorators (`mutts/decorator`)
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
- Standard decorators: `@cached`, `@debounce(ms)`, `@throttle(ms)`, `@deprecated(msg)`.
355
-
356
- ### 9.2 Mixin (`mutts/mixin`)
445
+ ### 10.2 Mixin
357
446
 
358
447
  ```ts
359
- import { mixin } from 'mutts/mixin'
360
- const Countable = mixin((base) => class extends base { count = 0; increment() { this.count++ } })
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
- ### 9.3 Eventful (`mutts/eventful`)
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
- const btn = new Button()
376
- btn.on.click((x, y) => { ... }) // dot notation
377
- btn.emit.click(100, 200)
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
- ### 9.4 Destroyable (`mutts/destroyable`)
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
- // Explicit: Destroyable.destroy(instance) or future `using` statement.
471
+ // Destroyed objects throw DestructionError on access
472
+ // Destroyable.destroy(instance) or `using` statement
394
473
  ```
395
474
 
396
- ### 9.5 Flavored Functions (`mutts/flavored`)
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
- ### 9.6 Indexable (`mutts/indexable`)
485
+ ### 10.6 Indexable
409
486
 
410
- Numeric index access (`obj[0]`) on custom classes via Proxy:
411
487
  ```ts
412
- import { Indexable } from 'mutts/indexable'
413
- const MyCollection = Indexable(BaseClass, {
414
- get(index) { return this.data[index] },
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
- ### 9.7 PromiseChain (`mutts/promiseChain`)
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
- ## 10. PHILOSOPHY & TRAPS
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
- ### Events Are Legacy
435
- Only use events for DOM interaction or external APIs. Internal logic should use reactive derivations (`effect`, `memoize`, `project`).
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
- ### Cleanup Semantics
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 |