mutts 1.0.7 → 1.0.9

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 (191) hide show
  1. package/README.md +72 -17
  2. package/dist/browser.cjs +3111 -0
  3. package/dist/browser.cjs.map +1 -0
  4. package/dist/browser.d.ts +1391 -1
  5. package/dist/browser.esm.js +264 -35
  6. package/dist/browser.esm.js.map +1 -1
  7. package/dist/chunks/async-browser-lvzLOCgk.cjs +297 -0
  8. package/dist/chunks/async-browser-lvzLOCgk.cjs.map +1 -0
  9. package/dist/chunks/async-core-CRLKP3l-.cjs +29 -0
  10. package/dist/chunks/async-core-CRLKP3l-.cjs.map +1 -0
  11. package/dist/chunks/async-node-C3DeIb0y.cjs +96 -0
  12. package/dist/chunks/async-node-C3DeIb0y.cjs.map +1 -0
  13. package/dist/chunks/index-2vea86wD.esm.js +3011 -0
  14. package/dist/chunks/index-2vea86wD.esm.js.map +1 -0
  15. package/dist/chunks/index-CtA2AWl3.esm.js +4376 -0
  16. package/dist/chunks/index-CtA2AWl3.esm.js.map +1 -0
  17. package/dist/chunks/index-VTO-b2vR.cjs +4461 -0
  18. package/dist/chunks/index-VTO-b2vR.cjs.map +1 -0
  19. package/dist/debug/debug.d.ts +122 -0
  20. package/dist/debug/debug.d.ts.map +1 -0
  21. package/dist/debug/index.d.ts +4 -0
  22. package/dist/debug/index.d.ts.map +1 -0
  23. package/dist/debug/lineage-panel.d.ts +5 -0
  24. package/dist/debug/lineage-panel.d.ts.map +1 -0
  25. package/dist/debug/lineage.d.ts +79 -0
  26. package/dist/debug/lineage.d.ts.map +1 -0
  27. package/dist/debug.cjs +35 -0
  28. package/dist/debug.cjs.map +1 -0
  29. package/dist/debug.d.ts +209 -0
  30. package/dist/debug.esm.js +2 -0
  31. package/dist/debug.esm.js.map +1 -0
  32. package/dist/devtools/devtool/devtools.d.ts +1 -0
  33. package/dist/devtools/devtool/devtools.d.ts.map +1 -0
  34. package/dist/devtools/devtool/panel.d.ts +2 -0
  35. package/dist/devtools/devtool/panel.d.ts.map +1 -0
  36. package/dist/index.d.ts +1 -0
  37. package/dist/mutts.umd.js +1 -1
  38. package/dist/mutts.umd.js.map +1 -1
  39. package/dist/mutts.umd.min.js +1 -1
  40. package/dist/mutts.umd.min.js.map +1 -1
  41. package/dist/node.cjs +107 -0
  42. package/dist/node.cjs.map +1 -0
  43. package/dist/node.d.ts +2 -2
  44. package/dist/node.esm.js +86 -33
  45. package/dist/node.esm.js.map +1 -1
  46. package/dist/src/async/browser.d.ts +2 -0
  47. package/dist/src/async/browser.d.ts.map +1 -0
  48. package/dist/src/async/index.d.ts +19 -0
  49. package/dist/src/async/index.d.ts.map +1 -0
  50. package/dist/src/async/node.d.ts +2 -0
  51. package/dist/src/async/node.d.ts.map +1 -0
  52. package/dist/src/decorator.d.ts +106 -0
  53. package/dist/src/decorator.d.ts.map +1 -0
  54. package/dist/src/destroyable.d.ts +87 -0
  55. package/dist/src/destroyable.d.ts.map +1 -0
  56. package/dist/src/entry-browser.d.ts +3 -0
  57. package/dist/src/entry-browser.d.ts.map +1 -0
  58. package/dist/src/entry-node.d.ts +3 -0
  59. package/dist/src/entry-node.d.ts.map +1 -0
  60. package/dist/src/eventful.d.ts +20 -0
  61. package/dist/src/eventful.d.ts.map +1 -0
  62. package/dist/src/flavored.d.ts +33 -0
  63. package/dist/src/flavored.d.ts.map +1 -0
  64. package/dist/src/index.d.ts +14 -0
  65. package/dist/src/index.d.ts.map +1 -0
  66. package/dist/src/indexable.d.ts +243 -0
  67. package/dist/src/indexable.d.ts.map +1 -0
  68. package/dist/src/introspection.d.ts +27 -0
  69. package/dist/src/introspection.d.ts.map +1 -0
  70. package/dist/src/iterableWeak.d.ts +53 -0
  71. package/dist/src/iterableWeak.d.ts.map +1 -0
  72. package/dist/src/mixins.d.ts +25 -0
  73. package/dist/src/mixins.d.ts.map +1 -0
  74. package/dist/src/promiseChain.d.ts +20 -0
  75. package/dist/src/promiseChain.d.ts.map +1 -0
  76. package/dist/src/reactive/array.d.ts +48 -0
  77. package/dist/src/reactive/array.d.ts.map +1 -0
  78. package/dist/src/reactive/buffer.d.ts +120 -0
  79. package/dist/src/reactive/buffer.d.ts.map +1 -0
  80. package/dist/src/reactive/change.d.ts +29 -0
  81. package/dist/src/reactive/change.d.ts.map +1 -0
  82. package/dist/src/reactive/deep-touch.d.ts +28 -0
  83. package/dist/src/reactive/deep-touch.d.ts.map +1 -0
  84. package/dist/src/reactive/deep-watch-state.d.ts +25 -0
  85. package/dist/src/reactive/deep-watch-state.d.ts.map +1 -0
  86. package/dist/src/reactive/deep-watch.d.ts +20 -0
  87. package/dist/src/reactive/deep-watch.d.ts.map +1 -0
  88. package/dist/src/reactive/describe.d.ts +12 -0
  89. package/dist/src/reactive/describe.d.ts.map +1 -0
  90. package/dist/src/reactive/effect-context.d.ts +34 -0
  91. package/dist/src/reactive/effect-context.d.ts.map +1 -0
  92. package/dist/src/reactive/effects.d.ts +164 -0
  93. package/dist/src/reactive/effects.d.ts.map +1 -0
  94. package/dist/src/reactive/index.d.ts +19 -0
  95. package/dist/src/reactive/index.d.ts.map +1 -0
  96. package/dist/src/reactive/map.d.ts +28 -0
  97. package/dist/src/reactive/map.d.ts.map +1 -0
  98. package/dist/src/reactive/memoize.d.ts +28 -0
  99. package/dist/src/reactive/memoize.d.ts.map +1 -0
  100. package/dist/src/reactive/non-reactive-state.d.ts +9 -0
  101. package/dist/src/reactive/non-reactive-state.d.ts.map +1 -0
  102. package/dist/src/reactive/non-reactive.d.ts +11 -0
  103. package/dist/src/reactive/non-reactive.d.ts.map +1 -0
  104. package/dist/src/reactive/project.d.ts +40 -0
  105. package/dist/src/reactive/project.d.ts.map +1 -0
  106. package/dist/src/reactive/proxy-state.d.ts +8 -0
  107. package/dist/src/reactive/proxy-state.d.ts.map +1 -0
  108. package/dist/src/reactive/proxy.d.ts +23 -0
  109. package/dist/src/reactive/proxy.d.ts.map +1 -0
  110. package/dist/src/reactive/record.d.ts +115 -0
  111. package/dist/src/reactive/record.d.ts.map +1 -0
  112. package/dist/src/reactive/register.d.ts +125 -0
  113. package/dist/src/reactive/register.d.ts.map +1 -0
  114. package/dist/src/reactive/registry.d.ts +21 -0
  115. package/dist/src/reactive/registry.d.ts.map +1 -0
  116. package/dist/src/reactive/set.d.ts +26 -0
  117. package/dist/src/reactive/set.d.ts.map +1 -0
  118. package/dist/src/reactive/tracking.d.ts +7 -0
  119. package/dist/src/reactive/tracking.d.ts.map +1 -0
  120. package/dist/src/reactive/types.d.ts +424 -0
  121. package/dist/src/reactive/types.d.ts.map +1 -0
  122. package/dist/src/reactive/watch.d.ts +48 -0
  123. package/dist/src/reactive/watch.d.ts.map +1 -0
  124. package/dist/src/std-decorators.d.ts +45 -0
  125. package/dist/src/std-decorators.d.ts.map +1 -0
  126. package/dist/src/utils.d.ts +49 -0
  127. package/dist/src/utils.d.ts.map +1 -0
  128. package/dist/src/zone.d.ts +40 -0
  129. package/dist/src/zone.d.ts.map +1 -0
  130. package/dist/types-DaHFfhlN.d.ts +414 -0
  131. package/docs/ai/api-reference.md +1 -1
  132. package/docs/ai/manual.md +443 -17
  133. package/docs/events.md +57 -2
  134. package/docs/flavored.md +210 -0
  135. package/docs/reactive/advanced.md +2 -3
  136. package/docs/reactive/attend.md +143 -0
  137. package/docs/reactive/collections.md +55 -0
  138. package/docs/reactive/core.md +120 -4
  139. package/docs/reactive/debugging.md +8 -10
  140. package/docs/reactive/describe.md +85 -0
  141. package/docs/reactive/error-handling.md +121 -0
  142. package/docs/reactive/project.md +1 -1
  143. package/docs/reactive/scan.md +215 -0
  144. package/docs/reactive.md +2 -0
  145. package/docs/std-decorators.md +171 -52
  146. package/docs/zone.md +9 -6
  147. package/package.json +44 -27
  148. package/src/async/browser.ts +277 -41
  149. package/src/async/index.ts +23 -4
  150. package/src/async/node.ts +78 -32
  151. package/src/entry-browser.ts +5 -0
  152. package/src/entry-node.ts +5 -0
  153. package/src/eventful.ts +82 -49
  154. package/src/flavored.ts +106 -0
  155. package/src/index.d.ts +12 -9
  156. package/src/index.ts +2 -1
  157. package/src/introspection.ts +1 -1
  158. package/src/reactive/array.ts +160 -80
  159. package/src/reactive/buffer.ts +187 -27
  160. package/src/reactive/change.ts +16 -23
  161. package/src/reactive/deep-touch.ts +19 -14
  162. package/src/reactive/deep-watch-state.ts +3 -3
  163. package/src/reactive/deep-watch.ts +7 -4
  164. package/src/reactive/describe.ts +39 -0
  165. package/src/reactive/effect-context.ts +71 -6
  166. package/src/reactive/effects.ts +445 -393
  167. package/src/reactive/index.ts +12 -14
  168. package/src/reactive/map.ts +8 -24
  169. package/src/reactive/memoize.ts +26 -3
  170. package/src/reactive/project.md +107 -0
  171. package/src/reactive/project.ts +48 -71
  172. package/src/reactive/proxy.ts +21 -3
  173. package/src/reactive/record.ts +30 -45
  174. package/src/reactive/register.ts +138 -19
  175. package/src/reactive/registry.ts +22 -9
  176. package/src/reactive/set.ts +7 -20
  177. package/src/reactive/tracking.ts +9 -12
  178. package/src/reactive/types.ts +104 -24
  179. package/src/reactive/{interface.ts → watch.ts} +29 -59
  180. package/src/std-decorators.ts +56 -17
  181. package/src/utils.ts +3 -4
  182. package/src/zone.ts +43 -28
  183. package/dist/browser.js +0 -161
  184. package/dist/browser.js.map +0 -1
  185. package/dist/chunks/index-BFYK02LG.js +0 -6378
  186. package/dist/chunks/index-BFYK02LG.js.map +0 -1
  187. package/dist/chunks/index-CNR6QRUl.esm.js +0 -6291
  188. package/dist/chunks/index-CNR6QRUl.esm.js.map +0 -1
  189. package/dist/node.js +0 -136
  190. package/dist/node.js.map +0 -1
  191. package/src/reactive/debug.ts +0 -517
@@ -0,0 +1,210 @@
1
+ # Flavored Functions
2
+
3
+ The `flavored` utility creates extensible functions with chainable property modifiers. It enables a fluent API where properties return specialized variants of the base function.
4
+
5
+ ## Overview
6
+
7
+ Flavored functions allow you to:
8
+ - Create property-based modifiers that transform how the function is called
9
+ - Chain multiple modifiers together
10
+ - Use either automatic option merging (`flavorOptions`) or custom argument transformation (`createFlavor`)
11
+ - Return hand-made functions for complete control (the generic case)
12
+
13
+ ## Basic Usage
14
+
15
+ ### Creating a Flavored Function
16
+
17
+ ```typescript
18
+ import { flavored, flavorOptions } from 'mutts'
19
+
20
+ const greet = flavored(
21
+ (name: string, options?: { loud?: boolean }) => {
22
+ const greeting = `Hello, ${name}!`
23
+ return options?.loud ? greeting.toUpperCase() : greeting
24
+ },
25
+ {
26
+ get loud() {
27
+ return flavorOptions(this, { loud: true })
28
+ }
29
+ }
30
+ )
31
+
32
+ greet('World') // "Hello, World!"
33
+ greet.loud('World') // "HELLO, WORLD!"
34
+ ```
35
+
36
+ ## API Reference
37
+
38
+ ### `flavored(fn, flavors)`
39
+
40
+ Creates a flavored function with chainable property modifiers.
41
+
42
+ **Parameters:**
43
+ - `fn` - The base function to flavor
44
+ - `flavors` - Object defining the flavor properties (getters or methods)
45
+
46
+ **Returns:** A proxy of the function with the flavor properties attached.
47
+
48
+ **Example:**
49
+ ```typescript
50
+ const effect = flavored(baseEffect, {
51
+ get opaque() {
52
+ return flavorOptions(this, { opaque: true })
53
+ },
54
+ named(name: string) {
55
+ return flavorOptions(this, { name })
56
+ }
57
+ })
58
+
59
+ effect(fn) // Basic usage
60
+ effect.opaque(fn) // With opaque option
61
+ effect.named('myEffect')(fn) // With name option
62
+ effect.opaque.named('x')(fn) // Chained
63
+ ```
64
+
65
+ ### `flavorOptions(fn, defaultOptions)`
66
+
67
+ Creates a flavored variant that merges options with the last argument.
68
+
69
+ **Use when:** The last argument is an options object that should be merged.
70
+
71
+ **Parameters:**
72
+ - `fn` - The base flavored function
73
+ - `defaultOptions` - Options to merge with provided options
74
+
75
+ **Example:**
76
+ ```typescript
77
+ const loudGreet = flavorOptions(greet, { loud: true })
78
+ loudGreet('World') // Uses { loud: true }
79
+ loudGreet('World', { prefix: 'Hi' }) // Merges both options
80
+ ```
81
+
82
+ ### `createFlavor(fn, transform)`
83
+
84
+ Creates a flavored variant with custom argument transformation.
85
+
86
+ **Use when:** You need to transform arguments before calling the base function.
87
+
88
+ **Parameters:**
89
+ - `fn` - The base flavored function
90
+ - `transform` - Function that receives and returns the arguments tuple
91
+
92
+ **Example:**
93
+ ```typescript
94
+ const doubleArgs = createFlavor(add, (a: number, b: number): [number, number] => {
95
+ return [a * 2, b * 2]
96
+ })
97
+ doubleArgs(3, 4) // Returns 14 (6 + 8)
98
+ ```
99
+
100
+ ## The Generic Case: Hand-Made Functions
101
+
102
+ When `flavorOptions` or `createFlavor` don't fit your needs, you can return a **hand-made function** directly. This gives you complete control over the behavior.
103
+
104
+ ### Basic Hand-Made Function
105
+
106
+ ```typescript
107
+ const fetch = flavored(fetchData, {
108
+ // Returns a completely custom function
109
+ withTimeout(timeout: number) {
110
+ return (url: string, options?: { retries?: number }) => {
111
+ return fetchData(url, { ...options, timeout })
112
+ }
113
+ }
114
+ })
115
+
116
+ // Usage
117
+ const fetchWith500ms = fetch.withTimeout(500)
118
+ fetchWith500ms('api.com') // Uses timeout: 500
119
+ ```
120
+
121
+ ### Re-Flavoring Hand-Made Functions
122
+
123
+ Hand-made functions can be re-flavored to enable further chaining:
124
+
125
+ ```typescript
126
+ const process = flavored(baseProcess, {
127
+ presetMultiplier(multiplier: number) {
128
+ // Create the preset function
129
+ const presetFn = (value: number, options?: { offset?: number }) => {
130
+ return baseProcess(value, { ...options, multiplier })
131
+ }
132
+ // Re-flavor it to enable chaining
133
+ return flavored(presetFn, {
134
+ get withOffset() {
135
+ return flavorOptions(this, { offset: 10 })
136
+ }
137
+ })
138
+ }
139
+ })
140
+
141
+ // Usage
142
+ const times3 = process.presetMultiplier(3)
143
+ times3(5) // 15 (5 * 3)
144
+ times3.withOffset(5) // 20 (5 * 3 + 5)
145
+ ```
146
+
147
+ ### When to Use Each Approach
148
+
149
+ | Approach | Use When |
150
+ |----------|----------|
151
+ | `flavorOptions` | You have an options object as the last argument |
152
+ | `createFlavor` | You need to transform arguments structurally |
153
+ | **Hand-made function** | You need complete control, different signature, or side effects |
154
+
155
+ ## Advanced Patterns
156
+
157
+ ### Chaining Multiple Modifiers
158
+
159
+ ```typescript
160
+ const createUser = flavored(createUserBase, {
161
+ get admin() {
162
+ return flavorOptions(this, { admin: true })
163
+ },
164
+ get verified() {
165
+ return flavorOptions(this, { verified: true })
166
+ }
167
+ })
168
+
169
+ createUser.admin.verified('Alice') // Both options applied
170
+ ```
171
+
172
+ ### Combining Approaches
173
+
174
+ ```typescript
175
+ const calculator = flavored(
176
+ (a: number, b: number, opts?: { multiply?: boolean }) => {
177
+ return opts?.multiply ? a * b : a + b
178
+ },
179
+ {
180
+ get multiply() {
181
+ return flavorOptions(this, { multiply: true })
182
+ },
183
+ double() {
184
+ return createFlavor(this, (a, b, opts?): [number, number, typeof opts] => {
185
+ return [a * 2, b * 2, opts]
186
+ })
187
+ }
188
+ }
189
+ )
190
+
191
+ calculator.multiply(3, 4) // 12
192
+ calculator.double()(3, 4) // 14 (6 + 8)
193
+ ```
194
+
195
+ ## TypeScript Considerations
196
+
197
+ Flavored functions use proxies and require type assertions for complex chaining scenarios:
198
+
199
+ ```typescript
200
+ const effectWithModifiers = flavored(effect, {
201
+ get opaque(): EffectWithModifiers {
202
+ return flavorOptions(this, { opaque: true }) as EffectWithModifiers
203
+ },
204
+ named(name: string): EffectWithModifiers {
205
+ return flavorOptions(this, { name }) as EffectWithModifiers
206
+ }
207
+ })
208
+ ```
209
+
210
+ The `as EffectWithModifiers` cast enables proper type inference for chained modifiers.
@@ -1169,8 +1169,7 @@ For a full guide on debugging, including cycle detection and memoization discrep
1169
1169
 
1170
1170
  ### Quick Summary
1171
1171
 
1172
- - **Cycle Detection**: Automatically catch circular dependencies via `reactiveOptions.cycleHandling`. Note: Instant mathematical detection requires choosing a mode other than `'none'`.
1173
- - **Flat Mode**: The default `reactiveOptions.cycleHandling = 'none'` provides maximum performance in high-frequency update scenarios by disabling graph maintenance.
1172
+ - **Cycle Detection**: Automatically catch circular dependencies via `reactiveOptions.cycleHandling`. Note: Instant mathematical detection requires choosing `'development'` or `'debug'` mode.
1173
+ - **Production Mode**: The default `reactiveOptions.cycleHandling` is set to `'production'`, providing maximum performance in high-frequency update scenarios by disabling graph maintenance.
1174
1174
  - **Memoization Discrepancy**: Detect "missing dependencies" by running computations twice during development using `reactiveOptions.onMemoizationDiscrepancy`.
1175
1175
  - **Global Hooks**: Use `reactiveOptions.touched`, `enter`, and `leave` to observe system activity.
1176
-
@@ -0,0 +1,143 @@
1
+ # Reactive Enumeration (`attend`)
2
+
3
+ The `attend` utility reactively iterates over the entries of a collection, running a per-key effect that is automatically created when a key appears and disposed when it disappears.
4
+
5
+ ## Overview
6
+
7
+ `attend` is the reactive equivalent of `forEach`. It:
8
+ - Tracks the **keys** (or indices, or values) of a collection inside an outer effect.
9
+ - **Creates** an inner effect for each key, via `ascend`.
10
+ - **Disposes** the inner effect when the key is removed from the collection.
11
+ - Allows the callback to return a **cleanup function** (like a regular effect closer).
12
+
13
+ This is the foundational lifecycle primitive that `describe` and `organized` are built on.
14
+
15
+ ## API
16
+
17
+ ```typescript
18
+ // Raw enumeration callback
19
+ function attend<Key>(
20
+ enumerate: () => Iterable<Key>,
21
+ callback: (key: Key) => EffectCloser | void
22
+ ): ScopedCallback
23
+
24
+ // Collection shorthands
25
+ function attend<T>(source: readonly T[], callback: (index: number) => EffectCloser | void): ScopedCallback
26
+ function attend<K, V>(source: Map<K, V>, callback: (key: K) => EffectCloser | void): ScopedCallback
27
+ function attend<T>(source: Set<T>, callback: (value: T) => EffectCloser | void): ScopedCallback
28
+ function attend<S extends Record<PropertyKey, any>>(source: S, callback: (key: keyof S & string) => EffectCloser | void): ScopedCallback
29
+ ```
30
+
31
+ ### Parameters
32
+
33
+ - **`source`** or **`enumerate`**: Either a collection (array, record, Map, Set) or a callback returning an `Iterable<Key>`. The enumeration runs inside the outer effect, so reactive reads (e.g. `source.length`, `Object.keys(source)`) are tracked automatically.
34
+ - **`callback`**: Called per key inside an inner effect. May return a cleanup function that runs when the key is removed or before the inner effect re-executes.
35
+
36
+ ### Returns
37
+
38
+ A `ScopedCallback` that tears down all inner effects and the outer effect.
39
+
40
+ ## Basic Usage
41
+
42
+ ### Record
43
+
44
+ ```typescript
45
+ import { reactive, attend } from 'mutts'
46
+
47
+ const config = reactive({ theme: 'dark', lang: 'en' })
48
+
49
+ const stop = attend(config, (key) => {
50
+ console.log(`${key} = ${config[key]}`)
51
+ return () => console.log(`cleanup: ${key}`)
52
+ })
53
+
54
+ // Logs: "theme = dark", "lang = en"
55
+
56
+ config.debug = true
57
+ // Logs: "debug = true"
58
+
59
+ delete config.lang
60
+ // Logs: "cleanup: lang"
61
+
62
+ config.theme = 'light'
63
+ // Logs: "cleanup: theme" then "theme = light"
64
+ // (inner effect re-runs: previous cleanup fires, then new execution)
65
+
66
+ stop()
67
+ // Disposes everything
68
+ ```
69
+
70
+ ### Array
71
+
72
+ ```typescript
73
+ const items = reactive([10, 20, 30])
74
+
75
+ attend(items, (i) => {
76
+ console.log(`[${i}] = ${items[i]}`)
77
+ })
78
+
79
+ items.push(40)
80
+ // Logs: "[3] = 40"
81
+ ```
82
+
83
+ ### Map
84
+
85
+ ```typescript
86
+ const registry = reactive(new Map([['a', 1]]))
87
+
88
+ attend(registry, (key) => {
89
+ console.log(`${key} -> ${registry.get(key)}`)
90
+ return () => console.log(`removed: ${key}`)
91
+ })
92
+
93
+ registry.set('b', 2) // Logs: "b -> 2"
94
+ registry.delete('a') // Logs: "removed: a"
95
+ ```
96
+
97
+ ### Set
98
+
99
+ ```typescript
100
+ const tags = reactive(new Set(['alpha', 'beta']))
101
+
102
+ attend(tags, (value) => {
103
+ console.log(`tag: ${value}`)
104
+ return () => console.log(`untagged: ${value}`)
105
+ })
106
+
107
+ tags.add('gamma') // Logs: "tag: gamma"
108
+ tags.delete('alpha') // Logs: "untagged: alpha"
109
+ ```
110
+
111
+ ### Raw enumeration callback
112
+
113
+ For custom iteration logic or non-standard collections:
114
+
115
+ ```typescript
116
+ const source = reactive({ a: 1, b: 2 })
117
+
118
+ attend(
119
+ () => Reflect.ownKeys(source),
120
+ (key) => {
121
+ console.log(key, source[key])
122
+ }
123
+ )
124
+ ```
125
+
126
+ ## How it Works
127
+
128
+ 1. An **outer effect** calls `enumerate()` (or derives it from the collection type), collecting the current keys into a `Set`.
129
+ 2. For each **new key**, `ascend` creates an inner effect that runs the callback.
130
+ 3. For each **removed key**, the corresponding inner effect is stopped (which triggers its cleanup).
131
+ 4. The **inner effect** tracks its own reactive dependencies — so if a value changes for an existing key, only that key's effect re-runs.
132
+
133
+ ## Relationship to Other Primitives
134
+
135
+ | Primitive | Uses `attend`? | Purpose |
136
+ |---|---|---|
137
+ | `attend` | — | Reactive lifecycle per key |
138
+ | `describe` | ✅ | Reactive `Object.defineProperty` per key |
139
+ | `organized` | ✅ | Reactive record mapping with access objects |
140
+ | `project` | ❌ | Reactive collection mapping (manages a target + projection context) |
141
+ | `scan` | ❌ | Reactive accumulation (sequential key dependency) |
142
+
143
+ `project` and `scan` have additional concerns (target management, sequential dependencies) that go beyond `attend`'s independent-key lifecycle model.
@@ -347,6 +347,61 @@ The `Register` exposes additional methods and behaviors that standard arrays do
347
347
  - `toArray(): T[]` materializes the current values into a plain array.
348
348
  - `toString(): string` returns a concise description like `[Register length=3]`.
349
349
 
350
+ ### Register CRUD Events
351
+
352
+ `Register` emits lifecycle events for add, delete, update, and rekey operations. This enables side effects like logging, syncing with external systems, or triggering derived updates.
353
+
354
+ ```typescript
355
+ const list = register(({id}: { id: number }) => id)
356
+
357
+ // Listen to individual events
358
+ list.on('add', (item, key, index) => {
359
+ console.log(`Added ${key} at index ${index}:`, item)
360
+ })
361
+
362
+ list.on('delete', (item, key, index) => {
363
+ console.log(`Removed ${key} from index ${index}:`, item)
364
+ })
365
+
366
+ list.on('update', (oldItem, newItem, key, index) => {
367
+ console.log(`Updated ${key} at index ${index}`)
368
+ })
369
+
370
+ list.on('rekey', (item, oldKey, newKey, index) => {
371
+ console.log(`Key changed from ${oldKey} to ${newKey}`)
372
+ })
373
+
374
+ // Bulk event registration
375
+ list.on({
376
+ add: (item) => console.log('Added:', item),
377
+ delete: (item) => console.log('Deleted:', item),
378
+ })
379
+
380
+ // Global hook - receive all events
381
+ const unhook = list.hook((event, ...args) => {
382
+ console.log(`Event: ${String(event)}`, args)
383
+ })
384
+
385
+ // Unsubscribe
386
+ const unsubscribe = list.on('add', handler)
387
+ unsubscribe()
388
+ ```
389
+
390
+ **Event Types:**
391
+
392
+ | Event | Arguments | Description |
393
+ |-------|-----------|-------------|
394
+ | `add` | `(item, key, index)` | New item added to register |
395
+ | `delete` | `(item, key, index)` | Item removed from register |
396
+ | `update` | `(oldItem, newItem, key, index)` | Item value updated (same key) |
397
+ | `rekey` | `(item, oldKey, newKey, index)` | Item's key changed |
398
+
399
+ **Use Cases:**
400
+ - Audit logging
401
+ - Syncing with databases
402
+ - Triggering notifications
403
+ - Cascading updates to dependent systems
404
+
350
405
  Notes:
351
406
  - Direct length modification via `list.length = n` is not supported; use `splice` instead.
352
407
  - Assigning to an index (`list[i] = value`) uses the key function to bind that slot to `value`’s key.
@@ -7,6 +7,7 @@
7
7
  - [5-Minute Quick Start](#5-minute-quick-start)
8
8
  - [Core API](#core-api)
9
9
  - [Effect System](#effect-system)
10
+ - [Error Handling](./error-handling.md)
10
11
  - [Atomic Operations](./advanced.md#atomic-operations)
11
12
  - [Advanced Effects](./advanced.md#advanced-effects)
12
13
  - [Evolution Tracking](./advanced.md#evolution-tracking)
@@ -19,6 +20,7 @@
19
20
  - [Non-Reactive System](./advanced.md#non-reactive-system)
20
21
  - [Array Mapping](./advanced.md#array-mapping)
21
22
  - [Projection](./advanced.md#projection)
23
+ - [Reactive Property Description](./describe.md)
22
24
  - [Record Organization](./advanced.md#record-organization)
23
25
  - [Memoization](./advanced.md#memoization)
24
26
  - [Debugging and Development](./advanced.md#debugging-and-development)
@@ -111,6 +113,19 @@ const doubled = project(source, ({ value }) => value * 2)
111
113
  source.push(4) // doubled automatically becomes [2, 4, 6, 8]
112
114
  ```
113
115
 
116
+ **6. Define properties reactively:**
117
+ ```typescript
118
+ const target = {}
119
+ const descriptors = reactive({
120
+ foo: { value: 1, enumerable: true }
121
+ })
122
+ describe(descriptors, target)
123
+ // target.foo is 1
124
+
125
+ descriptors.bar = { value: 2, enumerable: true }
126
+ // target.bar is 2 automatically
127
+ ```
128
+
114
129
  **Ready to go!** Continue reading for advanced features.
115
130
 
116
131
  ---
@@ -204,6 +219,19 @@ reactiveObj.count = 5 // Triggers effect
204
219
 
205
220
  See [Prototype Chains and Pure Objects](#prototype-chains-and-pure-objects) for detailed information about prototype chain handling.
206
221
 
222
+ ### `describe()`
223
+
224
+ Reactively defines properties on a target object based on a descriptors record.
225
+
226
+ ```typescript
227
+ function describe<T extends object>(
228
+ descriptors: Record<PropertyKey, PropertyDescriptor>,
229
+ target: T = Object.create(null) as T
230
+ ): T
231
+ ```
232
+
233
+ See the [Reactive Property Description](./describe.md) guide for details.
234
+
207
235
  ### `effect()`
208
236
 
209
237
  Creates a reactive effect that automatically re-runs when dependencies change.
@@ -448,7 +476,7 @@ state.c = 15 // Does NOT trigger effect
448
476
 
449
477
  The `effect` function provides a special `access` parameter with `tracked` and `ascend` functions that restore the active effect context for dependency tracking in asynchronous operations.
450
478
 
451
- In modern `mutts`, this is powered by the **Zone system**. When you use `configureAsyncZone()`, the active effect context is automatically preserved across `await` points and timers, making manual use of `tracked` optional for these cases.
479
+ In modern `mutts`, this is powered by the **Zone system**. When zones are registered in `asyncZone`, the active effect context is automatically preserved across `await` points and timers, making manual use of `tracked` optional for these cases.
452
480
 
453
481
  #### The Problem with Async Effects
454
482
 
@@ -466,7 +494,7 @@ effect(() => {
466
494
  const another = state.name // ✅ Tracked (active effect is still set)
467
495
  })
468
496
 
469
- // Async effect WITHOUT configureAsyncZone() - context is lost after await
497
+ // Async effect WITHOUT zone registration - context is lost after await
470
498
  effect(async () => {
471
499
  const value = state.count // ✅ Tracked
472
500
 
@@ -476,7 +504,7 @@ effect(async () => {
476
504
  const another = state.name // ❌ NOT tracked
477
505
  })
478
506
 
479
- // Async effect WITH configureAsyncZone() - context is preserved
507
+ // Async effect WITH zone registration - context is preserved
480
508
  effect(async () => {
481
509
  const value = state.count // ✅ Tracked
482
510
 
@@ -497,7 +525,7 @@ effect(async ({ tracked }) => {
497
525
 
498
526
  #### Key Benefits of the Zone System
499
527
 
500
- 1. **Automatic Restoration**: With `configureAsyncZone()`, most native async APIs (Promises, timers) automatically preserve the reactive context.
528
+ 1. **Automatic Restoration**: With zones registered in `asyncZone`, most native async APIs (Promises, timers) automatically preserve the reactive context.
501
529
  2. **Manual Control**: `access.tracked()` allows you to manually "passport" the context into third-party libraries or unmanaged callbacks.
502
530
 
503
531
  ### Using `ascend` for Parent Effect Tracking
@@ -603,6 +631,94 @@ reactiveOptions.leave = (effect) => console.log('Leaving effect:', effect)
603
631
  reactiveOptions.chain = (caller, target) => console.log('Chaining:', caller, '->', target)
604
632
  ```
605
633
 
634
+ ### Effect Modifiers
635
+
636
+ The `effect` function provides convenient shortcut modifiers for common options. These can be chained for more concise syntax:
637
+
638
+ #### `.opaque`
639
+
640
+ Creates an opaque effect that tracks object references rather than deep content. This is useful when you want effects to re-run only when the object identity changes, not when its properties change.
641
+
642
+ ```typescript
643
+ import { effect, reactive } from 'mutts/reactive'
644
+
645
+ const item = reactive({ id: 1, data: { value: 10 } })
646
+
647
+ // Regular effect - triggers on any property change
648
+ effect(() => {
649
+ console.log('Item data:', item.data.value) // Triggers on item.data.value changes
650
+ })
651
+
652
+ // Opaque effect - only triggers when item.data reference changes
653
+ effect.opaque(() => {
654
+ console.log('Data object:', item.data) // Only triggers when item.data is replaced
655
+ })
656
+
657
+ item.data.value = 20 // Triggers regular effect, NOT opaque effect
658
+ item.data = { value: 30 } // Triggers BOTH effects
659
+ ```
660
+
661
+ **Use cases for opaque effects:**
662
+ - When you only care about object identity (e.g., cache keys, memoization)
663
+ - When deep watching would be too expensive
664
+ - When working with external data that shouldn't trigger deep reactivity
665
+
666
+ #### `.named(name)`
667
+
668
+ Creates a named effect for easier debugging and profiling. The name appears in DevTools and debug logs.
669
+
670
+ ```typescript
671
+ import { effect, reactive } from 'mutts/reactive'
672
+
673
+ const state = reactive({ count: 0 })
674
+
675
+ // Create a named effect
676
+ effect.named('counter-effect')(() => {
677
+ console.log('Count:', state.count)
678
+ })
679
+
680
+ // Named effects can also be combined with other options
681
+ effect.named('data-loader').opaque(() => {
682
+ console.log('Loading data...')
683
+ })
684
+ ```
685
+
686
+ **Benefits of named effects:**
687
+ - Easier identification in DevTools
688
+ - Better stack traces during debugging
689
+ - Helpful for performance profiling
690
+
691
+ #### Combining Modifiers
692
+
693
+ Modifiers can be chained in any order:
694
+
695
+ ```typescript
696
+ // Named opaque effect
697
+ effect.named('my-effect').opaque(() => {
698
+ // Effect code
699
+ })
700
+
701
+ // These are equivalent - order doesn't matter for the result
702
+ effect.opaque.named('my-effect')(() => {
703
+ // Effect code
704
+ })
705
+ ```
706
+
707
+ Note: The modifiers return new effect functions with the options pre-applied, so they can be stored and reused:
708
+
709
+ ```typescript
710
+ // Create a reusable named effect factory
711
+ const createDataEffect = effect.named('data-layer')
712
+
713
+ createDataEffect(() => {
714
+ console.log('Effect 1')
715
+ })
716
+
717
+ createDataEffect(() => {
718
+ console.log('Effect 2')
719
+ })
720
+ ```
721
+
606
722
 
607
723
  ### `@reactive` Decorator
608
724
 
@@ -25,7 +25,7 @@ These hooks are called during the execution of effects and computed values.
25
25
 
26
26
  - **`beginChain(targets: Function[]) / endChain()`**: Called when a batch of effects starts and ends its execution.
27
27
  - **`maxEffectChain`**: (Default: `100`) Limits the depth of synchronous effect triggering to prevent stack overflows.
28
- - **`maxTriggerPerBatch`**: (Default: `10`) Limits how many times a single effect can be triggered within the same batch. Useful for detecting aggressive re-computation or infinite cycles in `cycleHandling: 'none'` mode.
28
+ - **`maxTriggerPerBatch`**: (Default: `10`) Limits how many times a single effect can be triggered within the same batch. Useful for detecting aggressive re-computation or infinite cycles in `cycleHandling: 'production'` mode.
29
29
 
30
30
  ## Cycle Detection
31
31
 
@@ -35,18 +35,16 @@ These hooks are called during the execution of effects and computed values.
35
35
 
36
36
  You can control how cycles are handled via `reactiveOptions.cycleHandling`:
37
37
 
38
- - **`'none'`** (Default): High-performance FIFO mode. Disables the dependency graph and topological sorting.
39
- - **`'throw'`**: Throws a `ReactiveError` with a detailed path.
40
- - **`'warn'`**: Logs a warning but breaks the cycle to allow the application to continue.
41
- - **`'break'`**: Silently breaks the cycle.
42
- - **`'strict'`**: Performs a graph check *before* execution to prevent cycles from even starting. This has the highest overhead.
38
+ - **`'production'`**: High-performance FIFO mode. Disables the dependency graph and topological sorting. Uses heuristic detection via `maxEffectChain`.
39
+ - **`'development'`** (Default): Maintains direct dependency graph for early cycle detection during edge creation. Throws immediately with basic path information.
40
+ - **`'debug'`**: Full diagnostic mode with transitive closures and topological sorting. Provides detailed cycle path reporting.
43
41
 
44
42
  ### Topological vs. Flat Mode Detection
45
43
 
46
44
  | Mode | `cycleHandling` | Detection Method | Error Code |
47
45
  | :--- | :--- | :--- | :--- |
48
- | **Topological** | `'throw'` (or other) | **Mathematical**: Analyzes the dependency graph. | `CYCLE_DETECTED` |
49
- | **Flat Mode** | `'none'` (Default) | **Heuristic**: Counts executions per batch. | `MAX_REACTION_EXCEEDED` |
46
+ | **Debug** | `'debug'` or `'development'` | **Mathematical**: Analyzes the dependency graph. | `CYCLE_DETECTED` |
47
+ | **Production** | `'production'` (Default) | **Heuristic**: Counts executions per batch. | `MAX_REACTION_EXCEEDED` |
50
48
 
51
49
  In **Topological mode**, the system maintains a transitive closure of all effects, allowing it to know instantly if an effect is its own cause. In **Flat mode**, the system is "blind" to the graph and relies on the execution threshold (`maxTriggerPerBatch`) to interrupt infinite loops.
52
50
 
@@ -157,12 +155,12 @@ Since these are runtime options, you can toggle them based on your environment:
157
155
 
158
156
  ```typescript
159
157
  if (process.env.NODE_ENV === 'development') {
160
- reactiveOptions.cycleHandling = 'throw';
158
+ reactiveOptions.cycleHandling = 'debug';
161
159
  reactiveOptions.onMemoizationDiscrepancy = myHandler;
162
160
  enableIntrospection();
163
161
  } else {
164
162
  // Ensure they are off in production for performance
165
163
  reactiveOptions.onMemoizationDiscrepancy = undefined;
166
- reactiveOptions.cycleHandling = 'break';
164
+ reactiveOptions.cycleHandling = 'production';
167
165
  }
168
166
  ```