mutts 1.0.8 → 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 (195) hide show
  1. package/README.md +77 -35
  2. package/dist/browser.cjs +2144 -5355
  3. package/dist/browser.cjs.map +1 -1
  4. package/dist/browser.d.ts +2 -1655
  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 +20 -107
  11. package/dist/browser.esm.js.map +1 -1
  12. package/dist/chunks/{async-browser-CA0jPWIi.cjs → async-browser-BU_IfxYD.cjs} +19 -107
  13. package/dist/chunks/async-browser-BU_IfxYD.cjs.map +1 -0
  14. package/dist/chunks/{async-core-UqHzvJ-S.cjs → async-core-CRLKP3l-.cjs} +8 -4
  15. package/dist/chunks/async-core-CRLKP3l-.cjs.map +1 -0
  16. package/dist/chunks/{async-node-BYHuGTni.cjs → async-node-3PrbVAbB.cjs} +7 -12
  17. package/dist/chunks/async-node-3PrbVAbB.cjs.map +1 -0
  18. package/dist/chunks/index-CaaQQlPJ.esm.js +3174 -0
  19. package/dist/chunks/index-CaaQQlPJ.esm.js.map +1 -0
  20. package/dist/chunks/node-nKJBk8iJ.esm.js +96 -0
  21. package/dist/chunks/node-nKJBk8iJ.esm.js.map +1 -0
  22. package/dist/chunks/proxy-Dtg-bJ3T.cjs +3763 -0
  23. package/dist/chunks/proxy-Dtg-bJ3T.cjs.map +1 -0
  24. package/dist/chunks/proxy-r7lARftl.esm.js +3701 -0
  25. package/dist/chunks/proxy-r7lARftl.esm.js.map +1 -0
  26. package/dist/debug.cjs +980 -0
  27. package/dist/debug.cjs.map +1 -0
  28. package/dist/debug.d.ts +208 -0
  29. package/dist/debug.esm.js +950 -0
  30. package/dist/debug.esm.js.map +1 -0
  31. package/dist/devtools/manifest.json +1 -1
  32. package/dist/devtools/panel.html +1 -1
  33. package/dist/devtools/panel.js +107 -94
  34. package/dist/devtools/panel.js.map +1 -1
  35. package/dist/index.d.ts +1447 -13
  36. package/dist/mutts.umd.js +7167 -1
  37. package/dist/mutts.umd.js.map +1 -1
  38. package/dist/mutts.umd.min.js +1 -1
  39. package/dist/mutts.umd.min.js.map +1 -1
  40. package/dist/node.cjs +70 -66
  41. package/dist/node.cjs.map +1 -1
  42. package/dist/node.d.ts +2 -1
  43. package/dist/node.dev.cjs +110 -0
  44. package/dist/node.dev.cjs.map +1 -0
  45. package/dist/node.dev.d.ts +2 -0
  46. package/dist/node.dev.esm.js +6 -0
  47. package/dist/node.dev.esm.js.map +1 -0
  48. package/dist/node.esm.js +4 -103
  49. package/dist/node.esm.js.map +1 -1
  50. package/dist/{reactive/types.d.ts → types-W5vD6m2n.d.ts} +186 -93
  51. package/docs/ai/api-reference.md +12 -19
  52. package/docs/ai/manual.md +521 -17
  53. package/docs/events.md +57 -2
  54. package/docs/flavored.md +210 -0
  55. package/docs/reactive/advanced.md +314 -5
  56. package/docs/reactive/attend.md +142 -0
  57. package/docs/reactive/collections.md +76 -39
  58. package/docs/reactive/core.md +100 -14
  59. package/docs/reactive/debugging.md +123 -18
  60. package/docs/reactive/error-handling.md +121 -0
  61. package/docs/reactive/resource.md +125 -0
  62. package/docs/reactive/scan.md +247 -1
  63. package/docs/reactive.md +4 -1
  64. package/docs/std-decorators.md +102 -51
  65. package/docs/utils.md +70 -0
  66. package/docs/zone.md +3 -7
  67. package/package.json +64 -30
  68. package/dist/async/browser.d.ts +0 -2
  69. package/dist/async/browser.d.ts.map +0 -1
  70. package/dist/async/index.d.ts +0 -18
  71. package/dist/async/index.d.ts.map +0 -1
  72. package/dist/async/node.d.ts +0 -2
  73. package/dist/async/node.d.ts.map +0 -1
  74. package/dist/chunks/async-browser-CA0jPWIi.cjs.map +0 -1
  75. package/dist/chunks/async-core-UqHzvJ-S.cjs.map +0 -1
  76. package/dist/chunks/async-node-BYHuGTni.cjs.map +0 -1
  77. package/dist/chunks/index-DhaOVusv.esm.js +0 -6411
  78. package/dist/chunks/index-DhaOVusv.esm.js.map +0 -1
  79. package/dist/decorator.d.ts +0 -106
  80. package/dist/decorator.d.ts.map +0 -1
  81. package/dist/destroyable.d.ts +0 -87
  82. package/dist/destroyable.d.ts.map +0 -1
  83. package/dist/entry-browser.d.ts +0 -3
  84. package/dist/entry-browser.d.ts.map +0 -1
  85. package/dist/entry-node.d.ts +0 -3
  86. package/dist/entry-node.d.ts.map +0 -1
  87. package/dist/eventful.d.ts +0 -18
  88. package/dist/eventful.d.ts.map +0 -1
  89. package/dist/index.d.ts.map +0 -1
  90. package/dist/indexable.d.ts +0 -243
  91. package/dist/indexable.d.ts.map +0 -1
  92. package/dist/introspection.d.ts +0 -27
  93. package/dist/introspection.d.ts.map +0 -1
  94. package/dist/iterableWeak.d.ts +0 -53
  95. package/dist/iterableWeak.d.ts.map +0 -1
  96. package/dist/mixins.d.ts +0 -25
  97. package/dist/mixins.d.ts.map +0 -1
  98. package/dist/promiseChain.d.ts +0 -20
  99. package/dist/promiseChain.d.ts.map +0 -1
  100. package/dist/reactive/array.d.ts +0 -49
  101. package/dist/reactive/array.d.ts.map +0 -1
  102. package/dist/reactive/buffer.d.ts +0 -44
  103. package/dist/reactive/buffer.d.ts.map +0 -1
  104. package/dist/reactive/change.d.ts +0 -29
  105. package/dist/reactive/change.d.ts.map +0 -1
  106. package/dist/reactive/debug.d.ts +0 -111
  107. package/dist/reactive/debug.d.ts.map +0 -1
  108. package/dist/reactive/deep-touch.d.ts +0 -28
  109. package/dist/reactive/deep-touch.d.ts.map +0 -1
  110. package/dist/reactive/deep-watch-state.d.ts +0 -25
  111. package/dist/reactive/deep-watch-state.d.ts.map +0 -1
  112. package/dist/reactive/deep-watch.d.ts +0 -19
  113. package/dist/reactive/deep-watch.d.ts.map +0 -1
  114. package/dist/reactive/effect-context.d.ts +0 -7
  115. package/dist/reactive/effect-context.d.ts.map +0 -1
  116. package/dist/reactive/effects.d.ts +0 -151
  117. package/dist/reactive/effects.d.ts.map +0 -1
  118. package/dist/reactive/index.d.ts +0 -20
  119. package/dist/reactive/index.d.ts.map +0 -1
  120. package/dist/reactive/interface.d.ts +0 -64
  121. package/dist/reactive/interface.d.ts.map +0 -1
  122. package/dist/reactive/map.d.ts +0 -30
  123. package/dist/reactive/map.d.ts.map +0 -1
  124. package/dist/reactive/memoize.d.ts +0 -5
  125. package/dist/reactive/memoize.d.ts.map +0 -1
  126. package/dist/reactive/non-reactive-state.d.ts +0 -9
  127. package/dist/reactive/non-reactive-state.d.ts.map +0 -1
  128. package/dist/reactive/non-reactive.d.ts +0 -11
  129. package/dist/reactive/non-reactive.d.ts.map +0 -1
  130. package/dist/reactive/project.d.ts +0 -41
  131. package/dist/reactive/project.d.ts.map +0 -1
  132. package/dist/reactive/proxy-state.d.ts +0 -8
  133. package/dist/reactive/proxy-state.d.ts.map +0 -1
  134. package/dist/reactive/proxy.d.ts +0 -23
  135. package/dist/reactive/proxy.d.ts.map +0 -1
  136. package/dist/reactive/record.d.ts +0 -116
  137. package/dist/reactive/record.d.ts.map +0 -1
  138. package/dist/reactive/register.d.ts +0 -64
  139. package/dist/reactive/register.d.ts.map +0 -1
  140. package/dist/reactive/registry.d.ts +0 -20
  141. package/dist/reactive/registry.d.ts.map +0 -1
  142. package/dist/reactive/set.d.ts +0 -28
  143. package/dist/reactive/set.d.ts.map +0 -1
  144. package/dist/reactive/tracking.d.ts +0 -7
  145. package/dist/reactive/tracking.d.ts.map +0 -1
  146. package/dist/reactive/types.d.ts.map +0 -1
  147. package/dist/std-decorators.d.ts +0 -50
  148. package/dist/std-decorators.d.ts.map +0 -1
  149. package/dist/utils.d.ts +0 -49
  150. package/dist/utils.d.ts.map +0 -1
  151. package/dist/zone.d.ts +0 -40
  152. package/dist/zone.d.ts.map +0 -1
  153. package/docs/reactive/project.md +0 -93
  154. package/src/async/browser.ts +0 -319
  155. package/src/async/index.ts +0 -23
  156. package/src/async/node.ts +0 -104
  157. package/src/decorator.ts +0 -272
  158. package/src/destroyable.ts +0 -199
  159. package/src/entry-browser.ts +0 -5
  160. package/src/entry-node.ts +0 -5
  161. package/src/eventful.ts +0 -77
  162. package/src/index.d.ts +0 -12
  163. package/src/index.ts +0 -64
  164. package/src/indexable.ts +0 -526
  165. package/src/introspection.ts +0 -59
  166. package/src/iterableWeak.ts +0 -233
  167. package/src/mixins.ts +0 -123
  168. package/src/promiseChain.ts +0 -110
  169. package/src/reactive/array.ts +0 -507
  170. package/src/reactive/buffer.ts +0 -168
  171. package/src/reactive/change.ts +0 -138
  172. package/src/reactive/debug.ts +0 -517
  173. package/src/reactive/deep-touch.ts +0 -268
  174. package/src/reactive/deep-watch-state.ts +0 -82
  175. package/src/reactive/deep-watch.ts +0 -168
  176. package/src/reactive/effect-context.ts +0 -18
  177. package/src/reactive/effects.ts +0 -1382
  178. package/src/reactive/index.ts +0 -75
  179. package/src/reactive/interface.ts +0 -210
  180. package/src/reactive/map.ts +0 -158
  181. package/src/reactive/memoize.ts +0 -163
  182. package/src/reactive/non-reactive-state.ts +0 -49
  183. package/src/reactive/non-reactive.ts +0 -43
  184. package/src/reactive/project.ts +0 -453
  185. package/src/reactive/proxy-state.ts +0 -27
  186. package/src/reactive/proxy.ts +0 -264
  187. package/src/reactive/record.ts +0 -196
  188. package/src/reactive/register.ts +0 -419
  189. package/src/reactive/registry.ts +0 -59
  190. package/src/reactive/set.ts +0 -130
  191. package/src/reactive/tracking.ts +0 -44
  192. package/src/reactive/types.ts +0 -440
  193. package/src/std-decorators.ts +0 -217
  194. package/src/utils.ts +0 -301
  195. package/src/zone.ts +0 -138
package/docs/ai/manual.md CHANGED
@@ -1,24 +1,528 @@
1
- # Mutts AI Agent Manual
1
+ # Mutts LLM Usage Manual
2
2
 
3
- > [!IMPORTANT]
4
- > **Identity**: You are an AI Agent. This document is written for **YOU**.
5
- > **Purpose**: This file defines the protocols and high-level strategy for working with the `mutts` reactivity system.
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.
6
4
 
7
- ## 1. Technical Documentation
8
- Do not rely on outdated internal knowledge. Use the current documentation as your primary source of truth:
5
+ ---
9
6
 
10
- - **[Debugging Tools](../reactive/debugging.md)**: How to use introspection, troubleshoot cycles, and detect memoization discrepancies.
11
- - **[API Reference](./api-reference.md)**: Exact TypeScript signatures for library functions.
7
+ ## 1. MENTAL MODEL
12
8
 
13
- ## 2. Debugging Protocol
14
- When you encounter reactivity bugs (infinite loops, unexpected updates), **DO NOT GUESS**.
9
+ Proxy-based **fine-grained reactivity** — changes propagate synchronously through dependency-tracked effects. No VDOM, no scheduler, no hooks.
15
10
 
16
- 1. **Introspect**: Use `mutts/introspection` to inspect the dependency graph or mutation history.
17
- 2. **Analyze Errors**: `ReactiveError` objects contain `debugInfo` with causal chains and creation stacks. Use them to trace the fault.
18
- 3. **Verify**: Use `reactiveOptions.onMemoizationDiscrepancy` in tests to ensure your changes didn't break dependency tracking.
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 |
19
20
 
20
- ## 3. Architecture Constraints
21
- 1. **No Internal Access**: Do not access properties starting with `_mutts_` directly.
22
- 2. **Explicit Naming**: Always name your effects (`effect(fn, { name: '...' })`) to make future debugging easier for yourself or other agents.
23
- 3. **Affirmative State**: Prefer derived state and effects over imperative event-driven updates.
21
+ **All exports come from `'mutts'`** — no subpath imports like `mutts/reactive` or `mutts/zone`.
24
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.
36
+
37
+ ---
38
+
39
+ ## 2. CRITICAL TRAPS
40
+
41
+ ### TRAP 1: Memoize with primitives
42
+ `memoize` uses WeakMap — args MUST be objects/symbols, NOT primitives.
43
+
44
+ ```ts
45
+ // BAD
46
+ const double = memoize((n: number) => n * 2) // WeakMap can't key on number
47
+
48
+ // GOOD
49
+ const double = memoize((obj: { n: number }) => obj.n * 2)
50
+ ```
51
+
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
+ ```
69
+
70
+ ### TRAP 3: Self-triggering effects (cycles)
71
+ Effect reads and writes same state → infinite loop.
72
+
73
+ ```ts
74
+ // BAD — cycle
75
+ effect(() => {
76
+ state.count = state.count + 1 // reads count, writes count → re-triggers
77
+ })
78
+
79
+ // GOOD — defer mutation
80
+ effect(() => {
81
+ const val = state.count
82
+ defer(() => { state.processedCount = val }) // runs after batch
83
+ })
84
+ ```
85
+
86
+ ### TRAP 4: Using queueMicrotask for cycle avoidance
87
+ `defer()` is batch-aware and synchronous. `queueMicrotask` breaks batching.
88
+
89
+ ```ts
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
+ })
99
+ ```
100
+
101
+ ### TRAP 5: Array clearing
102
+ `array.length = 0` works (triggers all indices + length). `splice(0)` is equivalent.
103
+
104
+ ```ts
105
+ array.length = 0 // works — touches all affected indices
106
+ array.splice(0) // also works — explicit clear
107
+ ```
108
+
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
+ ```
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).
122
+
123
+ ```ts
124
+ // EXPENSIVE
125
+ watch(state, () => { ... }, { deep: true })
126
+
127
+ // BETTER — explicit property reads
128
+ effect(() => {
129
+ state.user.name // only tracks .user.name
130
+ })
131
+ ```
132
+
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
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
157
+
158
+ ```ts
159
+ const stop = effect(({ reaction, tracked, ascend }) => {
160
+ console.log(state.count) // auto-tracked
161
+ return () => { /* cleanup: before re-run or disposal */ }
162
+ })
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
199
+ ```
200
+
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
209
+
210
+ ```ts
211
+ effect(() => {
212
+ const len = state.items.length
213
+ defer(() => { state.processedCount = len }) // sync after batch, FIFO
214
+ })
215
+ // Outside batch: immediate. NO microtask delay.
216
+ ```
217
+
218
+ ### 3.9 biDi() — Bidirectional binding
219
+
220
+ ```ts
221
+ const provide = biDi(
222
+ (v) => input.value = v, // external setter
223
+ { get: () => model.value, set: (v) => model.value = v }
224
+ )
225
+ input.addEventListener('input', () => provide(input.value))
226
+ // Prevents infinite loops automatically
227
+ ```
228
+
229
+ ### 3.10 watch() — Observe changes
230
+
231
+ ```ts
232
+ watch(() => state.count, (newVal, oldVal) => {}) // specific derivation
233
+ watch(state, () => {}) // any property
234
+ watch(state, () => {}, { deep: true }) // deep (expensive)
235
+ ```
236
+
237
+ ### 3.10 cleanedBy() — Attach cleanup to objects
238
+
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
243
+ ```
244
+
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
258
+
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
267
+
268
+ | Type | Tracking |
269
+ |------|----------|
270
+ | `reactive([])` | Per-index, `.length`, iteration |
271
+ | `reactive(new Map())` | Per-key, `.size`, iteration |
272
+ | `reactive(new Set())` | Per-value, `.size`, iteration |
273
+ | `reactive(new WeakMap/Set())` | Per-key/value only |
274
+
275
+ Array methods (`push`, `splice`, `sort`, etc.) all trigger reactivity.
276
+
277
+ ### 4.1 Register — Keyed ordered collection
278
+
279
+ ```ts
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
285
+ list.upsert(v => list.push(v), ...items) // update or insert
286
+ // Full array surface + CRUD events: on('add'|'delete'|'update'|'rekey', ...)
287
+ ```
288
+
289
+ ---
290
+
291
+ ## 5. COLLECTION TRANSFORMS
292
+
293
+ All transforms return reactive results. Cleanup via `result[cleanup]()`.
294
+
295
+ ### 5.1 project() — Per-entry reactive map
296
+
297
+ ```ts
298
+ const names = project(users, ({ get }) => get().name.toUpperCase())
299
+ // names[0] recomputes ONLY when users[0] changes
300
+ // Variants: project.record(), project.map() — auto-dispatches by source type
301
+ ```
302
+
303
+ **Access object**: `{ get(), set(v), key, source, old, value }`
304
+
305
+ ### 5.2 attend() — Per-key lifecycle
306
+
307
+ ```ts
308
+ attend(reactiveRecord, (key) => {
309
+ console.log(`${key} = ${reactiveRecord[key]}`)
310
+ return () => console.log(`cleanup: ${key}`) // disposed when key disappears
311
+ })
312
+ // Works with: arrays, Maps, Sets, or raw () => Iterable<Key>
313
+ ```
314
+
315
+ ### 5.3 organized() — Per-key record transform
316
+
317
+ ```ts
318
+ const doubled = organized(source, (access, target) => {
319
+ target[access.key] = access.get() * 2
320
+ return () => delete target[access.key]
321
+ })
322
+ ```
323
+
324
+ ### 5.4 scan() — Reactive accumulation
325
+
326
+ ```ts
327
+ const result = scan(source, (acc, item) => acc + item.val, 0)
328
+ // [1, 3, 6] — changing source[1] recomputes from index 1 onward
329
+ // Items must be objects (WeakMap keys). Move-optimized.
330
+ ```
331
+
332
+ ### 5.6 lift() — Sync a computed array/object
333
+
334
+ ```ts
335
+ const filtered = lift(() => items.filter(x => x.active))
336
+ // Element-wise diff — only changed elements sync, not full rebuild
337
+ ```
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
+
343
+ ---
344
+
345
+ ## 6. EVOLUTION TRACKING
346
+
347
+ ```ts
348
+ let state = getState(obj)
349
+ effect(() => {
350
+ while ('evolution' in state) {
351
+ console.log(state.evolution) // { type: 'set'|'add'|'del'|'bunch', prop, method }
352
+ state = state.next
353
+ }
354
+ })
355
+ ```
356
+
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`.
358
+
359
+ ---
360
+
361
+ ## 7. ERROR HANDLING
362
+
363
+ ```ts
364
+ effect(() => {
365
+ caught((error) => {
366
+ // return without throwing = handled
367
+ // throw = try next handler
368
+ // return function = cleanup on disposal
369
+ })
370
+ // ... code that might throw
371
+ })
372
+ ```
373
+
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()`.
377
+
378
+ ---
379
+
380
+ ## 8. DEBUGGING & OPTIONS
381
+
382
+ ```ts
383
+ import { reactiveOptions } from 'mutts'
384
+
385
+ // Cycle detection
386
+ reactiveOptions.cycleHandling = 'development' // default: graph-based, throws
387
+ reactiveOptions.cycleHandling = 'debug' // full transitive closure, detailed paths
388
+ reactiveOptions.cycleHandling = 'production' // heuristic via maxTriggerPerBatch
389
+
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) => {}
398
+
399
+ // Introspection (memory-intensive, dev only)
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()
406
+ ```
407
+
408
+ **ReactiveError.debugInfo**: `causalChain`, `creationStack`, `cycle` (for CYCLE_DETECTED).
409
+
410
+ ---
411
+
412
+ ## 9. ZONES (Async Context)
413
+
414
+ ```ts
415
+ import { Zone, asyncZone } from 'mutts'
416
+
417
+ const requestId = new Zone<string>()
418
+ asyncZone.add(requestId) // register for async propagation
419
+
420
+ requestId.with('req-123', async () => {
421
+ await somePromise()
422
+ requestId.active // still 'req-123'
423
+ })
424
+ ```
425
+
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).
432
+
433
+ ---
434
+
435
+ ## 10. OTHER MODULES
436
+
437
+ ### 10.1 Decorators
438
+
439
+ ```ts
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)
443
+ ```
444
+
445
+ ### 10.2 Mixin
446
+
447
+ ```ts
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)
451
+ ```
452
+
453
+ ### 10.3 Eventful
454
+
455
+ ```ts
456
+ interface MyEvents { click: (x: number, y: number) => void }
457
+ class Button extends Eventful<MyEvents> { }
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
462
+ ```
463
+
464
+ ### 10.4 Destroyable
465
+
466
+ ```ts
467
+ class FileHandler extends Destroyable() {
468
+ @allocated accessor filePath: string
469
+ [destructor](alloc) { console.log(`Closing: ${alloc.filePath}`) }
470
+ }
471
+ // Destroyed objects throw DestructionError on access
472
+ // Destroyable.destroy(instance) or `using` statement
473
+ ```
474
+
475
+ ### 10.5 Flavored Functions
476
+
477
+ ```ts
478
+ const greet = flavored(
479
+ (name: string, opts?: { loud?: boolean }) => opts?.loud ? name.toUpperCase() : name,
480
+ { get loud() { return flavorOptions(this, { loud: true }) } }
481
+ )
482
+ greet.loud('hi') // "HI" — chainable property modifiers
483
+ ```
484
+
485
+ ### 10.6 Indexable
486
+
487
+ ```ts
488
+ const MyCol = Indexable(Base, {
489
+ get(i) { return this.data[i] },
490
+ set(i, v) { this.data[i] = v }
491
+ })
492
+ // Enables obj[0] numeric index access on custom classes via Proxy
493
+ ```
494
+
495
+ ### 10.7 PromiseChain
496
+
497
+ ```ts
498
+ const theme = await chainPromise(api.getUser('123')).getProfile().getSettings().theme
499
+ // Fluent chaining on Promises without intermediate await
500
+ ```
501
+
502
+ ---
503
+
504
+ ## 11. PHILOSOPHY
505
+
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.
509
+
510
+ ---
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 |
package/docs/events.md CHANGED
@@ -32,8 +32,9 @@ class MyClass extends Eventful<MyEvents> {
32
32
 
33
33
  #### `on(events: Partial<Events>): void`
34
34
  #### `on<EventType extends keyof Events>(event: EventType, cb: Events[EventType]): () => void`
35
+ #### `on.eventName(cb: Events['eventName']): () => void`
35
36
 
36
- Registers event listeners. Can be called with either a single event and callback, or an object containing multiple events and their callbacks.
37
+ Registers event listeners. Can be called with either a single event and callback, an object containing multiple events, or using dot notation.
37
38
 
38
39
  **Parameters:**
39
40
  - `event`: The event name (when using single event overload)
@@ -49,6 +50,11 @@ const unsubscribe = myObject.on('userLogin', (userId, timestamp) => {
49
50
  console.log(`User ${userId} logged in at ${timestamp}`)
50
51
  })
51
52
 
53
+ // Dot notation (equivalent to above)
54
+ const unsubscribe = myObject.on.userLogin((userId, timestamp) => {
55
+ console.log(`User ${userId} logged in at ${timestamp}`)
56
+ })
57
+
52
58
  // Multiple events
53
59
  myObject.on({
54
60
  userLogin: (userId, timestamp) => console.log('Login:', userId),
@@ -59,8 +65,9 @@ myObject.on({
59
65
 
60
66
  #### `off(events: Partial<Events>): void`
61
67
  #### `off<EventType extends keyof Events>(event: EventType, cb?: Events[EventType]): void`
68
+ #### `off.eventName(cb?: Events['eventName']): void`
62
69
 
63
- Removes event listeners. Can be called with either a single event (and optional callback), or an object containing multiple events.
70
+ Removes event listeners. Can be called with either a single event (and optional callback), an object containing multiple events, or using dot notation.
64
71
 
65
72
  **Parameters:**
66
73
  - `event`: The event name (when using single event overload)
@@ -72,9 +79,15 @@ Removes event listeners. Can be called with either a single event (and optional
72
79
  // Remove specific callback
73
80
  myObject.off('userLogin', myCallback)
74
81
 
82
+ // Remove specific callback using dot notation
83
+ myObject.off.userLogin(myCallback)
84
+
75
85
  // Remove all listeners for an event
76
86
  myObject.off('userLogin')
77
87
 
88
+ // Remove all listeners for an event using dot notation
89
+ myObject.off.userLogin()
90
+
78
91
  // Remove multiple events
79
92
  myObject.off({
80
93
  userLogin: myCallback,
@@ -83,6 +96,7 @@ myObject.off({
83
96
  ```
84
97
 
85
98
  #### `emit<EventType extends keyof Events>(event: EventType, ...args: Parameters<Events[EventType]>): void`
99
+ #### `emit.eventName(...args: Parameters<Events['eventName']>): void`
86
100
 
87
101
  Emits an event, calling all registered listeners and global hooks.
88
102
 
@@ -93,8 +107,13 @@ Emits an event, calling all registered listeners and global hooks.
93
107
  **Example:**
94
108
  ```typescript
95
109
  myObject.emit('userLogin', 'user123', new Date())
110
+ myObject.emit.userLogin('user123', new Date()) // Dot notation
111
+
96
112
  myObject.emit('dataUpdate', [1, 2, 3])
113
+ myObject.emit.dataUpdate([1, 2, 3]) // Dot notation
114
+
97
115
  myObject.emit('error', new Error('Something went wrong'))
116
+ myObject.emit.error(new Error('Something went wrong')) // Dot notation
98
117
  ```
99
118
 
100
119
  #### `hook(cb: <EventType extends keyof Events>(event: EventType, ...args: Parameters<Events[EventType]>) => void): () => void`
@@ -113,6 +132,42 @@ const unsubscribe = myObject.hook((event, ...args) => {
113
132
  })
114
133
  ```
115
134
 
135
+ ## Dot Notation Syntax
136
+
137
+ For convenience, `on`, `off`, and `emit` all support a dot notation syntax that provides a cleaner alternative to the string-based API.
138
+
139
+ | String Notation | Dot Notation |
140
+ |----------------|--------------|
141
+ | `obj.on('event', cb)` | `obj.on.event(cb)` |
142
+ | `obj.off('event', cb)` | `obj.off.event(cb)` |
143
+ | `obj.off('event')` | `obj.off.event()` |
144
+ | `obj.emit('event', args)` | `obj.emit.event(args)` |
145
+
146
+ Both forms are functionally equivalent. The dot notation provides:
147
+
148
+ - **Cleaner syntax**: No quotes needed around event names
149
+ - **Better IDE support**: Autocomplete for event names
150
+ - **Type safety**: Full TypeScript inference for event arguments
151
+
152
+ **Example:**
153
+
154
+ ```typescript
155
+ class Button extends Eventful<{ click: (x: number, y: number) => void; hover: () => void }> {}
156
+
157
+ const button = new Button()
158
+
159
+ // Register listener using dot notation
160
+ button.on.click((x, y) => console.log(`Clicked at ${x}, ${y}`))
161
+
162
+ // Emit using dot notation
163
+ button.emit.click(100, 200)
164
+
165
+ // Remove listener using dot notation
166
+ button.off.click(myCallback)
167
+ // Or remove all click listeners
168
+ button.off.click()
169
+ ```
170
+
116
171
  ## Usage Examples
117
172
 
118
173
  ### Basic Event Handling