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.
- package/README.md +58 -33
- package/dist/browser.cjs +1371 -1206
- package/dist/browser.cjs.map +1 -1
- package/dist/browser.d.ts +2 -1392
- package/dist/browser.dev.cjs +110 -0
- package/dist/browser.dev.cjs.map +1 -0
- package/dist/browser.dev.d.ts +2 -0
- package/dist/browser.dev.esm.js +5 -0
- package/dist/browser.dev.esm.js.map +1 -0
- package/dist/browser.esm.js +6 -87
- package/dist/browser.esm.js.map +1 -1
- package/dist/chunks/{async-browser-lvzLOCgk.cjs → async-browser-BU_IfxYD.cjs} +4 -85
- package/dist/chunks/async-browser-BU_IfxYD.cjs.map +1 -0
- package/dist/chunks/{async-node-C3DeIb0y.cjs → async-node-3PrbVAbB.cjs} +3 -1
- package/dist/chunks/async-node-3PrbVAbB.cjs.map +1 -0
- package/dist/chunks/{index-2vea86wD.esm.js → index-CaaQQlPJ.esm.js} +1116 -953
- package/dist/chunks/index-CaaQQlPJ.esm.js.map +1 -0
- package/dist/chunks/node-nKJBk8iJ.esm.js +96 -0
- package/dist/chunks/node-nKJBk8iJ.esm.js.map +1 -0
- package/dist/chunks/{index-VTO-b2vR.cjs → proxy-Dtg-bJ3T.cjs} +2285 -2983
- package/dist/chunks/proxy-Dtg-bJ3T.cjs.map +1 -0
- package/dist/chunks/{index-CtA2AWl3.esm.js → proxy-r7lARftl.esm.js} +2273 -2948
- package/dist/chunks/proxy-r7lARftl.esm.js.map +1 -0
- package/dist/debug.cjs +973 -28
- package/dist/debug.cjs.map +1 -1
- package/dist/debug.d.ts +36 -37
- package/dist/debug.esm.js +949 -1
- package/dist/debug.esm.js.map +1 -1
- package/dist/devtools/manifest.json +1 -1
- package/dist/devtools/panel.html +1 -1
- package/dist/devtools/panel.js +107 -94
- package/dist/devtools/panel.js.map +1 -1
- package/dist/index.d.ts +1447 -1
- package/dist/mutts.umd.js +7167 -1
- package/dist/mutts.umd.js.map +1 -1
- package/dist/mutts.umd.min.js +1 -1
- package/dist/mutts.umd.min.js.map +1 -1
- package/dist/node.cjs +62 -60
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.ts +2 -2
- package/dist/node.dev.cjs +110 -0
- package/dist/node.dev.cjs.map +1 -0
- package/dist/node.dev.d.ts +2 -0
- package/dist/node.dev.esm.js +6 -0
- package/dist/node.dev.esm.js.map +1 -0
- package/dist/node.esm.js +4 -97
- package/dist/node.esm.js.map +1 -1
- package/dist/{types-DaHFfhlN.d.ts → types-W5vD6m2n.d.ts} +99 -44
- package/docs/ai/api-reference.md +11 -18
- package/docs/ai/manual.md +308 -230
- package/docs/reactive/advanced.md +312 -2
- package/docs/reactive/attend.md +1 -2
- package/docs/reactive/collections.md +21 -39
- package/docs/reactive/core.md +7 -37
- package/docs/reactive/debugging.md +117 -10
- package/docs/reactive/error-handling.md +10 -10
- package/docs/reactive/resource.md +125 -0
- package/docs/reactive/scan.md +32 -1
- package/docs/reactive.md +3 -2
- package/docs/utils.md +70 -0
- package/package.json +59 -30
- package/dist/chunks/async-browser-lvzLOCgk.cjs.map +0 -1
- package/dist/chunks/async-node-C3DeIb0y.cjs.map +0 -1
- package/dist/chunks/index-2vea86wD.esm.js.map +0 -1
- package/dist/chunks/index-CtA2AWl3.esm.js.map +0 -1
- package/dist/chunks/index-VTO-b2vR.cjs.map +0 -1
- package/dist/debug/debug.d.ts +0 -122
- package/dist/debug/debug.d.ts.map +0 -1
- package/dist/debug/index.d.ts +0 -4
- package/dist/debug/index.d.ts.map +0 -1
- package/dist/debug/lineage-panel.d.ts +0 -5
- package/dist/debug/lineage-panel.d.ts.map +0 -1
- package/dist/debug/lineage.d.ts +0 -79
- package/dist/debug/lineage.d.ts.map +0 -1
- package/dist/src/async/browser.d.ts +0 -2
- package/dist/src/async/browser.d.ts.map +0 -1
- package/dist/src/async/index.d.ts +0 -19
- package/dist/src/async/index.d.ts.map +0 -1
- package/dist/src/async/node.d.ts +0 -2
- package/dist/src/async/node.d.ts.map +0 -1
- package/dist/src/decorator.d.ts +0 -106
- package/dist/src/decorator.d.ts.map +0 -1
- package/dist/src/destroyable.d.ts +0 -87
- package/dist/src/destroyable.d.ts.map +0 -1
- package/dist/src/entry-browser.d.ts +0 -3
- package/dist/src/entry-browser.d.ts.map +0 -1
- package/dist/src/entry-node.d.ts +0 -3
- package/dist/src/entry-node.d.ts.map +0 -1
- package/dist/src/eventful.d.ts +0 -20
- package/dist/src/eventful.d.ts.map +0 -1
- package/dist/src/flavored.d.ts +0 -33
- package/dist/src/flavored.d.ts.map +0 -1
- package/dist/src/index.d.ts +0 -14
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/indexable.d.ts +0 -243
- package/dist/src/indexable.d.ts.map +0 -1
- package/dist/src/introspection.d.ts +0 -27
- package/dist/src/introspection.d.ts.map +0 -1
- package/dist/src/iterableWeak.d.ts +0 -53
- package/dist/src/iterableWeak.d.ts.map +0 -1
- package/dist/src/mixins.d.ts +0 -25
- package/dist/src/mixins.d.ts.map +0 -1
- package/dist/src/promiseChain.d.ts +0 -20
- package/dist/src/promiseChain.d.ts.map +0 -1
- package/dist/src/reactive/array.d.ts +0 -48
- package/dist/src/reactive/array.d.ts.map +0 -1
- package/dist/src/reactive/buffer.d.ts +0 -120
- package/dist/src/reactive/buffer.d.ts.map +0 -1
- package/dist/src/reactive/change.d.ts +0 -29
- package/dist/src/reactive/change.d.ts.map +0 -1
- package/dist/src/reactive/deep-touch.d.ts +0 -28
- package/dist/src/reactive/deep-touch.d.ts.map +0 -1
- package/dist/src/reactive/deep-watch-state.d.ts +0 -25
- package/dist/src/reactive/deep-watch-state.d.ts.map +0 -1
- package/dist/src/reactive/deep-watch.d.ts +0 -20
- package/dist/src/reactive/deep-watch.d.ts.map +0 -1
- package/dist/src/reactive/describe.d.ts +0 -12
- package/dist/src/reactive/describe.d.ts.map +0 -1
- package/dist/src/reactive/effect-context.d.ts +0 -34
- package/dist/src/reactive/effect-context.d.ts.map +0 -1
- package/dist/src/reactive/effects.d.ts +0 -164
- package/dist/src/reactive/effects.d.ts.map +0 -1
- package/dist/src/reactive/index.d.ts +0 -19
- package/dist/src/reactive/index.d.ts.map +0 -1
- package/dist/src/reactive/map.d.ts +0 -28
- package/dist/src/reactive/map.d.ts.map +0 -1
- package/dist/src/reactive/memoize.d.ts +0 -28
- package/dist/src/reactive/memoize.d.ts.map +0 -1
- package/dist/src/reactive/non-reactive-state.d.ts +0 -9
- package/dist/src/reactive/non-reactive-state.d.ts.map +0 -1
- package/dist/src/reactive/non-reactive.d.ts +0 -11
- package/dist/src/reactive/non-reactive.d.ts.map +0 -1
- package/dist/src/reactive/project.d.ts +0 -40
- package/dist/src/reactive/project.d.ts.map +0 -1
- package/dist/src/reactive/proxy-state.d.ts +0 -8
- package/dist/src/reactive/proxy-state.d.ts.map +0 -1
- package/dist/src/reactive/proxy.d.ts +0 -23
- package/dist/src/reactive/proxy.d.ts.map +0 -1
- package/dist/src/reactive/record.d.ts +0 -115
- package/dist/src/reactive/record.d.ts.map +0 -1
- package/dist/src/reactive/register.d.ts +0 -125
- package/dist/src/reactive/register.d.ts.map +0 -1
- package/dist/src/reactive/registry.d.ts +0 -21
- package/dist/src/reactive/registry.d.ts.map +0 -1
- package/dist/src/reactive/set.d.ts +0 -26
- package/dist/src/reactive/set.d.ts.map +0 -1
- package/dist/src/reactive/tracking.d.ts +0 -7
- package/dist/src/reactive/tracking.d.ts.map +0 -1
- package/dist/src/reactive/types.d.ts +0 -424
- package/dist/src/reactive/types.d.ts.map +0 -1
- package/dist/src/reactive/watch.d.ts +0 -48
- package/dist/src/reactive/watch.d.ts.map +0 -1
- package/dist/src/std-decorators.d.ts +0 -45
- package/dist/src/std-decorators.d.ts.map +0 -1
- package/dist/src/utils.d.ts +0 -49
- package/dist/src/utils.d.ts.map +0 -1
- package/dist/src/zone.d.ts +0 -40
- package/dist/src/zone.d.ts.map +0 -1
- package/docs/reactive/describe.md +0 -85
- package/docs/reactive/project.md +0 -93
- package/src/async/browser.ts +0 -323
- package/src/async/index.ts +0 -27
- package/src/async/node.ts +0 -92
- package/src/decorator.ts +0 -272
- package/src/destroyable.ts +0 -199
- package/src/entry-browser.ts +0 -5
- package/src/entry-node.ts +0 -5
- package/src/eventful.ts +0 -110
- package/src/flavored.ts +0 -106
- package/src/index.d.ts +0 -12
- package/src/index.ts +0 -64
- package/src/indexable.ts +0 -526
- package/src/introspection.ts +0 -59
- package/src/iterableWeak.ts +0 -233
- package/src/mixins.ts +0 -123
- package/src/promiseChain.ts +0 -110
- package/src/reactive/array.ts +0 -500
- package/src/reactive/buffer.ts +0 -328
- package/src/reactive/change.ts +0 -131
- package/src/reactive/deep-touch.ts +0 -273
- package/src/reactive/deep-watch-state.ts +0 -82
- package/src/reactive/deep-watch.ts +0 -171
- package/src/reactive/describe.ts +0 -39
- package/src/reactive/effect-context.ts +0 -83
- package/src/reactive/effects.ts +0 -1434
- package/src/reactive/index.ts +0 -72
- package/src/reactive/map.ts +0 -142
- package/src/reactive/memoize.ts +0 -186
- package/src/reactive/non-reactive-state.ts +0 -49
- package/src/reactive/non-reactive.ts +0 -43
- package/src/reactive/project.md +0 -107
- package/src/reactive/project.ts +0 -430
- package/src/reactive/proxy-state.ts +0 -27
- package/src/reactive/proxy.ts +0 -282
- package/src/reactive/record.ts +0 -181
- package/src/reactive/register.ts +0 -538
- package/src/reactive/registry.ts +0 -72
- package/src/reactive/set.ts +0 -117
- package/src/reactive/tracking.ts +0 -41
- package/src/reactive/types.ts +0 -520
- package/src/reactive/watch.ts +0 -180
- package/src/std-decorators.ts +0 -256
- package/src/utils.ts +0 -300
- 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
|
-
|
|
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
|
package/docs/reactive/attend.md
CHANGED
|
@@ -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 `
|
|
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
|
-
##
|
|
411
|
+
## Morphing
|
|
412
412
|
|
|
413
|
-
### `
|
|
413
|
+
### `morph()`
|
|
414
414
|
|
|
415
|
-
`
|
|
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,
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
455
|
+
The `morph()` function automatically selects the appropriate helper based on the source type:
|
|
457
456
|
|
|
458
457
|
```typescript
|
|
459
|
-
//
|
|
460
|
-
const doubled =
|
|
458
|
+
// Arrays
|
|
459
|
+
const doubled = morph([1, 2, 3], (x) => x * 2)
|
|
461
460
|
|
|
462
|
-
//
|
|
463
|
-
const upper =
|
|
461
|
+
// Records
|
|
462
|
+
const upper = morph({ a: 'hello', b: 'world' }, (s) => s.toUpperCase())
|
|
464
463
|
|
|
465
|
-
//
|
|
466
|
-
const counts =
|
|
464
|
+
// Maps
|
|
465
|
+
const counts = morph(new Map([['x', 1], ['y', 2]]), (v) => v * 2)
|
|
467
466
|
```
|
|
468
467
|
|
|
469
|
-
####
|
|
470
|
-
|
|
471
|
-
The callback receives a `ProjectAccess` object with:
|
|
468
|
+
#### Callback Signature
|
|
472
469
|
|
|
473
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
`
|
|
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 =
|
|
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 =
|
|
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
|
package/docs/reactive/core.md
CHANGED
|
@@ -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
|
-
|
|
17
|
-
- [
|
|
18
|
-
|
|
19
|
-
- [
|
|
20
|
-
- [
|
|
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 =
|
|
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
|
|