mutts 1.0.9 → 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/README.md +58 -33
  2. package/dist/browser.cjs +1371 -1206
  3. package/dist/browser.cjs.map +1 -1
  4. package/dist/browser.d.ts +2 -1392
  5. package/dist/browser.dev.cjs +110 -0
  6. package/dist/browser.dev.cjs.map +1 -0
  7. package/dist/browser.dev.d.ts +2 -0
  8. package/dist/browser.dev.esm.js +5 -0
  9. package/dist/browser.dev.esm.js.map +1 -0
  10. package/dist/browser.esm.js +6 -87
  11. package/dist/browser.esm.js.map +1 -1
  12. package/dist/chunks/{async-browser-lvzLOCgk.cjs → async-browser-BU_IfxYD.cjs} +4 -85
  13. package/dist/chunks/async-browser-BU_IfxYD.cjs.map +1 -0
  14. package/dist/chunks/{async-node-C3DeIb0y.cjs → async-node-3PrbVAbB.cjs} +3 -1
  15. package/dist/chunks/async-node-3PrbVAbB.cjs.map +1 -0
  16. package/dist/chunks/{index-2vea86wD.esm.js → index-CaaQQlPJ.esm.js} +1116 -953
  17. package/dist/chunks/index-CaaQQlPJ.esm.js.map +1 -0
  18. package/dist/chunks/node-nKJBk8iJ.esm.js +96 -0
  19. package/dist/chunks/node-nKJBk8iJ.esm.js.map +1 -0
  20. package/dist/chunks/{index-VTO-b2vR.cjs → proxy-Dtg-bJ3T.cjs} +2285 -2983
  21. package/dist/chunks/proxy-Dtg-bJ3T.cjs.map +1 -0
  22. package/dist/chunks/{index-CtA2AWl3.esm.js → proxy-r7lARftl.esm.js} +2273 -2948
  23. package/dist/chunks/proxy-r7lARftl.esm.js.map +1 -0
  24. package/dist/debug.cjs +973 -28
  25. package/dist/debug.cjs.map +1 -1
  26. package/dist/debug.d.ts +36 -37
  27. package/dist/debug.esm.js +949 -1
  28. package/dist/debug.esm.js.map +1 -1
  29. package/dist/devtools/manifest.json +1 -1
  30. package/dist/devtools/panel.html +1 -1
  31. package/dist/devtools/panel.js +107 -94
  32. package/dist/devtools/panel.js.map +1 -1
  33. package/dist/index.d.ts +1447 -1
  34. package/dist/mutts.umd.js +7167 -1
  35. package/dist/mutts.umd.js.map +1 -1
  36. package/dist/mutts.umd.min.js +1 -1
  37. package/dist/mutts.umd.min.js.map +1 -1
  38. package/dist/node.cjs +62 -60
  39. package/dist/node.cjs.map +1 -1
  40. package/dist/node.d.ts +2 -2
  41. package/dist/node.dev.cjs +110 -0
  42. package/dist/node.dev.cjs.map +1 -0
  43. package/dist/node.dev.d.ts +2 -0
  44. package/dist/node.dev.esm.js +6 -0
  45. package/dist/node.dev.esm.js.map +1 -0
  46. package/dist/node.esm.js +4 -97
  47. package/dist/node.esm.js.map +1 -1
  48. package/dist/{types-DaHFfhlN.d.ts → types-W5vD6m2n.d.ts} +99 -44
  49. package/docs/ai/api-reference.md +11 -18
  50. package/docs/ai/manual.md +308 -230
  51. package/docs/reactive/advanced.md +312 -2
  52. package/docs/reactive/attend.md +1 -2
  53. package/docs/reactive/collections.md +21 -39
  54. package/docs/reactive/core.md +7 -37
  55. package/docs/reactive/debugging.md +117 -10
  56. package/docs/reactive/error-handling.md +10 -10
  57. package/docs/reactive/resource.md +125 -0
  58. package/docs/reactive/scan.md +32 -1
  59. package/docs/reactive.md +3 -2
  60. package/docs/utils.md +70 -0
  61. package/package.json +59 -30
  62. package/dist/chunks/async-browser-lvzLOCgk.cjs.map +0 -1
  63. package/dist/chunks/async-node-C3DeIb0y.cjs.map +0 -1
  64. package/dist/chunks/index-2vea86wD.esm.js.map +0 -1
  65. package/dist/chunks/index-CtA2AWl3.esm.js.map +0 -1
  66. package/dist/chunks/index-VTO-b2vR.cjs.map +0 -1
  67. package/dist/debug/debug.d.ts +0 -122
  68. package/dist/debug/debug.d.ts.map +0 -1
  69. package/dist/debug/index.d.ts +0 -4
  70. package/dist/debug/index.d.ts.map +0 -1
  71. package/dist/debug/lineage-panel.d.ts +0 -5
  72. package/dist/debug/lineage-panel.d.ts.map +0 -1
  73. package/dist/debug/lineage.d.ts +0 -79
  74. package/dist/debug/lineage.d.ts.map +0 -1
  75. package/dist/src/async/browser.d.ts +0 -2
  76. package/dist/src/async/browser.d.ts.map +0 -1
  77. package/dist/src/async/index.d.ts +0 -19
  78. package/dist/src/async/index.d.ts.map +0 -1
  79. package/dist/src/async/node.d.ts +0 -2
  80. package/dist/src/async/node.d.ts.map +0 -1
  81. package/dist/src/decorator.d.ts +0 -106
  82. package/dist/src/decorator.d.ts.map +0 -1
  83. package/dist/src/destroyable.d.ts +0 -87
  84. package/dist/src/destroyable.d.ts.map +0 -1
  85. package/dist/src/entry-browser.d.ts +0 -3
  86. package/dist/src/entry-browser.d.ts.map +0 -1
  87. package/dist/src/entry-node.d.ts +0 -3
  88. package/dist/src/entry-node.d.ts.map +0 -1
  89. package/dist/src/eventful.d.ts +0 -20
  90. package/dist/src/eventful.d.ts.map +0 -1
  91. package/dist/src/flavored.d.ts +0 -33
  92. package/dist/src/flavored.d.ts.map +0 -1
  93. package/dist/src/index.d.ts +0 -14
  94. package/dist/src/index.d.ts.map +0 -1
  95. package/dist/src/indexable.d.ts +0 -243
  96. package/dist/src/indexable.d.ts.map +0 -1
  97. package/dist/src/introspection.d.ts +0 -27
  98. package/dist/src/introspection.d.ts.map +0 -1
  99. package/dist/src/iterableWeak.d.ts +0 -53
  100. package/dist/src/iterableWeak.d.ts.map +0 -1
  101. package/dist/src/mixins.d.ts +0 -25
  102. package/dist/src/mixins.d.ts.map +0 -1
  103. package/dist/src/promiseChain.d.ts +0 -20
  104. package/dist/src/promiseChain.d.ts.map +0 -1
  105. package/dist/src/reactive/array.d.ts +0 -48
  106. package/dist/src/reactive/array.d.ts.map +0 -1
  107. package/dist/src/reactive/buffer.d.ts +0 -120
  108. package/dist/src/reactive/buffer.d.ts.map +0 -1
  109. package/dist/src/reactive/change.d.ts +0 -29
  110. package/dist/src/reactive/change.d.ts.map +0 -1
  111. package/dist/src/reactive/deep-touch.d.ts +0 -28
  112. package/dist/src/reactive/deep-touch.d.ts.map +0 -1
  113. package/dist/src/reactive/deep-watch-state.d.ts +0 -25
  114. package/dist/src/reactive/deep-watch-state.d.ts.map +0 -1
  115. package/dist/src/reactive/deep-watch.d.ts +0 -20
  116. package/dist/src/reactive/deep-watch.d.ts.map +0 -1
  117. package/dist/src/reactive/describe.d.ts +0 -12
  118. package/dist/src/reactive/describe.d.ts.map +0 -1
  119. package/dist/src/reactive/effect-context.d.ts +0 -34
  120. package/dist/src/reactive/effect-context.d.ts.map +0 -1
  121. package/dist/src/reactive/effects.d.ts +0 -164
  122. package/dist/src/reactive/effects.d.ts.map +0 -1
  123. package/dist/src/reactive/index.d.ts +0 -19
  124. package/dist/src/reactive/index.d.ts.map +0 -1
  125. package/dist/src/reactive/map.d.ts +0 -28
  126. package/dist/src/reactive/map.d.ts.map +0 -1
  127. package/dist/src/reactive/memoize.d.ts +0 -28
  128. package/dist/src/reactive/memoize.d.ts.map +0 -1
  129. package/dist/src/reactive/non-reactive-state.d.ts +0 -9
  130. package/dist/src/reactive/non-reactive-state.d.ts.map +0 -1
  131. package/dist/src/reactive/non-reactive.d.ts +0 -11
  132. package/dist/src/reactive/non-reactive.d.ts.map +0 -1
  133. package/dist/src/reactive/project.d.ts +0 -40
  134. package/dist/src/reactive/project.d.ts.map +0 -1
  135. package/dist/src/reactive/proxy-state.d.ts +0 -8
  136. package/dist/src/reactive/proxy-state.d.ts.map +0 -1
  137. package/dist/src/reactive/proxy.d.ts +0 -23
  138. package/dist/src/reactive/proxy.d.ts.map +0 -1
  139. package/dist/src/reactive/record.d.ts +0 -115
  140. package/dist/src/reactive/record.d.ts.map +0 -1
  141. package/dist/src/reactive/register.d.ts +0 -125
  142. package/dist/src/reactive/register.d.ts.map +0 -1
  143. package/dist/src/reactive/registry.d.ts +0 -21
  144. package/dist/src/reactive/registry.d.ts.map +0 -1
  145. package/dist/src/reactive/set.d.ts +0 -26
  146. package/dist/src/reactive/set.d.ts.map +0 -1
  147. package/dist/src/reactive/tracking.d.ts +0 -7
  148. package/dist/src/reactive/tracking.d.ts.map +0 -1
  149. package/dist/src/reactive/types.d.ts +0 -424
  150. package/dist/src/reactive/types.d.ts.map +0 -1
  151. package/dist/src/reactive/watch.d.ts +0 -48
  152. package/dist/src/reactive/watch.d.ts.map +0 -1
  153. package/dist/src/std-decorators.d.ts +0 -45
  154. package/dist/src/std-decorators.d.ts.map +0 -1
  155. package/dist/src/utils.d.ts +0 -49
  156. package/dist/src/utils.d.ts.map +0 -1
  157. package/dist/src/zone.d.ts +0 -40
  158. package/dist/src/zone.d.ts.map +0 -1
  159. package/docs/reactive/describe.md +0 -85
  160. package/docs/reactive/project.md +0 -93
  161. package/src/async/browser.ts +0 -323
  162. package/src/async/index.ts +0 -27
  163. package/src/async/node.ts +0 -92
  164. package/src/decorator.ts +0 -272
  165. package/src/destroyable.ts +0 -199
  166. package/src/entry-browser.ts +0 -5
  167. package/src/entry-node.ts +0 -5
  168. package/src/eventful.ts +0 -110
  169. package/src/flavored.ts +0 -106
  170. package/src/index.d.ts +0 -12
  171. package/src/index.ts +0 -64
  172. package/src/indexable.ts +0 -526
  173. package/src/introspection.ts +0 -59
  174. package/src/iterableWeak.ts +0 -233
  175. package/src/mixins.ts +0 -123
  176. package/src/promiseChain.ts +0 -110
  177. package/src/reactive/array.ts +0 -500
  178. package/src/reactive/buffer.ts +0 -328
  179. package/src/reactive/change.ts +0 -131
  180. package/src/reactive/deep-touch.ts +0 -273
  181. package/src/reactive/deep-watch-state.ts +0 -82
  182. package/src/reactive/deep-watch.ts +0 -171
  183. package/src/reactive/describe.ts +0 -39
  184. package/src/reactive/effect-context.ts +0 -83
  185. package/src/reactive/effects.ts +0 -1434
  186. package/src/reactive/index.ts +0 -72
  187. package/src/reactive/map.ts +0 -142
  188. package/src/reactive/memoize.ts +0 -186
  189. package/src/reactive/non-reactive-state.ts +0 -49
  190. package/src/reactive/non-reactive.ts +0 -43
  191. package/src/reactive/project.md +0 -107
  192. package/src/reactive/project.ts +0 -430
  193. package/src/reactive/proxy-state.ts +0 -27
  194. package/src/reactive/proxy.ts +0 -282
  195. package/src/reactive/record.ts +0 -181
  196. package/src/reactive/register.ts +0 -538
  197. package/src/reactive/registry.ts +0 -72
  198. package/src/reactive/set.ts +0 -117
  199. package/src/reactive/tracking.ts +0 -41
  200. package/src/reactive/types.ts +0 -520
  201. package/src/reactive/watch.ts +0 -180
  202. package/src/std-decorators.ts +0 -256
  203. package/src/utils.ts +0 -300
  204. package/src/zone.ts +0 -142
@@ -40,15 +40,40 @@ You can control how cycles are handled via `reactiveOptions.cycleHandling`:
40
40
  - **`'debug'`**: Full diagnostic mode with transitive closures and topological sorting. Provides detailed cycle path reporting.
41
41
 
42
42
  ### Topological vs. Flat Mode Detection
43
+ - **Detection**: Cycles are detected when the execution depth exceeds `maxEffectChain` (default 100).
44
+ - **Diagnostics**: The resulting `ReactiveError` includes a `trace` property (the recent execution sequence) and attempts to identify a repeating `cycle`.
45
+ - **Recommendation**: Use this mode only for production to minimize performance overhead.
43
46
 
44
- | Mode | `cycleHandling` | Detection Method | Error Code |
45
- | :--- | :--- | :--- | :--- |
46
- | **Debug** | `'debug'` or `'development'` | **Mathematical**: Analyzes the dependency graph. | `CYCLE_DETECTED` |
47
- | **Production** | `'production'` (Default) | **Heuristic**: Counts executions per batch. | `MAX_REACTION_EXCEEDED` |
47
+ ### Cycle Handling Modes
48
48
 
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.
49
+ You can configure how the system handles cycles via `reactiveOptions.cycleHandling`:
50
50
 
51
- ## Memoization Discrepancy Detection
51
+ | Mode | Detection Timing | Cycle Information | Performance |
52
+ |------|-----------------|-------------------|-------------|
53
+ | `'production'` | Late (Heuristic) | Trace of last N effects | Fastest |
54
+ | `'development'` | Eager (On edge) | Exact path (DFS) | Moderate |
55
+ | `'debug'` | Structural | Transitive closures | Slowest |
56
+
57
+ #### Finding Cycle Information
58
+
59
+ When a cycle is detected, a `ReactiveError` is thrown. You can find detailed path information in the error object:
60
+
61
+ ```typescript
62
+ try {
63
+ atom(() => { /* cycle logic */ });
64
+ } catch (error) {
65
+ if (error.code === 'Cycle detected') {
66
+ console.log('Cycle Path:', error.cycle.join(' → '));
67
+ console.log('Trigger Chain:', error.causalChain);
68
+ }
69
+ }
70
+ ```
71
+
72
+ - **`error.cycle`**: An array of effect names forming the cycle.
73
+ - **`error.causalChain`**: The sequence of triggers that led to the current effect.
74
+ - **`error.lineage`**: The creation stack of the effect (available in `development` or `debug` modes).
75
+
76
+ ### Memoization Discrepancy Detection
52
77
 
53
78
  The most powerful debugging tool in `mutts` is the **Discrepancy Detector**. It helps identify "missing dependencies"—reactive values used inside a computation that the system isn't tracking.
54
79
 
@@ -125,10 +150,12 @@ When the reactive system encounters a critical failure (like a cycle or max dept
125
150
  ### `ReactiveErrorCode`
126
151
 
127
152
  Always check `error.debugInfo.code` to identify the failure type:
128
- - `CYCLE_DETECTED`: A circular dependency was found.
129
- - `MAX_DEPTH_EXCEEDED`: The synchronous effect chain reached `maxEffectChain`.
130
- - `MAX_REACTION_EXCEEDED`: An effect was triggered too many times in a single batch.
131
- - `WRITE_IN_COMPUTED`: An attempt was made to modify reactive state inside a `memoize` or `derived` function.
153
+ - `Cycle detected`: A circular dependency was found.
154
+ - `Max depth exceeded`: The synchronous effect chain reached `maxEffectChain`.
155
+ - `Max reaction exceeded`: An effect was triggered too many times in a single batch.
156
+ - `Write in computed`: An attempt was made to modify reactive state inside a `memoize` function.
157
+ - `Tracking error`: Internal inconsistency detected in the active dependency stack.
158
+ - `Broken effects`: The system has entered an unrecoverable "broken" state after a root-level panic. Call `reset()` to recover.
132
159
 
133
160
  ### Rich Debug Info
134
161
 
@@ -136,6 +163,7 @@ The `debugInfo` property on `ReactiveError` includes:
136
163
  - **`causalChain`**: A string array describing the logical path of modifications leading to the error.
137
164
  - **`creationStack`**: The stack trace of where the effect was originally created, helping you locate the source in your code.
138
165
  - **`cycle`**: (For `CYCLE_DETECTED`) The names of the effects that form the loop.
166
+ - **`lineage`**: Detailed source-to-sink dependency traces for debugging (requires `lineages` introspection).
139
167
 
140
168
  ## Best Practices for Debugging
141
169
 
@@ -147,8 +175,15 @@ Always provide a name for your effects to make debug logs and error messages rea
147
175
  effect(() => {
148
176
  // ...
149
177
  }, { name: 'UpdateSidebarCounter' });
178
+
179
+ // Or
180
+
181
+ effect.named('UpdateSidebarCounter')(() => {
182
+ // ...
183
+ });
150
184
  ```
151
185
 
186
+
152
187
  ### Activation & Deactivation
153
188
 
154
189
  Since these are runtime options, you can toggle them based on your environment:
@@ -164,3 +199,75 @@ if (process.env.NODE_ENV === 'development') {
164
199
  reactiveOptions.cycleHandling = 'production';
165
200
  }
166
201
  ```
202
+
203
+ ## Advanced Debugging (`mutts/debug`)
204
+
205
+ For a deeper look into the reactivity graph and execution flow, you can import the `mutts/debug` module. Simply importing it enables several background tracking features.
206
+
207
+ ```typescript
208
+ import 'mutts/debug';
209
+ ```
210
+
211
+ ### Reaction Reasons (`CleanupReason`)
212
+
213
+ When an effect or watcher re-runs, it receives a `reaction` property (in `EffectAccess`) that describes *why* it was triggered. This is also passed to the `cleanup` function.
214
+
215
+ ```typescript
216
+ effect(({ reaction }) => {
217
+ if (reaction && typeof reaction === 'object') {
218
+ if (reaction.type === 'propChange') {
219
+ console.log('Triggered by:', reaction.triggers.map(t => t.evolution.prop));
220
+ }
221
+ }
222
+
223
+ return (reason) => {
224
+ // reason is also a CleanupReason
225
+ if (reason?.type === 'stopped') console.log('Effect manually stopped');
226
+ };
227
+ });
228
+ ```
229
+
230
+ #### `formatCleanupReason`
231
+
232
+ A built-in utility to turn a `CleanupReason` into an inspectable console log array.
233
+
234
+ ```typescript
235
+ import { formatCleanupReason } from 'mutts';
236
+
237
+ effect(()=> {
238
+ ...
239
+ return (reason)=> {
240
+ if (reason) console.log(...formatCleanupReason(reason));
241
+ }
242
+ });
243
+ ```
244
+
245
+ ### Lineage Tracking
246
+
247
+ Lineage tracking allows you to see the "causal path" of an effect—not just the current stack trace, but the stack traces of all parent effects that created the current execution.
248
+
249
+ When an effect is created, it is assigned a `lineage` property that contains the stack traces of all parent effects that lead to its creation.
250
+
251
+ - **`logLineage()`**: Prints a formatted, interactive tree of the current effect's lineage to the console.
252
+ - **`captureLineage()`**: Captures the current lineage as a structured object.
253
+
254
+ #### Lineage Options
255
+
256
+ You can control how lineages are captured via `reactiveOptions.introspection.gatherReasons.lineages`:
257
+ - `'none'`: Disable lineage capture.
258
+ - `'touch'`: Capture lineage when a property is accessed (default).
259
+ - `'dependency'`: Capture lineage when a dependency is recorded.
260
+ - `'both'`: Capture both.
261
+
262
+ ### The `__MUTTS_DEBUG__` Global
263
+
264
+ When `mutts/debug` is active (or after calling `enableDevTools()`), a global `__MUTTS_DEBUG__` object is exposed in the environment (Node.js `global` or Browser `window`).
265
+
266
+ This object provides low-level access to the graph, lineage capture, and renaming utilities:
267
+ - `__MUTTS_DEBUG__.getGraph()`: Returns the full reactivity graph.
268
+ - `__MUTTS_DEBUG__.logLineage()`: logs the current lineage.
269
+ - `__MUTTS_DEBUG__.browserLineage`: captures lineage for the DevTools panel.
270
+
271
+ ### Custom DevTools Formatters
272
+
273
+ `mutts/debug` automatically registers [Custom Formatters](https://bit.ly/chrome-extension-custom-formatters) in Chrome. This makes lineage objects and reactive proxies appear as clean, structured trees in the console instead of opaque Proxy objects.
@@ -1,18 +1,18 @@
1
1
  # Effect Error Handling
2
2
 
3
- The `onEffectThrow` function allows you to catch and handle errors within reactive effects.
3
+ The `caught` function allows you to catch and handle errors within reactive effects.
4
4
 
5
5
  ## Basic Usage
6
6
 
7
- Register an error handler inside an effect using `onEffectThrow`:
7
+ Register an error handler inside an effect using `caught`:
8
8
 
9
9
  ```typescript
10
- import { effect, onEffectThrow, reactive } from 'mutts'
10
+ import { effect, caught, reactive } from 'mutts'
11
11
 
12
12
  const state = reactive({ value: 0 })
13
13
 
14
14
  effect(() => {
15
- onEffectThrow((error) => {
15
+ caught((error) => {
16
16
  console.error('Effect failed:', error)
17
17
  })
18
18
 
@@ -28,7 +28,7 @@ You can register multiple handlers. They are tried in order until one succeeds:
28
28
  ```typescript
29
29
  effect(() => {
30
30
  // First handler - try to recover
31
- onEffectThrow((error) => {
31
+ caught((error) => {
32
32
  if (error.message === 'Retryable') {
33
33
  retryOperation()
34
34
  return // Success - stops here
@@ -37,7 +37,7 @@ effect(() => {
37
37
  })
38
38
 
39
39
  // Second handler - log and continue
40
- onEffectThrow((error) => {
40
+ caught((error) => {
41
41
  console.log('Operation failed:', error)
42
42
  })
43
43
  })
@@ -50,7 +50,7 @@ Errors in child effects propagate to parent effects:
50
50
  ```typescript
51
51
  effect(() => {
52
52
  // Parent catches child's error
53
- onEffectThrow((error) => {
53
+ caught((error) => {
54
54
  console.log('Child failed:', error.message)
55
55
  })
56
56
 
@@ -69,7 +69,7 @@ Handlers can return cleanup functions:
69
69
 
70
70
  ```typescript
71
71
  effect(() => {
72
- onEffectThrow((error) => {
72
+ caught((error) => {
73
73
  console.log('Handling error:', error)
74
74
 
75
75
  return () => {
@@ -82,7 +82,7 @@ effect(() => {
82
82
 
83
83
  ## API
84
84
 
85
- ### `onEffectThrow(handler)`
85
+ ### `caught(handler)`
86
86
 
87
87
  Registers an error handler for the current effect.
88
88
 
@@ -118,4 +118,4 @@ Parent's handlers try
118
118
 
119
119
  - Handlers must be registered **before** the code that might throw
120
120
  - Handlers are cleared on each effect re-run (re-register if needed)
121
- - Errors in async effects (Promises) are not caught by `onEffectThrow` - use `.catch()` on the Promise
121
+ - Errors in async effects (Promises) are not caught by `caught` - use `.catch()` on the Promise
@@ -0,0 +1,125 @@
1
+ # Reactive Resource (`resource`)
2
+
3
+ The `resource` utility creates a reactive object that automatically tracks the state of an asynchronous operation (loading, value, error). It is designed to simplify data fetching and async state management in a reactive environment.
4
+
5
+ ## Overview
6
+
7
+ `resource` wraps an async function (the "fetcher") and returns a reactive object that:
8
+ - **Tracks dependencies**: Automatically re-runs the fetcher when reactive dependencies accessed within it change.
9
+ - **Manages state**: Provides `loading`, `value`, `error`, and `latest` properties that update automatically.
10
+ - **Handles race conditions**: Ensures that only the result of the latest fetch is applied, discarding results from stale requests.
11
+ - **Supports manual reload**: Exposes a `reload()` method to force a refresh.
12
+
13
+ ## API
14
+
15
+ ```typescript
16
+ function resource<T>(
17
+ fetcher: (dep: EffectAccess) => Promise<T> | T,
18
+ options?: { initialValue?: T }
19
+ ): Resource<T>
20
+
21
+ interface Resource<T> {
22
+ value: T | undefined // The current successful value
23
+ loading: boolean // True if a fetch is in progress
24
+ error: any // Error from the last failed fetch
25
+ latest: T | undefined // The latest successful value (preserved during loading)
26
+ reload: () => void // Function to manually trigger a re-fetch
27
+ }
28
+ ```
29
+
30
+ ### Parameters
31
+
32
+ - **`fetcher`**: A function that returns a value `T` or a `Promise<T>`. It receives an `EffectAccess` object, allowing for dependency tracking control (e.g., `dep.tracked()`). Reactive properties accessed synchronously or within tracked scopes are dependencies.
33
+ - **`options`**: Optional configuration object.
34
+ - `initialValue`: Initial value for `value` and `latest` before the first fetch completes (or if the first fetch is async).
35
+
36
+ ### Returns
37
+
38
+ A reactive object implementing the `Resource<T>` interface.
39
+
40
+ ## Usage
41
+
42
+ ### Basic Async Fetch
43
+
44
+ ```typescript
45
+ import { reactive, resource } from 'mutts/reactive'
46
+
47
+ // ID usually comes from another reactive source
48
+ const state = reactive({ userId: 1 })
49
+
50
+ // Create a resource that fetches user data based on state.userId
51
+ const user = resource(async () => {
52
+ const response = await fetch(`/api/users/${state.userId}`)
53
+ return response.json()
54
+ })
55
+
56
+ // Use the resource in your view
57
+ effect(() => {
58
+ if (user.loading) {
59
+ console.log('Loading...')
60
+ } else if (user.error) {
61
+ console.error('Error:', user.error)
62
+ } else {
63
+ console.log('User:', user.value)
64
+ }
65
+ })
66
+
67
+ // Changing state.userId automatically triggers a new fetch
68
+ state.userId = 2
69
+ ```
70
+
71
+ ### Handling Race Conditions
72
+
73
+ `resource` automatically handles race conditions. If `state.userId` changes quickly from 1 to 2, and the request for user 1 takes longer than user 2, the result for user 1 will be ignored when it arrives, preventing inconsistent state.
74
+
75
+ ### Using `latest` for Smooth Transitions
76
+
77
+ The `value` property becomes `undefined` (or `initialValue`) when a new fetch starts if the previous value is not preserved (current implementation resets `value` only on success? No, `value` is kept from previous success? - *Clarification: In the implementation, `value` is NOT reset to undefined on new fetch start, it retains the old value until new value arrives, unless `initialValue` was used? Let's check implementation behavior.*)
78
+
79
+ *Correction based on implementation:*
80
+ The current implementation:
81
+ ```typescript
82
+ state.loading = true
83
+ state.error = undefined
84
+ // ... fetcher executes ...
85
+ // on success:
86
+ state.value = val
87
+ state.latest = val
88
+ state.loading = false
89
+ ```
90
+ It does **NOT** reset `value` to `undefined` when a new fetch starts. So `value` acts like `latest`. The distinction is primarily semantic or for future behavior where `value` might be reset. Currently `value` and `latest` behave similarly regarding preservation of old data during loading.
91
+
92
+ ```typescript
93
+ effect(() => {
94
+ // Show old data while loading new data
95
+ if (user.loading) {
96
+ console.log('Reloading... showing cached version:', user.latest)
97
+ } else {
98
+ console.log('Current:', user.value)
99
+ }
100
+ })
101
+ ```
102
+
103
+ ### Manual Reload
104
+
105
+ You can trigger a re-fetch without changing dependencies:
106
+
107
+ ```typescript
108
+ // Refresh the data (e.g., user clicked a "Refresh" button)
109
+ user.reload()
110
+ ```
111
+
112
+ ### Synchronous Resources
113
+
114
+ `resource` can also wrap synchronous calculations, essentially behaving like a computed property but with the `Resource` interface structure.
115
+
116
+ ```typescript
117
+ const count = reactive({ value: 1 })
118
+
119
+ const doubled = resource(() => {
120
+ return count.value * 2
121
+ })
122
+
123
+ console.log(doubled.value) // 2
124
+ console.log(doubled.loading) // false
125
+ ```
@@ -42,7 +42,7 @@ The implementation consists of:
42
42
  2. **Intermediates**: Class instances that link `val` and `prev`. They expose an `acc` getter decorated with `@memoize`.
43
43
  3. **Index Sync Effects**: Granular effects (one per result index) that subscribe to `indexToIntermediate[i].acc`.
44
44
 
45
- This "Project-like" architecture ensures that the main loop only does structural work, while the actual logic propagation is handled by the dependency chain of the intermediates.
45
+ This "Morph-like" architecture ensures that the main loop only does structural work, while the actual logic propagation is handled by the dependency chain of the intermediates.
46
46
 
47
47
  ## API Reference
48
48
 
@@ -276,6 +276,37 @@ showAdvanced.value = false
276
276
  | **Use Case** | Derived arrays/objects (map, filter, computed properties) | Cumulative operations (sum, reduce) |
277
277
  | **Data Types** | Arrays and objects | Arrays only (object items required) |
278
278
 
279
+ ## Comparison with Recursive Touching (Deep Touch)
280
+
281
+ When you assign a new array/object to a reactive property (`state.items = newArray`), the reactive system performs a **recursive touch** — it diffs old vs new element-by-element and fires per-index notifications on the *same proxy*. This raises the question: is `lift` redundant?
282
+
283
+ | | Recursive Touching | `lift` |
284
+ | :--- | :--- | :--- |
285
+ | **Trigger** | Direct assignment to a reactive property | Any reactive dependency change inside the callback |
286
+ | **Scope** | Same-shape replacement of one value | Arbitrary computation → stable reactive output |
287
+ | **Identity** | Same proxy, same object | Returns a **new persistent proxy** that outlives re-evaluations |
288
+ | **Use case** | `state.user = fetchedUser` — fine-grained diff on assignment | `lift(() => items.filter(x => x.active))` — derived collection |
289
+
290
+ Deep touching makes `lift` unnecessary for **replacement** patterns (`state.items = newItems`). `lift` remains essential for **derived collections** where the result is a transformation (filter, map, reshape) rather than a direct assignment — there is no single property to assign to, and the whole output is recomputed from scratch each time.
291
+
292
+ ## Comparison with `memoize`
293
+
294
+ Both `lift` and `memoize` compute derived values from reactive dependencies, but they differ in evaluation strategy and output type.
295
+
296
+ | | `memoize` | `lift` |
297
+ | :--- | :--- | :--- |
298
+ | **Evaluation** | Lazy — invalidates on dep change, recomputes on next read | Eager — recomputes immediately on dep change |
299
+ | **Return type** | The raw return value of the function | A **stable reactive proxy** (array or object) |
300
+ | **Downstream reactivity** | Consumers get a new value each time (identity changes) | Consumers see per-property/per-index diffs on the *same* proxy |
301
+ | **Arguments** | Keyed by object args (WeakMap cache tree) | No args — closure over reactive deps |
302
+ | **Decorator** | Yes (`@memoize` on getters/methods) | No |
303
+ | **Cleanup** | Automatic (WeakMap GC) | Explicit `result[cleanup]()` |
304
+
305
+ **When to use which:**
306
+ - **`lift`** for derived collections where downstream consumers (e.g., `morph()`, effects) benefit from per-element diffing on a stable proxy.
307
+ - **`memoize`** for parameterized caching (`memoize((user) => expensiveCompute(user))`) or lazy evaluation where recomputation should only happen on access.
308
+ - For a scalar result read in one place, they are nearly interchangeable — prefer `memoize` for its laziness and automatic cleanup.
309
+
279
310
  ## Performance Considerations
280
311
 
281
312
  ### Arrays
package/docs/reactive.md CHANGED
@@ -11,12 +11,13 @@ The Mutts Reactive System documentation has been split into focused sections for
11
11
  * **[Reactive Collections](./reactive/collections.md#collections)**: Map, Set, WeakMap, WeakSet
12
12
  * **[Reactive Arrays](./reactive/collections.md#reactivearray)**: Full array method support
13
13
  * **[Register](./reactive/collections.md#register)**: ID-keyed ordered collections
14
- * **[Projections](./reactive/collections.md#projection)**: `project`, `organized`
14
+ * **[Morphing](./reactive/collections.md#morph)**: `morph`, `organized`
15
15
  * **[Attend](./reactive/attend.md)**: Reactive enumeration (`attend`)
16
- * **[Describe](./reactive/describe.md)**: Reactive property definition (`describe`)
17
16
  * **[Scan](./reactive/scan.md)**: Reactive scan and accumulation
17
+ * **[Resource](./reactive/resource.md)**: Async state tracking (`resource`)
18
18
 
19
19
  ## [Advanced Topics](./reactive/advanced.md)
20
+ * **[Choosing the Right Primitive](./reactive/advanced.md#choosing-the-right-reactive-primitive)**: Comparison table of effect-value functions (memoize, lift, project, scan, etc.)
20
21
  * **[Atomic Operations](./reactive/advanced.md#atomic-operations)**: Batching and Bidirectional binding
21
22
  * **[Evolution Tracking](./reactive/advanced.md#evolution-tracking)**: History introspection
22
23
  * **[Prototype Chains](./reactive/advanced.md#prototype-chains-and-pure-objects)**: Advanced inheritance patterns
package/docs/utils.md ADDED
@@ -0,0 +1,70 @@
1
+ # Utilities
2
+
3
+ Mutts provides a collection of lightweight, high-performance utility functions. These are used extensively within the reactive engine but are also exported for general application logic.
4
+
5
+ ## Collection Utilities
6
+
7
+ ### `zip(...arrays)`
8
+
9
+ A generator that yields tuples containing elements from each input array. It continues until the **longest** array is exhausted (returning `undefined` for shorter arrays).
10
+
11
+ ```typescript
12
+ import { zip } from 'mutts';
13
+
14
+ const names = ['Alice', 'Bob'];
15
+ const scores = [100, 95, 80];
16
+
17
+ for (const [name, score] of zip(names, scores)) {
18
+ console.log(`${name}: ${score}`);
19
+ }
20
+ // Alice: 100
21
+ // Bob: 95
22
+ // undefined: 80
23
+ ```
24
+
25
+ > [!NOTE]
26
+ > `zip` is implemented as a generator for memory efficiency. If you need a plain array, spread the result: `[...zip(a, b)]`.
27
+
28
+
29
+ ### `deepCompare(a, b)`
30
+
31
+ A robust deep comparison utility that handles circular references and various built-in types.
32
+
33
+ - **Supported Types**: Objects, Arrays, `Set`, `Map`, `Date`, `RegExp`.
34
+ - **Circular References**: Safely handled via internal tracking.
35
+ - **Prototypes**: Objects must have matching prototypes to be considered equal.
36
+
37
+ ```typescript
38
+ import { deepCompare } from 'mutts';
39
+
40
+ const obj1 = { date: new Date(0), map: new Map([['a', 1]]) };
41
+ const obj2 = { date: new Date(0), map: new Map([['a', 1]]) };
42
+
43
+ deepCompare(obj1, obj2); // true
44
+ ```
45
+
46
+ ## Type Reflection
47
+
48
+ ### `isConstructor(fn)` / `isObject(value)`
49
+
50
+ Utilities for robust type checking without the pitfalls of `typeof`.
51
+
52
+ - `isConstructor`: Returns `true` if the function is a `class` or a native constructor (like `Array`).
53
+ - `isObject`: Returns `true` for plain objects. Returns `false` for `null`, `Array`, `Date`, `Map`, etc.
54
+
55
+ ## Debugging & Metadata
56
+
57
+ ### `tag(name, obj)`
58
+
59
+ Applies a debugging "tag" to an object. It sets `Symbol.toStringTag` and overrides `toString()` so the object appears clearly in logs and DevTools.
60
+
61
+ ### `named(name, fn)`
62
+
63
+ Renames a function for better stack traces. If the function already has a name, it appends the new name using `::` as a separator (e.g., `original::new`).
64
+
65
+ ```typescript
66
+ const myFn = named('Enhanced', () => {});
67
+ console.log(myFn.name); // "Enhanced"
68
+ ```
69
+
70
+ ---
package/package.json CHANGED
@@ -1,56 +1,86 @@
1
1
  {
2
2
  "name": "mutts",
3
3
  "description": "Modern UTility TS: A collection of TypeScript utilities",
4
- "version": "1.0.9",
4
+ "version": "1.0.10",
5
5
  "main": "dist/browser.cjs",
6
6
  "module": "dist/browser.esm.js",
7
- "types": "dist/browser.d.ts",
7
+ "types": "./dist/index.d.ts",
8
8
  "exports": {
9
9
  ".": {
10
- "test-node": {
11
- "import": "./src/entry-node.ts"
12
- },
13
- "test-browser": {
14
- "import": "./src/entry-browser.ts"
15
- },
16
10
  "node": {
17
11
  "types": "./dist/node.d.ts",
18
- "import": "./dist/node.esm.js",
19
- "require": "./dist/node.cjs"
12
+ "development": {
13
+ "import": "./dist/node.dev.esm.js",
14
+ "require": "./dist/node.dev.cjs"
15
+ },
16
+ "default": {
17
+ "import": "./dist/node.esm.js",
18
+ "require": "./dist/node.cjs"
19
+ }
20
20
  },
21
21
  "default": {
22
22
  "types": "./dist/browser.d.ts",
23
- "import": "./dist/browser.esm.js",
24
- "require": "./dist/browser.cjs"
23
+ "development": {
24
+ "import": "./dist/browser.dev.esm.js",
25
+ "require": "./dist/browser.dev.cjs"
26
+ },
27
+ "default": {
28
+ "import": "./dist/browser.esm.js",
29
+ "require": "./dist/browser.cjs"
30
+ }
25
31
  }
26
32
  },
27
33
  "./browser": {
28
34
  "types": "./dist/browser.d.ts",
29
- "import": "./dist/browser.esm.js",
30
- "require": "./dist/browser.cjs"
35
+ "development": {
36
+ "import": "./dist/browser.dev.esm.js",
37
+ "require": "./dist/browser.dev.cjs"
38
+ },
39
+ "default": {
40
+ "import": "./dist/browser.esm.js",
41
+ "require": "./dist/browser.cjs"
42
+ }
31
43
  },
32
44
  "./node": {
33
45
  "types": "./dist/node.d.ts",
34
- "import": "./dist/node.esm.js",
35
- "require": "./dist/node.cjs"
36
- },
37
- "./src": {
38
- "node": {
39
- "types": "./src/entry-node.ts",
40
- "import": "./src/entry-node.ts"
46
+ "development": {
47
+ "import": "./dist/node.dev.esm.js",
48
+ "require": "./dist/node.dev.cjs"
41
49
  },
42
50
  "default": {
43
- "types": "./src/entry-browser.ts",
44
- "import": "./src/entry-browser.ts"
51
+ "import": "./dist/node.esm.js",
52
+ "require": "./dist/node.cjs"
45
53
  }
46
54
  },
47
- "./src/browser": {
48
- "types": "./src/entry-browser.ts",
49
- "import": "./src/entry-browser.ts"
55
+ "./dev": {
56
+ "types": "./dist/browser.d.ts",
57
+ "import": "./dist/browser.dev.esm.js",
58
+ "require": "./dist/browser.dev.cjs"
50
59
  },
51
- "./src/node": {
52
- "types": "./src/entry-node.ts",
53
- "import": "./src/entry-node.ts"
60
+ "./prod": {
61
+ "types": "./dist/browser.d.ts",
62
+ "import": "./dist/browser.esm.js",
63
+ "require": "./dist/browser.cjs"
64
+ },
65
+ "./browser/dev": {
66
+ "types": "./dist/browser.d.ts",
67
+ "import": "./dist/browser.dev.esm.js",
68
+ "require": "./dist/browser.dev.cjs"
69
+ },
70
+ "./browser/prod": {
71
+ "types": "./dist/browser.d.ts",
72
+ "import": "./dist/browser.esm.js",
73
+ "require": "./dist/browser.cjs"
74
+ },
75
+ "./node/dev": {
76
+ "types": "./dist/node.d.ts",
77
+ "import": "./dist/node.dev.esm.js",
78
+ "require": "./dist/node.dev.cjs"
79
+ },
80
+ "./node/prod": {
81
+ "types": "./dist/node.d.ts",
82
+ "import": "./dist/node.esm.js",
83
+ "require": "./dist/node.cjs"
54
84
  },
55
85
  "./debug": {
56
86
  "types": "./dist/debug.d.ts",
@@ -60,7 +90,6 @@
60
90
  },
61
91
  "files": [
62
92
  "dist",
63
- "src",
64
93
  "README.md",
65
94
  "docs"
66
95
  ],