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.
Files changed (206) hide show
  1. package/README.md +60 -35
  2. package/dist/browser.cjs +1333 -1760
  3. package/dist/browser.cjs.map +1 -1
  4. package/dist/browser.d.ts +2 -1392
  5. package/dist/browser.dev.cjs +114 -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 +18 -97
  11. package/dist/browser.esm.js.map +1 -1
  12. package/dist/chunks/{async-browser-lvzLOCgk.cjs → async-browser-Dgr5CreQ.cjs} +16 -95
  13. package/dist/chunks/async-browser-Dgr5CreQ.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-Sf74wXTV.esm.js +2577 -0
  17. package/dist/chunks/index-Sf74wXTV.esm.js.map +1 -0
  18. package/dist/chunks/node-Bo7WU5S2.esm.js +96 -0
  19. package/dist/chunks/node-Bo7WU5S2.esm.js.map +1 -0
  20. package/dist/chunks/{index-VTO-b2vR.cjs → proxy-Cc79Lrzj.cjs} +2593 -3216
  21. package/dist/chunks/proxy-Cc79Lrzj.cjs.map +1 -0
  22. package/dist/chunks/{index-CtA2AWl3.esm.js → proxy-D2C49sXH.esm.js} +2570 -3174
  23. package/dist/chunks/proxy-D2C49sXH.esm.js.map +1 -0
  24. package/dist/debug.cjs +987 -28
  25. package/dist/debug.cjs.map +1 -1
  26. package/dist/debug.d.ts +37 -38
  27. package/dist/debug.esm.js +963 -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 +1322 -1
  34. package/dist/mutts.umd.js +6647 -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 +69 -63
  39. package/dist/node.cjs.map +1 -1
  40. package/dist/node.d.ts +2 -2
  41. package/dist/node.dev.cjs +114 -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-Bx2PhORg.d.ts} +134 -88
  49. package/docs/ai/api-reference.md +11 -32
  50. package/docs/ai/manual.md +297 -239
  51. package/docs/reactive/advanced.md +318 -9
  52. package/docs/reactive/attend.md +2 -4
  53. package/docs/reactive/collections.md +22 -187
  54. package/docs/reactive/core.md +223 -131
  55. package/docs/reactive/debugging.md +119 -12
  56. package/docs/reactive/error-handling.md +10 -10
  57. package/docs/reactive/resource.md +125 -0
  58. package/docs/reactive.md +3 -4
  59. package/docs/utils.md +70 -0
  60. package/docs/zone.md +1 -1
  61. package/package.json +76 -38
  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 +0 -3011
  65. package/dist/chunks/index-2vea86wD.esm.js.map +0 -1
  66. package/dist/chunks/index-CtA2AWl3.esm.js.map +0 -1
  67. package/dist/chunks/index-VTO-b2vR.cjs.map +0 -1
  68. package/dist/debug/debug.d.ts +0 -122
  69. package/dist/debug/debug.d.ts.map +0 -1
  70. package/dist/debug/index.d.ts +0 -4
  71. package/dist/debug/index.d.ts.map +0 -1
  72. package/dist/debug/lineage-panel.d.ts +0 -5
  73. package/dist/debug/lineage-panel.d.ts.map +0 -1
  74. package/dist/debug/lineage.d.ts +0 -79
  75. package/dist/debug/lineage.d.ts.map +0 -1
  76. package/dist/src/async/browser.d.ts +0 -2
  77. package/dist/src/async/browser.d.ts.map +0 -1
  78. package/dist/src/async/index.d.ts +0 -19
  79. package/dist/src/async/index.d.ts.map +0 -1
  80. package/dist/src/async/node.d.ts +0 -2
  81. package/dist/src/async/node.d.ts.map +0 -1
  82. package/dist/src/decorator.d.ts +0 -106
  83. package/dist/src/decorator.d.ts.map +0 -1
  84. package/dist/src/destroyable.d.ts +0 -87
  85. package/dist/src/destroyable.d.ts.map +0 -1
  86. package/dist/src/entry-browser.d.ts +0 -3
  87. package/dist/src/entry-browser.d.ts.map +0 -1
  88. package/dist/src/entry-node.d.ts +0 -3
  89. package/dist/src/entry-node.d.ts.map +0 -1
  90. package/dist/src/eventful.d.ts +0 -20
  91. package/dist/src/eventful.d.ts.map +0 -1
  92. package/dist/src/flavored.d.ts +0 -33
  93. package/dist/src/flavored.d.ts.map +0 -1
  94. package/dist/src/index.d.ts +0 -14
  95. package/dist/src/index.d.ts.map +0 -1
  96. package/dist/src/indexable.d.ts +0 -243
  97. package/dist/src/indexable.d.ts.map +0 -1
  98. package/dist/src/introspection.d.ts +0 -27
  99. package/dist/src/introspection.d.ts.map +0 -1
  100. package/dist/src/iterableWeak.d.ts +0 -53
  101. package/dist/src/iterableWeak.d.ts.map +0 -1
  102. package/dist/src/mixins.d.ts +0 -25
  103. package/dist/src/mixins.d.ts.map +0 -1
  104. package/dist/src/promiseChain.d.ts +0 -20
  105. package/dist/src/promiseChain.d.ts.map +0 -1
  106. package/dist/src/reactive/array.d.ts +0 -48
  107. package/dist/src/reactive/array.d.ts.map +0 -1
  108. package/dist/src/reactive/buffer.d.ts +0 -120
  109. package/dist/src/reactive/buffer.d.ts.map +0 -1
  110. package/dist/src/reactive/change.d.ts +0 -29
  111. package/dist/src/reactive/change.d.ts.map +0 -1
  112. package/dist/src/reactive/deep-touch.d.ts +0 -28
  113. package/dist/src/reactive/deep-touch.d.ts.map +0 -1
  114. package/dist/src/reactive/deep-watch-state.d.ts +0 -25
  115. package/dist/src/reactive/deep-watch-state.d.ts.map +0 -1
  116. package/dist/src/reactive/deep-watch.d.ts +0 -20
  117. package/dist/src/reactive/deep-watch.d.ts.map +0 -1
  118. package/dist/src/reactive/describe.d.ts +0 -12
  119. package/dist/src/reactive/describe.d.ts.map +0 -1
  120. package/dist/src/reactive/effect-context.d.ts +0 -34
  121. package/dist/src/reactive/effect-context.d.ts.map +0 -1
  122. package/dist/src/reactive/effects.d.ts +0 -164
  123. package/dist/src/reactive/effects.d.ts.map +0 -1
  124. package/dist/src/reactive/index.d.ts +0 -19
  125. package/dist/src/reactive/index.d.ts.map +0 -1
  126. package/dist/src/reactive/map.d.ts +0 -28
  127. package/dist/src/reactive/map.d.ts.map +0 -1
  128. package/dist/src/reactive/memoize.d.ts +0 -28
  129. package/dist/src/reactive/memoize.d.ts.map +0 -1
  130. package/dist/src/reactive/non-reactive-state.d.ts +0 -9
  131. package/dist/src/reactive/non-reactive-state.d.ts.map +0 -1
  132. package/dist/src/reactive/non-reactive.d.ts +0 -11
  133. package/dist/src/reactive/non-reactive.d.ts.map +0 -1
  134. package/dist/src/reactive/project.d.ts +0 -40
  135. package/dist/src/reactive/project.d.ts.map +0 -1
  136. package/dist/src/reactive/proxy-state.d.ts +0 -8
  137. package/dist/src/reactive/proxy-state.d.ts.map +0 -1
  138. package/dist/src/reactive/proxy.d.ts +0 -23
  139. package/dist/src/reactive/proxy.d.ts.map +0 -1
  140. package/dist/src/reactive/record.d.ts +0 -115
  141. package/dist/src/reactive/record.d.ts.map +0 -1
  142. package/dist/src/reactive/register.d.ts +0 -125
  143. package/dist/src/reactive/register.d.ts.map +0 -1
  144. package/dist/src/reactive/registry.d.ts +0 -21
  145. package/dist/src/reactive/registry.d.ts.map +0 -1
  146. package/dist/src/reactive/set.d.ts +0 -26
  147. package/dist/src/reactive/set.d.ts.map +0 -1
  148. package/dist/src/reactive/tracking.d.ts +0 -7
  149. package/dist/src/reactive/tracking.d.ts.map +0 -1
  150. package/dist/src/reactive/types.d.ts +0 -424
  151. package/dist/src/reactive/types.d.ts.map +0 -1
  152. package/dist/src/reactive/watch.d.ts +0 -48
  153. package/dist/src/reactive/watch.d.ts.map +0 -1
  154. package/dist/src/std-decorators.d.ts +0 -45
  155. package/dist/src/std-decorators.d.ts.map +0 -1
  156. package/dist/src/utils.d.ts +0 -49
  157. package/dist/src/utils.d.ts.map +0 -1
  158. package/dist/src/zone.d.ts +0 -40
  159. package/dist/src/zone.d.ts.map +0 -1
  160. package/docs/reactive/describe.md +0 -85
  161. package/docs/reactive/project.md +0 -93
  162. package/docs/reactive/scan.md +0 -293
  163. package/src/async/browser.ts +0 -323
  164. package/src/async/index.ts +0 -27
  165. package/src/async/node.ts +0 -92
  166. package/src/decorator.ts +0 -272
  167. package/src/destroyable.ts +0 -199
  168. package/src/entry-browser.ts +0 -5
  169. package/src/entry-node.ts +0 -5
  170. package/src/eventful.ts +0 -110
  171. package/src/flavored.ts +0 -106
  172. package/src/index.d.ts +0 -12
  173. package/src/index.ts +0 -64
  174. package/src/indexable.ts +0 -526
  175. package/src/introspection.ts +0 -59
  176. package/src/iterableWeak.ts +0 -233
  177. package/src/mixins.ts +0 -123
  178. package/src/promiseChain.ts +0 -110
  179. package/src/reactive/array.ts +0 -500
  180. package/src/reactive/buffer.ts +0 -328
  181. package/src/reactive/change.ts +0 -131
  182. package/src/reactive/deep-touch.ts +0 -273
  183. package/src/reactive/deep-watch-state.ts +0 -82
  184. package/src/reactive/deep-watch.ts +0 -171
  185. package/src/reactive/describe.ts +0 -39
  186. package/src/reactive/effect-context.ts +0 -83
  187. package/src/reactive/effects.ts +0 -1434
  188. package/src/reactive/index.ts +0 -72
  189. package/src/reactive/map.ts +0 -142
  190. package/src/reactive/memoize.ts +0 -186
  191. package/src/reactive/non-reactive-state.ts +0 -49
  192. package/src/reactive/non-reactive.ts +0 -43
  193. package/src/reactive/project.md +0 -107
  194. package/src/reactive/project.ts +0 -430
  195. package/src/reactive/proxy-state.ts +0 -27
  196. package/src/reactive/proxy.ts +0 -282
  197. package/src/reactive/record.ts +0 -181
  198. package/src/reactive/register.ts +0 -538
  199. package/src/reactive/registry.ts +0 -72
  200. package/src/reactive/set.ts +0 -117
  201. package/src/reactive/tracking.ts +0 -41
  202. package/src/reactive/types.ts +0 -520
  203. package/src/reactive/watch.ts +0 -180
  204. package/src/std-decorators.ts +0 -256
  205. package/src/utils.ts +0 -300
  206. 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** — 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, 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. 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
+ })
29
61
 
30
- ### 2.2 effect()React to changes
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
- 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.
67
111
 
68
- ### 2.4 untracked() — Escape tracking
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
- 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.
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
- unreactive(obj) // mark object as non-reactive
80
- unreactive(MyClass) // mark entire class
81
- @unreactive('id', 'meta') // mark specific properties on a @reactive class
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.6 atomic() — Batch mutations
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
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
- Also works as `@atomic` decorator on class methods.
201
+ ### 3.7 atom() Immediate atomic execution
96
202
 
97
- ### 2.7 defer() / addBatchCleanup() — Avoid cycles
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
+ ```
256
+
257
+ ### 3.12 when() — Promise-based reactive wait
134
258
 
135
- ## 3. COLLECTIONS
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
- All standard collections get reactive wrappers via `reactive()`:
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
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
- ## 4. COLLECTION TRANSFORMS
279
+ ## 5. COLLECTION TRANSFORMS
173
280
 
174
- ### 4.1 project() Per-entry reactive map
281
+ All transforms return reactive results. Cleanup via `result[cleanup]()`.
175
282
 
176
- The reactive replacement for `.map()`. Each entry gets its own effect — only changed entries recompute.
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 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]()
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
- ### 4.2 attend() — Per-key lifecycle
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
- // Also works with: arrays, Maps, Sets, or raw () => Iterable<Key>
300
+ // Works with: arrays, Maps, Sets, or raw () => Iterable<Key>
212
301
  ```
213
302
 
214
- ### 4.3 organized() — Per-key record transform with side effects
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
- ### 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
312
+ ### 5.6 lift() — Sync a computed array/object
233
313
 
234
314
  ```ts
235
- 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]()
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
- Items must be objects (WeakMap keys). Move-optimized (reorders reuse cached intermediates).
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
- ```ts
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
- ## 5. EVOLUTION TRACKING
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 a wholesale parent notification. Disable with `reactiveOptions.recursiveTouching = false`.
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
- ## 6. ERROR HANDLING
341
+ ## 7. ERROR HANDLING
272
342
 
273
343
  ```ts
274
- import { onEffectThrow } from 'mutts'
275
-
276
344
  effect(() => {
277
- onEffectThrow((error) => {
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 effect disposal
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 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.
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
- ## 7. DEBUGGING
360
+ ## 8. DEBUGGING & OPTIONS
294
361
 
295
362
  ```ts
296
- import { reactiveOptions } from 'mutts/reactive'
363
+ import { reactiveOptions } from 'mutts'
297
364
 
298
- // Cycle detection modes
299
- reactiveOptions.cycleHandling = 'development' // default graph-based, throws immediately
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 (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
- }
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 { enableIntrospection, getDependencyGraph, getMutationHistory } from 'mutts/introspection'
315
- enableIntrospection({ historySize: 100 })
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** contains: `causalChain`, `creationStack`, `cycle` (for CYCLE_DETECTED).
388
+ **ReactiveError.debugInfo**: `causalChain`, `creationStack`, `cycle` (for CYCLE_DETECTED).
319
389
 
320
390
  ---
321
391
 
322
- ## 8. ZONES (Async Context)
392
+ ## 9. ZONES (Async Context)
323
393
 
324
394
  ```ts
325
- import { Zone, asyncZone } from 'mutts/zone'
395
+ import { Zone, asyncZone } from 'mutts'
326
396
 
327
397
  const requestId = new Zone<string>()
328
- asyncZone.add(requestId) // register for async propagation
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
- - `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).
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
- ## 9. OTHER MODULES
415
+ ## 10. OTHER MODULES
345
416
 
346
- ### 9.1 Decorators (`mutts/decorator`)
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
- Standard decorators: `@cached`, `@debounce(ms)`, `@throttle(ms)`, `@deprecated(msg)`.
355
-
356
- ### 9.2 Mixin (`mutts/mixin`)
425
+ ### 10.2 Mixin
357
426
 
358
427
  ```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
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
- ### 9.3 Eventful (`mutts/eventful`)
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
- 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
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
- ### 9.4 Destroyable (`mutts/destroyable`)
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
- // Explicit: Destroyable.destroy(instance) or future `using` statement.
451
+ // Destroyed objects throw DestructionError on access
452
+ // Destroyable.destroy(instance) or `using` statement
394
453
  ```
395
454
 
396
- ### 9.5 Flavored Functions (`mutts/flavored`)
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
- ### 9.6 Indexable (`mutts/indexable`)
465
+ ### 10.6 Indexable
409
466
 
410
- Numeric index access (`obj[0]`) on custom classes via Proxy:
411
467
  ```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 }
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
- ### 9.7 PromiseChain (`mutts/promiseChain`)
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
- ## 10. PHILOSOPHY & TRAPS
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
- ### Events Are Legacy
435
- Only use events for DOM interaction or external APIs. Internal logic should use reactive derivations (`effect`, `memoize`, `project`).
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
- ### 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 |
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 |