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
@@ -43,6 +43,43 @@ 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/reactive'
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.
@@ -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,225 @@ 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
+ | `scan(source, fn, init)` | Eager | Reactive array | Yes (per-index) | **Yes** | `result[cleanup]()` | Running accumulations (prefix sums) |
1146
+ | `when(() => cond)` | Eager | Promise\<T\> | N/A | N/A | Auto (on resolve/timeout) | Awaiting a reactive condition |
1147
+ | `watch(source, cb)` | Eager | Callback (old/new) | N/A | N/A | Auto (parent/GC) + Returned cleanup | Observing specific changes |
1148
+
1149
+ ### Key distinctions
1150
+
1151
+ - **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.
1152
+ - **Trackable**: Can downstream effects depend on the result? `memoize`'s return value is trackable because calling it runs inside an effect context. `lift`/`morph`/`scan` return reactive proxies where each property/index is independently trackable.
1153
+ - **Identity stable**: `lift`, `morph`, and `scan` 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.
1154
+
1155
+ ### Common patterns
1156
+
1157
+ **Lazy computed scalar** (like Vue/Solid `computed`):
1158
+ ```typescript
1159
+ const total = memoize(() => state.price * state.quantity)
1160
+ effect(() => console.log(total())) // recomputes only when read
1161
+ ```
1162
+
1163
+ **Eager computed object** (trackable reactive proxy):
1164
+ ```typescript
1165
+ const profile = lift(() => ({
1166
+ displayName: user.name.toUpperCase(),
1167
+ isAdult: user.age >= 18,
1168
+ }))
1169
+ effect(() => console.log(profile.displayName)) // tracks .displayName only
1170
+ ```
1171
+
1172
+ **Derived filtered collection**:
1173
+ ```typescript
1174
+ const active = lift(() => items.filter(x => x.active))
1175
+ // active is a reactive array — project() or effects on active[i] work fine
1176
+ ```
1177
+
1178
+ **Per-element transform**:
1179
+ ```typescript
1180
+ const doubled = morph(numbers, ({ value }) => value * 2)
1181
+ // Each index has its own effect — changing numbers[3] only recomputes doubled[3]
1182
+ ```
1183
+
1184
+ ### `lift` vs `morph` — choosing the right primitive
1185
+
1186
+ Both `lift` and `morph` produce a stable reactive object from reactive inputs, but they differ fundamentally in **effect topology**:
1187
+
1188
+ | | `lift` | `morph` |
1189
+ |---|---|---|
1190
+ | **Effects** | 1 (single callback) | N+1 (1 outer + 1 per non-pure key) |
1191
+ | **Input** | Callback returning a plain object/array | Reactive source (array/record/Map) |
1192
+ | **Granularity** | Re-runs entire callback, diffs result | Per-key effect, only changed keys re-run |
1193
+ | **Key lifecycle** | Automatic (diff adds/removes keys) | Automatic (arrayDiff handles updates) |
1194
+ | **Execution** | Eager (recomputes immediately) | **Lazy** (recomputes on access) |
1195
+ | **Best for** | Computed shapes, small derived objects | Large collections with independent per-item logic |
1196
+
1197
+ **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).
1198
+
1199
+ ```typescript
1200
+ // lift: 1 effect, diffs ~5 properties — ideal for derived shapes
1201
+ const profile = lift(() => ({
1202
+ displayName: user.name.toUpperCase(),
1203
+ isAdult: user.age >= 18,
1204
+ label: `${user.name} (${user.role})`,
1205
+ }))
1206
+
1207
+ // morph: 1000 items, only item[42] changes — only item[42]'s effect re-runs
1208
+ const results = morph(numbers, (val) => val * 2)
1209
+ ```
1210
+
1211
+ **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.
1212
+
1213
+ ### When deep touching makes `lift` unnecessary
1214
+
1215
+ If you're simply **replacing** a reactive property with a new value of the same shape, deep touching already diffs element-by-element:
1216
+
1217
+ ```typescript
1218
+ state.items = fetchedItems // deep touch diffs old vs new per-index — no lift needed
1219
+ ```
1220
+
1221
+ `lift` is for **derived** data where there's no single property to assign to — the output is computed from scratch each time.
1222
+
1223
+ ## Morph
1224
+
1225
+ ### `morph(source, fn)`
1226
+
1227
+ `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.
1228
+
1229
+ **Signature**
1230
+
1231
+ ```typescript
1232
+ import { morph } from 'mutts/reactive'
1233
+
1234
+ function morph<I, O>(
1235
+ source: readonly I[] | (() => readonly I[]),
1236
+ fn: (arg: I) => O,
1237
+ options?: { pure?: boolean | ((i: I) => boolean) }
1238
+ ): O[]
1239
+ ```
1240
+
1241
+ **Parameters**
1242
+
1243
+ - `source`: a reactive array or a function returning one. Array mutations are tracked via `arrayDiff`.
1244
+ - `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.
1245
+ - `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.
1246
+
1247
+ **Behaviour**
1248
+
1249
+ - **Lazy**: elements are only computed when accessed (e.g. `result[0]`). Unaccessed indices remain `undefined` in the cache.
1250
+ - **Identity stable**: the returned reactive array proxy is the same object across source mutations. Only affected indices are invalidated.
1251
+ - **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.
1252
+ - **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.
1253
+
1254
+ **Basic usage**
1255
+
1256
+ ```typescript
1257
+ import { morph, reactive, effect } from 'mutts/reactive'
1258
+
1259
+ const items = reactive(['alice', 'bob', 'charlie'])
1260
+ const upper = morph(items, name => name.toUpperCase())
1261
+
1262
+ effect(() => {
1263
+ console.log(upper[0]) // "ALICE" — only element 0 is computed
1264
+ })
1265
+
1266
+ items.push('dave')
1267
+ console.log(upper[3]) // "DAVE"
1268
+
1269
+ items.splice(1, 1) // Remove 'bob' — indices shift, cache invalidated for affected positions
1270
+ ```
1271
+
1272
+ **With reactive callback dependencies**
1273
+
1274
+ ```typescript
1275
+ const source = reactive([1, 2, 3])
1276
+ const multiplier = reactive({ value: 2 })
1277
+
1278
+ const scaled = morph(source, x => x * multiplier.value)
1279
+
1280
+ console.log(scaled[0]) // 2
1281
+ multiplier.value = 10
1282
+ console.log(scaled[0]) // 10 — per-item effect re-ran
1283
+ ```
1284
+
1285
+ ### `morph.pure`
1286
+
1287
+ A flavored variant that skips per-item effects. Use when the callback is a **pure function** of its argument with no external reactive dependencies.
1288
+
1289
+ ```typescript
1290
+ const doubled = morph.pure(source, x => x * 2)
1291
+ ```
1292
+
1293
+ **Differences from default `morph`:**
1294
+
1295
+ | | `morph` | `morph.pure` |
1296
+ |---|---|---|
1297
+ | **Per-item effects** | Yes — tracks `fn`'s reactive reads | No — `fn` runs once, result is cached |
1298
+ | **Callback dependency invalidation** | Automatic | None — stale if `fn` reads reactive values |
1299
+ | **Non-reactive source optimization** | Returns reactive proxy | Returns plain `source.map(fn)` |
1300
+ | **Best for** | Callbacks that read reactive state | Pure transforms (`x => x * 2`, `e => e.render()`) |
1301
+
1302
+ ### `pure` predicate — selective per-item mode
1303
+
1304
+ When `pure` is a function, each item is individually classified as pure or reactive on first access:
1305
+
1306
+ ```typescript
1307
+ const result = morph(source, x => x * factor.value, {
1308
+ pure: (x: number) => x > 2
1309
+ })
1310
+ ```
1311
+
1312
+ 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.
1313
+
1314
+ ```typescript
1315
+ const source = reactive(['static', 'dynamic', 'static'])
1316
+ const suffix = reactive({ value: '!' })
1317
+
1318
+ const result = morph(source, s => s + suffix.value, {
1319
+ pure: (s: string) => s === 'static'
1320
+ })
1321
+
1322
+ result[0] // 'static!' — pure, no effect
1323
+ result[1] // 'dynamic!' — reactive, tracks suffix.value
1324
+
1325
+ suffix.value = '?'
1326
+ result[0] // 'static!' — stale (pure)
1327
+ result[1] // 'dynamic?' — updated (reactive)
1328
+ ```
1329
+
1330
+ 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.
1331
+
1332
+ **Example: pure vs reactive**
1333
+
1334
+ ```typescript
1335
+ const source = reactive([1, 2])
1336
+ const factor = reactive({ value: 10 })
1337
+
1338
+ // Reactive: tracks factor.value per element
1339
+ const reactive = morph(source, x => x * factor.value)
1340
+
1341
+ // Pure: ignores factor.value changes
1342
+ const pure = morph.pure(source, x => x * factor.value)
1343
+
1344
+ factor.value = 20
1345
+ reactive[0] // 20 — recomputed
1346
+ pure[0] // 10 — stale, no per-item effect to invalidate
1347
+ ```
1348
+
1067
1349
  ## Memoization
1068
1350
 
1069
1351
  ### `memoize()`
@@ -1139,9 +1421,30 @@ describe({ id: 'x' } as any, { language: 'en' }) // locale is ignored, only the
1139
1421
 
1140
1422
  Use `maxArgs` when the memoized function should only consider the first _n_ arguments. Subsequent arguments are ignored and not forwarded to `fn`.
1141
1423
 
1424
+ ### `memoize.lenient`
1425
+
1426
+ A flavored variant that gracefully handles non-WeakKey arguments (primitives, `null`, `undefined`). Instead of throwing, it falls back to recomputing the function without caching.
1427
+
1428
+ ```typescript
1429
+ import { memoize } from 'mutts/reactive'
1430
+
1431
+ const process = memoize.lenient((value: string | { data: string }) => {
1432
+ return typeof value === 'string' ? value.toUpperCase() : value.data
1433
+ })
1434
+
1435
+ const obj = { data: 'hello' }
1436
+ process(obj) // Cached (object is a WeakKey)
1437
+ process(obj) // Returns cached result
1438
+
1439
+ process('world') // Recomputed each time (string is not a WeakKey)
1440
+ process('world') // Recomputed again — no caching for primitives
1441
+ ```
1442
+
1443
+ This is useful when a memoized function may receive both objects and primitives, and you want caching where possible without errors for the rest.
1444
+
1142
1445
  ### Decorator usage
1143
1446
 
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.
1447
+ 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
1448
 
1146
1449
  ```typescript
1147
1450
  import { memoize, reactive } from 'mutts/reactive'
@@ -1159,6 +1462,13 @@ class Example {
1159
1462
  return a.value + b.value + this.state.count
1160
1463
  }
1161
1464
  }
1465
+
1466
+ class Flexible {
1467
+ @memoize.lenient
1468
+ process(value: string) {
1469
+ return value.toUpperCase() // No throw — recomputes each time
1470
+ }
1471
+ }
1162
1472
  ```
1163
1473
 
1164
1474
  ## 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,7 +135,6 @@ 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
140
  | `scan` | ❌ | Reactive accumulation (sequential key dependency) |
@@ -408,20 +408,20 @@ Notes:
408
408
 
409
409
  ## Class Reactivity
410
410
 
411
- ## Projection
411
+ ## Morphing
412
412
 
413
- ### `project()`
413
+ ### `morph()`
414
414
 
415
- `project()` provides a unified API for transforming reactive collections (arrays, records, and maps) into new reactive collections. Each source entry gets its own reactive effect that recomputes only when that specific entry changes, enabling granular updates perfect for rendering pipelines.
415
+ `morph()` provides a unified API for transforming reactive collections (arrays, records, and maps) into new reactive collections. Each source entry gets its own reactive effect that recomputes only when that specific entry changes, enabling granular updates perfect for rendering pipelines.
416
416
 
417
417
  #### Basic Usage
418
418
 
419
419
  ```typescript
420
- import { cleanup, project, reactive } from 'mutts/reactive'
420
+ import { cleanup, morph, reactive } from 'mutts/reactive'
421
421
 
422
422
  // Arrays
423
423
  const users = reactive([{ name: 'John', age: 30 }, { name: 'Jane', age: 25 }])
424
- const names = project.array(users, ({ get }) => get()?.name.toUpperCase() ?? '')
424
+ const names = morph(users, (user) => user.name.toUpperCase())
425
425
 
426
426
  console.log(names) // ['JOHN', 'JANE']
427
427
 
@@ -430,8 +430,7 @@ console.log(names[0]) // 'JOHNNY' - only index 0 recomputed
430
430
 
431
431
  // Records
432
432
  const scores = reactive({ math: 90, science: 85 })
433
- const grades = project.record(scores, ({ get }) => {
434
- const score = get()
433
+ const grades = morph(scores, (score) => {
435
434
  return score >= 90 ? 'A' : score >= 80 ? 'B' : 'C'
436
435
  })
437
436
 
@@ -444,7 +443,7 @@ const inventory = reactive(new Map([
444
443
  ['apples', { count: 10 }],
445
444
  ['oranges', { count: 5 }]
446
445
  ]))
447
- const totals = project.map(inventory, ({ get }) => get()?.count ?? 0)
446
+ const totals = morph(inventory, (item) => item.count)
448
447
 
449
448
  console.log(totals.get('apples')) // 10
450
449
  inventory.get('apples')!.count = 15
@@ -453,41 +452,25 @@ console.log(totals.get('apples')) // 15 - only 'apples' key recomputed
453
452
 
454
453
  #### Automatic Type Selection
455
454
 
456
- You can use `project()` directly and it will automatically select the appropriate helper based on the source type:
455
+ The `morph()` function automatically selects the appropriate helper based on the source type:
457
456
 
458
457
  ```typescript
459
- // Automatically uses project.array
460
- const doubled = project([1, 2, 3], ({ get }) => get() * 2)
458
+ // Arrays
459
+ const doubled = morph([1, 2, 3], (x) => x * 2)
461
460
 
462
- // Automatically uses project.record
463
- const upper = project({ a: 'hello', b: 'world' }, ({ get }) => get()?.toUpperCase() ?? '')
461
+ // Records
462
+ const upper = morph({ a: 'hello', b: 'world' }, (s) => s.toUpperCase())
464
463
 
465
- // Automatically uses project.map
466
- const counts = project(new Map([['x', 1], ['y', 2]]), ({ get }) => get() * 2)
464
+ // Maps
465
+ const counts = morph(new Map([['x', 1], ['y', 2]]), (v) => v * 2)
467
466
  ```
468
467
 
469
- #### Access Object
470
-
471
- The callback receives a `ProjectAccess` object with:
468
+ #### Callback Signature
472
469
 
473
- - **`get()`**: Function that returns the current source value for this key/index
474
- - **`set(value)`**: Function to update the source value (if the source is mutable)
475
- - **`key`**: The current key or index
476
- - **`source`**: Reference to the original source collection
477
- - **`old`**: Previously computed result for this entry (undefined on first run)
478
- - **`value`**: Computed property that mirrors `get()` (for convenience)
470
+ The callback function receives the current value of the entry being processed:
479
471
 
480
472
  ```typescript
481
- const transformed = project.array(items, (access) => {
482
- // Access the source value
483
- const item = access.get()
484
-
485
- // Access the key/index
486
- console.log(`Processing index ${access.key}`)
487
-
488
- // Leverage previous result
489
- console.log(`Previous result: ${access.old}`)
490
-
473
+ const transformed = morph(items, (item) => {
491
474
  // Transform and return
492
475
  return item.value * 2
493
476
  })
@@ -505,9 +488,8 @@ const users = reactive([
505
488
  ])
506
489
 
507
490
  let computeCount = 0
508
- const summaries = project.array(users, ({ get }) => {
491
+ const summaries = morph(users, (user) => {
509
492
  computeCount++
510
- const user = get()
511
493
  return `${user.name}: ${user.score}`
512
494
  })
513
495
 
@@ -525,11 +507,11 @@ console.log(computeCount) // 5 (only new index 3 computed)
525
507
 
526
508
  #### Key Addition and Removal
527
509
 
528
- `project()` automatically handles keys being added or removed from the source:
510
+ `morph()` automatically handles keys being added or removed from the source:
529
511
 
530
512
  ```typescript
531
513
  const source = reactive({ a: 1, b: 2 })
532
- const doubled = project.record(source, ({ get }) => get() * 2)
514
+ const doubled = morph(source, (val) => val * 2)
533
515
 
534
516
  console.log(doubled.a) // 2
535
517
  console.log(doubled.b) // 4
@@ -548,7 +530,7 @@ console.log('a' in doubled) // false (automatically removed)
548
530
  The returned object includes a `cleanup` symbol that stops all reactive effects:
549
531
 
550
532
  ```typescript
551
- const result = project.array(items, ({ get }) => get() * 2)
533
+ const result = morph(items, (item) => item * 2)
552
534
 
553
535
  // Later, when done
554
536
  result[cleanup]() // Stops all effects and cleans up
@@ -13,15 +13,11 @@
13
13
  - [Evolution Tracking](./advanced.md#evolution-tracking)
14
14
  - [Prototype Chains and Pure Objects](./advanced.md#prototype-chains-and-pure-objects)
15
15
  - [Recursive Touching](./advanced.md#recursive-touching)
16
- - [Why Not Deep Watching?](./advanced.md#why-not-deep-watching)
17
- - [Collections](./advanced.md#collections)
18
- - [Register](./advanced.md#register)
19
- - [Class Reactivity](./advanced.md#class-reactivity)
20
- - [Non-Reactive System](./advanced.md#non-reactive-system)
21
- - [Array Mapping](./advanced.md#array-mapping)
22
- - [Projection](./advanced.md#projection)
23
- - [Reactive Property Description](./describe.md)
24
- - [Record Organization](./advanced.md#record-organization)
16
+ - [Collections](./collections.md)
17
+ - [Register](./collections.md#register)
18
+ - [Class Reactivity](#class-reactivity)
19
+ - [Non-Reactive System](#non-reactive-system)
20
+ - [Morphing](./advanced.md#morph)
25
21
  - [Memoization](./advanced.md#memoization)
26
22
  - [Debugging and Development](./advanced.md#debugging-and-development)
27
23
  - [Cycle Detection](./advanced.md#cycle-detection)
@@ -107,25 +103,12 @@ const result = memoized(user)
107
103
  **5. Map over arrays:**
108
104
  ```typescript
109
105
  const source = reactive([1, 2, 3])
110
- const doubled = project(source, ({ value }) => value * 2)
106
+ const doubled = morph(source, ({ value }) => value * 2)
111
107
  // [2, 4, 6]
112
108
 
113
109
  source.push(4) // doubled automatically becomes [2, 4, 6, 8]
114
110
  ```
115
111
 
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
-
129
112
  **Ready to go!** Continue reading for advanced features.
130
113
 
131
114
  ---
@@ -217,20 +200,7 @@ reactiveObj.count = 5 // Triggers effect
217
200
  - Objects with prototypes: `reactive(Object.create(parent))`
218
201
  - Class instances: `reactive(new MyClass())`
219
202
 
220
- See [Prototype Chains and Pure Objects](#prototype-chains-and-pure-objects) for detailed information about prototype chain handling.
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.
203
+ See [Prototype Chains and Pure Objects](./advanced.md#prototype-chains-and-pure-objects) for detailed information about prototype chain handling.
234
204
 
235
205
  ### `effect()`
236
206