mutts 1.0.9 → 1.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/README.md +60 -35
  2. package/dist/browser.cjs +1333 -1760
  3. package/dist/browser.cjs.map +1 -1
  4. package/dist/browser.d.ts +2 -1392
  5. package/dist/browser.dev.cjs +114 -0
  6. package/dist/browser.dev.cjs.map +1 -0
  7. package/dist/browser.dev.d.ts +2 -0
  8. package/dist/browser.dev.esm.js +5 -0
  9. package/dist/browser.dev.esm.js.map +1 -0
  10. package/dist/browser.esm.js +18 -97
  11. package/dist/browser.esm.js.map +1 -1
  12. package/dist/chunks/{async-browser-lvzLOCgk.cjs → async-browser-Dgr5CreQ.cjs} +16 -95
  13. package/dist/chunks/async-browser-Dgr5CreQ.cjs.map +1 -0
  14. package/dist/chunks/{async-node-C3DeIb0y.cjs → async-node-3PrbVAbB.cjs} +3 -1
  15. package/dist/chunks/async-node-3PrbVAbB.cjs.map +1 -0
  16. package/dist/chunks/index-Sf74wXTV.esm.js +2577 -0
  17. package/dist/chunks/index-Sf74wXTV.esm.js.map +1 -0
  18. package/dist/chunks/node-Bo7WU5S2.esm.js +96 -0
  19. package/dist/chunks/node-Bo7WU5S2.esm.js.map +1 -0
  20. package/dist/chunks/{index-VTO-b2vR.cjs → proxy-Cc79Lrzj.cjs} +2593 -3216
  21. package/dist/chunks/proxy-Cc79Lrzj.cjs.map +1 -0
  22. package/dist/chunks/{index-CtA2AWl3.esm.js → proxy-D2C49sXH.esm.js} +2570 -3174
  23. package/dist/chunks/proxy-D2C49sXH.esm.js.map +1 -0
  24. package/dist/debug.cjs +987 -28
  25. package/dist/debug.cjs.map +1 -1
  26. package/dist/debug.d.ts +37 -38
  27. package/dist/debug.esm.js +963 -1
  28. package/dist/debug.esm.js.map +1 -1
  29. package/dist/devtools/manifest.json +1 -1
  30. package/dist/devtools/panel.html +1 -1
  31. package/dist/devtools/panel.js +107 -94
  32. package/dist/devtools/panel.js.map +1 -1
  33. package/dist/index.d.ts +1322 -1
  34. package/dist/mutts.umd.js +6647 -1
  35. package/dist/mutts.umd.js.map +1 -1
  36. package/dist/mutts.umd.min.js +1 -1
  37. package/dist/mutts.umd.min.js.map +1 -1
  38. package/dist/node.cjs +69 -63
  39. package/dist/node.cjs.map +1 -1
  40. package/dist/node.d.ts +2 -2
  41. package/dist/node.dev.cjs +114 -0
  42. package/dist/node.dev.cjs.map +1 -0
  43. package/dist/node.dev.d.ts +2 -0
  44. package/dist/node.dev.esm.js +6 -0
  45. package/dist/node.dev.esm.js.map +1 -0
  46. package/dist/node.esm.js +4 -97
  47. package/dist/node.esm.js.map +1 -1
  48. package/dist/{types-DaHFfhlN.d.ts → types-Bx2PhORg.d.ts} +134 -88
  49. package/docs/ai/api-reference.md +11 -32
  50. package/docs/ai/manual.md +297 -239
  51. package/docs/reactive/advanced.md +318 -9
  52. package/docs/reactive/attend.md +2 -4
  53. package/docs/reactive/collections.md +22 -187
  54. package/docs/reactive/core.md +223 -131
  55. package/docs/reactive/debugging.md +119 -12
  56. package/docs/reactive/error-handling.md +10 -10
  57. package/docs/reactive/resource.md +125 -0
  58. package/docs/reactive.md +3 -4
  59. package/docs/utils.md +70 -0
  60. package/docs/zone.md +1 -1
  61. package/package.json +76 -38
  62. package/dist/chunks/async-browser-lvzLOCgk.cjs.map +0 -1
  63. package/dist/chunks/async-node-C3DeIb0y.cjs.map +0 -1
  64. package/dist/chunks/index-2vea86wD.esm.js +0 -3011
  65. package/dist/chunks/index-2vea86wD.esm.js.map +0 -1
  66. package/dist/chunks/index-CtA2AWl3.esm.js.map +0 -1
  67. package/dist/chunks/index-VTO-b2vR.cjs.map +0 -1
  68. package/dist/debug/debug.d.ts +0 -122
  69. package/dist/debug/debug.d.ts.map +0 -1
  70. package/dist/debug/index.d.ts +0 -4
  71. package/dist/debug/index.d.ts.map +0 -1
  72. package/dist/debug/lineage-panel.d.ts +0 -5
  73. package/dist/debug/lineage-panel.d.ts.map +0 -1
  74. package/dist/debug/lineage.d.ts +0 -79
  75. package/dist/debug/lineage.d.ts.map +0 -1
  76. package/dist/src/async/browser.d.ts +0 -2
  77. package/dist/src/async/browser.d.ts.map +0 -1
  78. package/dist/src/async/index.d.ts +0 -19
  79. package/dist/src/async/index.d.ts.map +0 -1
  80. package/dist/src/async/node.d.ts +0 -2
  81. package/dist/src/async/node.d.ts.map +0 -1
  82. package/dist/src/decorator.d.ts +0 -106
  83. package/dist/src/decorator.d.ts.map +0 -1
  84. package/dist/src/destroyable.d.ts +0 -87
  85. package/dist/src/destroyable.d.ts.map +0 -1
  86. package/dist/src/entry-browser.d.ts +0 -3
  87. package/dist/src/entry-browser.d.ts.map +0 -1
  88. package/dist/src/entry-node.d.ts +0 -3
  89. package/dist/src/entry-node.d.ts.map +0 -1
  90. package/dist/src/eventful.d.ts +0 -20
  91. package/dist/src/eventful.d.ts.map +0 -1
  92. package/dist/src/flavored.d.ts +0 -33
  93. package/dist/src/flavored.d.ts.map +0 -1
  94. package/dist/src/index.d.ts +0 -14
  95. package/dist/src/index.d.ts.map +0 -1
  96. package/dist/src/indexable.d.ts +0 -243
  97. package/dist/src/indexable.d.ts.map +0 -1
  98. package/dist/src/introspection.d.ts +0 -27
  99. package/dist/src/introspection.d.ts.map +0 -1
  100. package/dist/src/iterableWeak.d.ts +0 -53
  101. package/dist/src/iterableWeak.d.ts.map +0 -1
  102. package/dist/src/mixins.d.ts +0 -25
  103. package/dist/src/mixins.d.ts.map +0 -1
  104. package/dist/src/promiseChain.d.ts +0 -20
  105. package/dist/src/promiseChain.d.ts.map +0 -1
  106. package/dist/src/reactive/array.d.ts +0 -48
  107. package/dist/src/reactive/array.d.ts.map +0 -1
  108. package/dist/src/reactive/buffer.d.ts +0 -120
  109. package/dist/src/reactive/buffer.d.ts.map +0 -1
  110. package/dist/src/reactive/change.d.ts +0 -29
  111. package/dist/src/reactive/change.d.ts.map +0 -1
  112. package/dist/src/reactive/deep-touch.d.ts +0 -28
  113. package/dist/src/reactive/deep-touch.d.ts.map +0 -1
  114. package/dist/src/reactive/deep-watch-state.d.ts +0 -25
  115. package/dist/src/reactive/deep-watch-state.d.ts.map +0 -1
  116. package/dist/src/reactive/deep-watch.d.ts +0 -20
  117. package/dist/src/reactive/deep-watch.d.ts.map +0 -1
  118. package/dist/src/reactive/describe.d.ts +0 -12
  119. package/dist/src/reactive/describe.d.ts.map +0 -1
  120. package/dist/src/reactive/effect-context.d.ts +0 -34
  121. package/dist/src/reactive/effect-context.d.ts.map +0 -1
  122. package/dist/src/reactive/effects.d.ts +0 -164
  123. package/dist/src/reactive/effects.d.ts.map +0 -1
  124. package/dist/src/reactive/index.d.ts +0 -19
  125. package/dist/src/reactive/index.d.ts.map +0 -1
  126. package/dist/src/reactive/map.d.ts +0 -28
  127. package/dist/src/reactive/map.d.ts.map +0 -1
  128. package/dist/src/reactive/memoize.d.ts +0 -28
  129. package/dist/src/reactive/memoize.d.ts.map +0 -1
  130. package/dist/src/reactive/non-reactive-state.d.ts +0 -9
  131. package/dist/src/reactive/non-reactive-state.d.ts.map +0 -1
  132. package/dist/src/reactive/non-reactive.d.ts +0 -11
  133. package/dist/src/reactive/non-reactive.d.ts.map +0 -1
  134. package/dist/src/reactive/project.d.ts +0 -40
  135. package/dist/src/reactive/project.d.ts.map +0 -1
  136. package/dist/src/reactive/proxy-state.d.ts +0 -8
  137. package/dist/src/reactive/proxy-state.d.ts.map +0 -1
  138. package/dist/src/reactive/proxy.d.ts +0 -23
  139. package/dist/src/reactive/proxy.d.ts.map +0 -1
  140. package/dist/src/reactive/record.d.ts +0 -115
  141. package/dist/src/reactive/record.d.ts.map +0 -1
  142. package/dist/src/reactive/register.d.ts +0 -125
  143. package/dist/src/reactive/register.d.ts.map +0 -1
  144. package/dist/src/reactive/registry.d.ts +0 -21
  145. package/dist/src/reactive/registry.d.ts.map +0 -1
  146. package/dist/src/reactive/set.d.ts +0 -26
  147. package/dist/src/reactive/set.d.ts.map +0 -1
  148. package/dist/src/reactive/tracking.d.ts +0 -7
  149. package/dist/src/reactive/tracking.d.ts.map +0 -1
  150. package/dist/src/reactive/types.d.ts +0 -424
  151. package/dist/src/reactive/types.d.ts.map +0 -1
  152. package/dist/src/reactive/watch.d.ts +0 -48
  153. package/dist/src/reactive/watch.d.ts.map +0 -1
  154. package/dist/src/std-decorators.d.ts +0 -45
  155. package/dist/src/std-decorators.d.ts.map +0 -1
  156. package/dist/src/utils.d.ts +0 -49
  157. package/dist/src/utils.d.ts.map +0 -1
  158. package/dist/src/zone.d.ts +0 -40
  159. package/dist/src/zone.d.ts.map +0 -1
  160. package/docs/reactive/describe.md +0 -85
  161. package/docs/reactive/project.md +0 -93
  162. package/docs/reactive/scan.md +0 -293
  163. package/src/async/browser.ts +0 -323
  164. package/src/async/index.ts +0 -27
  165. package/src/async/node.ts +0 -92
  166. package/src/decorator.ts +0 -272
  167. package/src/destroyable.ts +0 -199
  168. package/src/entry-browser.ts +0 -5
  169. package/src/entry-node.ts +0 -5
  170. package/src/eventful.ts +0 -110
  171. package/src/flavored.ts +0 -106
  172. package/src/index.d.ts +0 -12
  173. package/src/index.ts +0 -64
  174. package/src/indexable.ts +0 -526
  175. package/src/introspection.ts +0 -59
  176. package/src/iterableWeak.ts +0 -233
  177. package/src/mixins.ts +0 -123
  178. package/src/promiseChain.ts +0 -110
  179. package/src/reactive/array.ts +0 -500
  180. package/src/reactive/buffer.ts +0 -328
  181. package/src/reactive/change.ts +0 -131
  182. package/src/reactive/deep-touch.ts +0 -273
  183. package/src/reactive/deep-watch-state.ts +0 -82
  184. package/src/reactive/deep-watch.ts +0 -171
  185. package/src/reactive/describe.ts +0 -39
  186. package/src/reactive/effect-context.ts +0 -83
  187. package/src/reactive/effects.ts +0 -1434
  188. package/src/reactive/index.ts +0 -72
  189. package/src/reactive/map.ts +0 -142
  190. package/src/reactive/memoize.ts +0 -186
  191. package/src/reactive/non-reactive-state.ts +0 -49
  192. package/src/reactive/non-reactive.ts +0 -43
  193. package/src/reactive/project.md +0 -107
  194. package/src/reactive/project.ts +0 -430
  195. package/src/reactive/proxy-state.ts +0 -27
  196. package/src/reactive/proxy.ts +0 -282
  197. package/src/reactive/record.ts +0 -181
  198. package/src/reactive/register.ts +0 -538
  199. package/src/reactive/registry.ts +0 -72
  200. package/src/reactive/set.ts +0 -117
  201. package/src/reactive/tracking.ts +0 -41
  202. package/src/reactive/types.ts +0 -520
  203. package/src/reactive/watch.ts +0 -180
  204. package/src/std-decorators.ts +0 -256
  205. package/src/utils.ts +0 -300
  206. package/src/zone.ts +0 -142
@@ -5,7 +5,7 @@
5
5
  The `atomic` function wraps a function to batch all reactive effects triggered within it, ensuring effects run only once after the function completes. It can be used both as a function wrapper and as a decorator.
6
6
 
7
7
  ```typescript
8
- import { atomic, reactive, effect } from 'mutts/reactive'
8
+ import { atomic, reactive, effect } from 'mutts'
9
9
 
10
10
  const state = reactive({ a: 0, b: 0 })
11
11
 
@@ -43,14 +43,51 @@ service.updateMultiple(5) // Effect runs only once with final values
43
43
 
44
44
  The wrapped function preserves its signature (parameters and return value), and all effects triggered by reactive changes inside it are automatically batched.
45
45
 
46
+ ### `atom()` - Immediate Atomic Execution
47
+
48
+ While `atomic()` **wraps** a function for later calls, `atom()` **runs** a function immediately and atomically. It always executes right away, even inside a nested batch.
49
+
50
+ ```typescript
51
+ import { atom, reactive, effect } from 'mutts'
52
+
53
+ const state = reactive({ a: 0, b: 0 })
54
+
55
+ effect(() => {
56
+ console.log('Values:', state.a, state.b)
57
+ })
58
+
59
+ // Execute multiple mutations atomically — effect fires only once
60
+ atom(() => {
61
+ state.a = 10
62
+ state.b = 20
63
+ })
64
+ ```
65
+
66
+ **`atom` vs `atomic`:**
67
+
68
+ | | `atom(fn)` | `atomic(fn)` |
69
+ |---|---|---|
70
+ | **When it runs** | Immediately | Returns a wrapped function for later calls |
71
+ | **Arguments** | `() => T` | `(...args) => T` (preserves signature) |
72
+ | **Use case** | One-shot batch of mutations | Reusable batched function / decorator |
73
+
74
+ ```typescript
75
+ // atom — runs now
76
+ atom(() => { state.a = 1; state.b = 2 })
77
+
78
+ // atomic — creates wrapper for later
79
+ const update = atomic((a, b) => { state.a = a; state.b = b })
80
+ update(1, 2) // runs when called
81
+ ```
82
+
46
83
  ### `addBatchCleanup()` / `defer()` - Deferring Work to Avoid Cycles
47
84
 
48
85
  When an effect needs to perform an action that would modify state the effect depends on, this can create a reactive cycle. The `addBatchCleanup` function (also exported as `defer` for semantic clarity) allows you to defer such work until after the current batch of effects completes.
49
86
 
50
87
  ```typescript
51
- import { addBatchCleanup, effect, reactive } from 'mutts/reactive'
88
+ import { addBatchCleanup, effect, reactive } from 'mutts'
52
89
  // or use the semantic alias:
53
- // import { defer } from 'mutts/reactive'
90
+ // import { defer } from 'mutts'
54
91
 
55
92
  const state = reactive({
56
93
  items: [],
@@ -261,7 +298,7 @@ effect(() => {
261
298
  Creates a bidirectional binding between a reactive value and a non-reactive external value (like DOM elements), automatically preventing infinite loops.
262
299
 
263
300
  ```typescript
264
- import { biDi, reactive } from 'mutts/reactive'
301
+ import { biDi, reactive } from 'mutts'
265
302
 
266
303
  const model = reactive({ value: '' })
267
304
 
@@ -469,6 +506,31 @@ user.age = 31 // Triggers the callback
469
506
  user.email = 'jane@example.com' // Triggers the callback
470
507
  ```
471
508
 
509
+ #### Flavored Watch
510
+
511
+ The `watch` function is **flavored**, meaning it provides chainable properties for common options like `immediate` and `deep`:
512
+
513
+ ```typescript
514
+ const state = reactive({ count: 0, nested: { a: 1 } })
515
+
516
+ // Watch with immediate execution
517
+ watch.immediate(() => state.count, (v) => console.log(v))
518
+ // Prints: 0 immediately, then 1, 2...
519
+
520
+ // Deep watch
521
+ watch.deep(() => state.nested, (v) => console.log('Nested changed'))
522
+
523
+ // Combined flavors
524
+ watch.immediate.deep(() => state.nested, (v) => {
525
+ console.log('Immediate nested:', v)
526
+ })
527
+ ```
528
+
529
+ These flavors are a shorthand for passing options:
530
+ - `watch.immediate(...)` is equivalent to `watch(..., { immediate: true })`
531
+ - `watch.deep(...)` is equivalent to `watch(..., { deep: true })`
532
+ - `watch.immediate.deep(...)` is equivalent to `watch(..., { immediate: true, deep: true })`
533
+
472
534
  #### Use Cases
473
535
 
474
536
  **Object-level watching** is particularly useful for:
@@ -995,7 +1057,8 @@ state.user = fetchUser() // Only notifies if actual values changed
995
1057
 
996
1058
  // Or explicitly track what you need
997
1059
  effect(() => {
998
- console.log(state.user.profile.name) // Only tracks this specific path
1060
+ // Only tracks this specific path: Even if the `user` object changes, this effect will run only if the `profile.name` property has a different value
1061
+ console.log(state.user.profile.name)
999
1062
  })
1000
1063
  ```
1001
1064
 
@@ -1064,6 +1127,224 @@ effect(() => {
1064
1127
 
1065
1128
  **Bottom line:** Recursive touching gives you the granular control you need without deep watching's overhead, making it ideal for modern reactive applications.
1066
1129
 
1130
+ ## Choosing the Right Reactive Primitive
1131
+
1132
+ Mutts provides several ways to derive values from reactive state. They differ in **when** they recompute (eager vs lazy), **what** they return (raw value vs reactive proxy), and **how** downstream consumers track changes.
1133
+
1134
+ ### Comparison Table
1135
+
1136
+ | Primitive | Evaluation | Output | Trackable | Identity stable | Cleanup | Best for |
1137
+ | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
1138
+ | `effect(() => ...)` | Eager | Side-effect (void) | N/A | N/A | Auto (parent/GC) + Returned cleanup | Side effects, DOM updates |
1139
+ | `memoize(() => expr)` | **Lazy** | Raw value | Yes (return) | No (new value each call) | Auto (WeakMap GC) | Cached scalars, computed getters |
1140
+ | `memoize(fn)(args)` | **Lazy** | Raw value | Yes (return) | No | Auto (WeakMap GC) | Parameterized caching |
1141
+ | `lift(() => [...])` | Eager | Reactive array proxy | Yes (per-index) | **Yes** | `result[cleanup]()` | Derived collections (filter, map) |
1142
+ | `lift(() => ({...}))` | Eager | Reactive object proxy | Yes (per-prop) | **Yes** | `result[cleanup]()` | Derived objects, computed shapes |
1143
+ | `morph(source, fn)` | **Lazy** | Reactive proxy | Yes (per-key) | **Yes** | `cleanedBy` | Lazy per-element map with identity tracking |
1144
+ | `morph.pure(source, fn)` | **Lazy** | Reactive proxy | Yes (per-key) | **Yes** | `cleanedBy` | Same, but skips per-item dependency tracking |
1145
+ | `when(() => cond)` | Eager | Promise\<T\> | N/A | N/A | Auto (on resolve/timeout) | Awaiting a reactive condition |
1146
+ | `watch(source, cb)` | Eager | Callback (old/new) | N/A | N/A | Auto (parent/GC) + Returned cleanup | Observing specific changes |
1147
+
1148
+ ### Key distinctions
1149
+
1150
+ - **Lazy vs Eager**: `memoize` only recomputes when the result is read. Everything else recomputes immediately when dependencies change — even if nobody is consuming the output.
1151
+ - **Trackable**: Can downstream effects depend on the result? `memoize`'s return value is trackable because calling it runs inside an effect context. `lift`/`morph` return reactive proxies where each property/index is independently trackable.
1152
+ - **Identity stable**: `lift` and `morph` return the **same proxy** across recomputations — only changed slots are updated. This is critical for downstream transformations or DOM reconciliation that relies on reference identity.
1153
+
1154
+ ### Common patterns
1155
+
1156
+ **Lazy computed scalar** (like Vue/Solid `computed`):
1157
+ ```typescript
1158
+ const total = memoize(() => state.price * state.quantity)
1159
+ effect(() => console.log(total())) // recomputes only when read
1160
+ ```
1161
+
1162
+ **Eager computed object** (trackable reactive proxy):
1163
+ ```typescript
1164
+ const profile = lift(() => ({
1165
+ displayName: user.name.toUpperCase(),
1166
+ isAdult: user.age >= 18,
1167
+ }))
1168
+ effect(() => console.log(profile.displayName)) // tracks .displayName only
1169
+ ```
1170
+
1171
+ **Derived filtered collection**:
1172
+ ```typescript
1173
+ const active = lift(() => items.filter(x => x.active))
1174
+ // active is a reactive array — project() or effects on active[i] work fine
1175
+ ```
1176
+
1177
+ **Per-element transform**:
1178
+ ```typescript
1179
+ const doubled = morph(numbers, ({ value }) => value * 2)
1180
+ // Each index has its own effect — changing numbers[3] only recomputes doubled[3]
1181
+ ```
1182
+
1183
+ ### `lift` vs `morph` — choosing the right primitive
1184
+
1185
+ Both `lift` and `morph` produce a stable reactive object from reactive inputs, but they differ fundamentally in **effect topology**:
1186
+
1187
+ | | `lift` | `morph` |
1188
+ |---|---|---|
1189
+ | **Effects** | 1 (single callback) | N+1 (1 outer + 1 per non-pure key) |
1190
+ | **Input** | Callback returning a plain object/array | Reactive source (array/record/Map) |
1191
+ | **Granularity** | Re-runs entire callback, diffs result | Per-key effect, only changed keys re-run |
1192
+ | **Key lifecycle** | Automatic (diff adds/removes keys) | Automatic (arrayDiff handles updates) |
1193
+ | **Execution** | Eager (recomputes immediately) | **Lazy** (recomputes on access) |
1194
+ | **Best for** | Computed shapes, small derived objects | Large collections with independent per-item logic |
1195
+
1196
+ **Rule of thumb**: if the source has **stable keys** and the mapping is **cheap**, prefer `lift` — one effect is far cheaper than N+1. Reserve `morph` for cases where per-key lifecycle matters (e.g. each key spawns side-effects, or the collection is large and only individual items change).
1197
+
1198
+ ```typescript
1199
+ // lift: 1 effect, diffs ~5 properties — ideal for derived shapes
1200
+ const profile = lift(() => ({
1201
+ displayName: user.name.toUpperCase(),
1202
+ isAdult: user.age >= 18,
1203
+ label: `${user.name} (${user.role})`,
1204
+ }))
1205
+
1206
+ // morph: 1000 items, only item[42] changes — only item[42]'s effect re-runs
1207
+ const results = morph(numbers, (val) => val * 2)
1208
+ ```
1209
+
1210
+ **Performance note**: each `morph` key creates a full reactive effect with its own dependency tracking. For a page with many small mappings (e.g. HTML element props), this compounds quickly. Converting small `morph` calls to `lift` can dramatically reduce total effect count.
1211
+
1212
+ ### When deep touching makes `lift` unnecessary
1213
+
1214
+ If you're simply **replacing** a reactive property with a new value of the same shape, deep touching already diffs element-by-element:
1215
+
1216
+ ```typescript
1217
+ state.items = fetchedItems // deep touch diffs old vs new per-index — no lift needed
1218
+ ```
1219
+
1220
+ `lift` is for **derived** data where there's no single property to assign to — the output is computed from scratch each time.
1221
+
1222
+ ## Morph
1223
+
1224
+ ### `morph(source, fn)`
1225
+
1226
+ `morph` creates a **lazy, identity-stable** reactive array by mapping each element of a source array through a callback. Unlike `lift` (which re-runs the entire callback and diffs the result), `morph` only computes an element when it is accessed, and tracks the source array via `arrayDiff` to efficiently handle insertions, removals, and moves.
1227
+
1228
+ **Signature**
1229
+
1230
+ ```typescript
1231
+ import { morph } from 'mutts'
1232
+
1233
+ function morph<I, O>(
1234
+ source: readonly I[] | (() => readonly I[]),
1235
+ fn: (arg: I) => O,
1236
+ options?: { pure?: boolean | ((i: I) => boolean) }
1237
+ ): O[]
1238
+ ```
1239
+
1240
+ **Parameters**
1241
+
1242
+ - `source`: a reactive array or a function returning one. Array mutations are tracked via `arrayDiff`.
1243
+ - `fn`: mapping callback. In the default (non-pure) mode, each element's computation runs inside its own effect, so reactive reads inside `fn` are tracked and will invalidate that element's cache when they change.
1244
+ - `options.pure`: controls per-item effect creation. `true` skips effects for all items (same as `morph.pure`). A **predicate function** `(i: I) => boolean` decides per-item: return `true` to skip the effect (pure), `false` to create one (reactive). The predicate receives the input item and is evaluated once per cache slot on first access.
1245
+
1246
+ **Behaviour**
1247
+
1248
+ - **Lazy**: elements are only computed when accessed (e.g. `result[0]`). Unaccessed indices remain `undefined` in the cache.
1249
+ - **Identity stable**: the returned reactive array proxy is the same object across source mutations. Only affected indices are invalidated.
1250
+ - **Per-item effects** (default): each accessed element gets its own effect. If `fn` reads reactive values beyond its argument, changes to those values invalidate and recompute only the affected elements.
1251
+ - **Cleanup**: the returned array is `cleanedBy` the internal morph effect. When the parent effect is disposed, the morph effect and all per-item effects are cleaned up.
1252
+
1253
+ **Basic usage**
1254
+
1255
+ ```typescript
1256
+ import { morph, reactive, effect } from 'mutts'
1257
+
1258
+ const items = reactive(['alice', 'bob', 'charlie'])
1259
+ const upper = morph(items, name => name.toUpperCase())
1260
+
1261
+ effect(() => {
1262
+ console.log(upper[0]) // "ALICE" — only element 0 is computed
1263
+ })
1264
+
1265
+ items.push('dave')
1266
+ console.log(upper[3]) // "DAVE"
1267
+
1268
+ items.splice(1, 1) // Remove 'bob' — indices shift, cache invalidated for affected positions
1269
+ ```
1270
+
1271
+ **With reactive callback dependencies**
1272
+
1273
+ ```typescript
1274
+ const source = reactive([1, 2, 3])
1275
+ const multiplier = reactive({ value: 2 })
1276
+
1277
+ const scaled = morph(source, x => x * multiplier.value)
1278
+
1279
+ console.log(scaled[0]) // 2
1280
+ multiplier.value = 10
1281
+ console.log(scaled[0]) // 10 — per-item effect re-ran
1282
+ ```
1283
+
1284
+ ### `morph.pure`
1285
+
1286
+ A flavored variant that skips per-item effects. Use when the callback is a **pure function** of its argument with no external reactive dependencies.
1287
+
1288
+ ```typescript
1289
+ const doubled = morph.pure(source, x => x * 2)
1290
+ ```
1291
+
1292
+ **Differences from default `morph`:**
1293
+
1294
+ | | `morph` | `morph.pure` |
1295
+ |---|---|---|
1296
+ | **Per-item effects** | Yes — tracks `fn`'s reactive reads | No — `fn` runs once, result is cached |
1297
+ | **Callback dependency invalidation** | Automatic | None — stale if `fn` reads reactive values |
1298
+ | **Non-reactive source optimization** | Returns reactive proxy | Returns plain `source.map(fn)` |
1299
+ | **Best for** | Callbacks that read reactive state | Pure transforms (`x => x * 2`, `e => e.render()`) |
1300
+
1301
+ ### `pure` predicate — selective per-item mode
1302
+
1303
+ When `pure` is a function, each item is individually classified as pure or reactive on first access:
1304
+
1305
+ ```typescript
1306
+ const result = morph(source, x => x * factor.value, {
1307
+ pure: (x: number) => x > 2
1308
+ })
1309
+ ```
1310
+
1311
+ Items where the predicate returns `true` are computed once (no effect, no dependency tracking). Items where it returns `false` get a per-item effect that reacts to external dependency changes.
1312
+
1313
+ ```typescript
1314
+ const source = reactive(['static', 'dynamic', 'static'])
1315
+ const suffix = reactive({ value: '!' })
1316
+
1317
+ const result = morph(source, s => s + suffix.value, {
1318
+ pure: (s: string) => s === 'static'
1319
+ })
1320
+
1321
+ result[0] // 'static!' — pure, no effect
1322
+ result[1] // 'dynamic!' — reactive, tracks suffix.value
1323
+
1324
+ suffix.value = '?'
1325
+ result[0] // 'static!' — stale (pure)
1326
+ result[1] // 'dynamic?' — updated (reactive)
1327
+ ```
1328
+
1329
+ New items added to the source are classified by the predicate at first access. This is useful when some items are known to be static while others depend on external reactive state.
1330
+
1331
+ **Example: pure vs reactive**
1332
+
1333
+ ```typescript
1334
+ const source = reactive([1, 2])
1335
+ const factor = reactive({ value: 10 })
1336
+
1337
+ // Reactive: tracks factor.value per element
1338
+ const reactive = morph(source, x => x * factor.value)
1339
+
1340
+ // Pure: ignores factor.value changes
1341
+ const pure = morph.pure(source, x => x * factor.value)
1342
+
1343
+ factor.value = 20
1344
+ reactive[0] // 20 — recomputed
1345
+ pure[0] // 10 — stale, no per-item effect to invalidate
1346
+ ```
1347
+
1067
1348
  ## Memoization
1068
1349
 
1069
1350
  ### `memoize()`
@@ -1073,7 +1354,7 @@ effect(() => {
1073
1354
  **Signature**
1074
1355
 
1075
1356
  ```typescript
1076
- import { memoize } from 'mutts/reactive'
1357
+ import { memoize } from 'mutts'
1077
1358
 
1078
1359
  type Memoizable = object | any[] | symbol
1079
1360
 
@@ -1098,7 +1379,7 @@ function memoize<Result>(
1098
1379
  **Basic usage**
1099
1380
 
1100
1381
  ```typescript
1101
- import { effect, memoize, reactive } from 'mutts/reactive'
1382
+ import { effect, memoize, reactive } from 'mutts'
1102
1383
 
1103
1384
  const source = reactive({ value: 1 })
1104
1385
  const args = { node: source }
@@ -1139,12 +1420,33 @@ describe({ id: 'x' } as any, { language: 'en' }) // locale is ignored, only the
1139
1420
 
1140
1421
  Use `maxArgs` when the memoized function should only consider the first _n_ arguments. Subsequent arguments are ignored and not forwarded to `fn`.
1141
1422
 
1423
+ ### `memoize.lenient`
1424
+
1425
+ A flavored variant that gracefully handles non-WeakKey arguments (primitives, `null`, `undefined`). Instead of throwing, it falls back to recomputing the function without caching.
1426
+
1427
+ ```typescript
1428
+ import { memoize } from 'mutts'
1429
+
1430
+ const process = memoize.lenient((value: string | { data: string }) => {
1431
+ return typeof value === 'string' ? value.toUpperCase() : value.data
1432
+ })
1433
+
1434
+ const obj = { data: 'hello' }
1435
+ process(obj) // Cached (object is a WeakKey)
1436
+ process(obj) // Returns cached result
1437
+
1438
+ process('world') // Recomputed each time (string is not a WeakKey)
1439
+ process('world') // Recomputed again — no caching for primitives
1440
+ ```
1441
+
1442
+ This is useful when a memoized function may receive both objects and primitives, and you want caching where possible without errors for the rest.
1443
+
1142
1444
  ### Decorator usage
1143
1445
 
1144
- Apply `@memoize` to class getters or methods to share the same cache semantics. Getters cache per instance; methods cache per instance and argument tuple.
1446
+ Apply `@memoize` (or `@memoize.lenient`) to class getters or methods to share the same cache semantics. Getters cache per instance; methods cache per instance and argument tuple.
1145
1447
 
1146
1448
  ```typescript
1147
- import { memoize, reactive } from 'mutts/reactive'
1449
+ import { memoize, reactive } from 'mutts'
1148
1450
 
1149
1451
  class Example {
1150
1452
  state = reactive({ count: 0 })
@@ -1159,6 +1461,13 @@ class Example {
1159
1461
  return a.value + b.value + this.state.count
1160
1462
  }
1161
1463
  }
1464
+
1465
+ class Flexible {
1466
+ @memoize.lenient
1467
+ process(value: string) {
1468
+ return value.toUpperCase() // No throw — recomputes each time
1469
+ }
1470
+ }
1162
1471
  ```
1163
1472
 
1164
1473
  ## Debugging and Development
@@ -10,7 +10,7 @@ The `attend` utility reactively iterates over the entries of a collection, runni
10
10
  - **Disposes** the inner effect when the key is removed from the collection.
11
11
  - Allows the callback to return a **cleanup function** (like a regular effect closer).
12
12
 
13
- This is the foundational lifecycle primitive that `describe` and `organized` are built on.
13
+ This is the foundational lifecycle primitive that `organized` is built on.
14
14
 
15
15
  ## API
16
16
 
@@ -135,9 +135,7 @@ attend(
135
135
  | Primitive | Uses `attend`? | Purpose |
136
136
  |---|---|---|
137
137
  | `attend` | — | Reactive lifecycle per key |
138
- | `describe` | ✅ | Reactive `Object.defineProperty` per key |
139
138
  | `organized` | ✅ | Reactive record mapping with access objects |
140
139
  | `project` | ❌ | Reactive collection mapping (manages a target + projection context) |
141
- | `scan` | ❌ | Reactive accumulation (sequential key dependency) |
142
140
 
143
- `project` and `scan` have additional concerns (target management, sequential dependencies) that go beyond `attend`'s independent-key lifecycle model.
141
+ `project` has additional concerns (target management) that go beyond `attend`'s independent-key lifecycle model.