mutts 1.0.8 → 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 +77 -35
- package/dist/browser.cjs +2144 -5355
- package/dist/browser.cjs.map +1 -1
- package/dist/browser.d.ts +2 -1655
- 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 +20 -107
- package/dist/browser.esm.js.map +1 -1
- package/dist/chunks/{async-browser-CA0jPWIi.cjs → async-browser-BU_IfxYD.cjs} +19 -107
- package/dist/chunks/async-browser-BU_IfxYD.cjs.map +1 -0
- package/dist/chunks/{async-core-UqHzvJ-S.cjs → async-core-CRLKP3l-.cjs} +8 -4
- package/dist/chunks/async-core-CRLKP3l-.cjs.map +1 -0
- package/dist/chunks/{async-node-BYHuGTni.cjs → async-node-3PrbVAbB.cjs} +7 -12
- package/dist/chunks/async-node-3PrbVAbB.cjs.map +1 -0
- package/dist/chunks/index-CaaQQlPJ.esm.js +3174 -0
- 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/proxy-Dtg-bJ3T.cjs +3763 -0
- package/dist/chunks/proxy-Dtg-bJ3T.cjs.map +1 -0
- package/dist/chunks/proxy-r7lARftl.esm.js +3701 -0
- package/dist/chunks/proxy-r7lARftl.esm.js.map +1 -0
- package/dist/debug.cjs +980 -0
- package/dist/debug.cjs.map +1 -0
- package/dist/debug.d.ts +208 -0
- package/dist/debug.esm.js +950 -0
- package/dist/debug.esm.js.map +1 -0
- 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 -13
- 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 +70 -66
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.ts +2 -1
- 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 -103
- package/dist/node.esm.js.map +1 -1
- package/dist/{reactive/types.d.ts → types-W5vD6m2n.d.ts} +186 -93
- package/docs/ai/api-reference.md +12 -19
- package/docs/ai/manual.md +521 -17
- package/docs/events.md +57 -2
- package/docs/flavored.md +210 -0
- package/docs/reactive/advanced.md +314 -5
- package/docs/reactive/attend.md +142 -0
- package/docs/reactive/collections.md +76 -39
- package/docs/reactive/core.md +100 -14
- package/docs/reactive/debugging.md +123 -18
- package/docs/reactive/error-handling.md +121 -0
- package/docs/reactive/resource.md +125 -0
- package/docs/reactive/scan.md +247 -1
- package/docs/reactive.md +4 -1
- package/docs/std-decorators.md +102 -51
- package/docs/utils.md +70 -0
- package/docs/zone.md +3 -7
- package/package.json +64 -30
- package/dist/async/browser.d.ts +0 -2
- package/dist/async/browser.d.ts.map +0 -1
- package/dist/async/index.d.ts +0 -18
- package/dist/async/index.d.ts.map +0 -1
- package/dist/async/node.d.ts +0 -2
- package/dist/async/node.d.ts.map +0 -1
- package/dist/chunks/async-browser-CA0jPWIi.cjs.map +0 -1
- package/dist/chunks/async-core-UqHzvJ-S.cjs.map +0 -1
- package/dist/chunks/async-node-BYHuGTni.cjs.map +0 -1
- package/dist/chunks/index-DhaOVusv.esm.js +0 -6411
- package/dist/chunks/index-DhaOVusv.esm.js.map +0 -1
- package/dist/decorator.d.ts +0 -106
- package/dist/decorator.d.ts.map +0 -1
- package/dist/destroyable.d.ts +0 -87
- package/dist/destroyable.d.ts.map +0 -1
- package/dist/entry-browser.d.ts +0 -3
- package/dist/entry-browser.d.ts.map +0 -1
- package/dist/entry-node.d.ts +0 -3
- package/dist/entry-node.d.ts.map +0 -1
- package/dist/eventful.d.ts +0 -18
- package/dist/eventful.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/indexable.d.ts +0 -243
- package/dist/indexable.d.ts.map +0 -1
- package/dist/introspection.d.ts +0 -27
- package/dist/introspection.d.ts.map +0 -1
- package/dist/iterableWeak.d.ts +0 -53
- package/dist/iterableWeak.d.ts.map +0 -1
- package/dist/mixins.d.ts +0 -25
- package/dist/mixins.d.ts.map +0 -1
- package/dist/promiseChain.d.ts +0 -20
- package/dist/promiseChain.d.ts.map +0 -1
- package/dist/reactive/array.d.ts +0 -49
- package/dist/reactive/array.d.ts.map +0 -1
- package/dist/reactive/buffer.d.ts +0 -44
- package/dist/reactive/buffer.d.ts.map +0 -1
- package/dist/reactive/change.d.ts +0 -29
- package/dist/reactive/change.d.ts.map +0 -1
- package/dist/reactive/debug.d.ts +0 -111
- package/dist/reactive/debug.d.ts.map +0 -1
- package/dist/reactive/deep-touch.d.ts +0 -28
- package/dist/reactive/deep-touch.d.ts.map +0 -1
- package/dist/reactive/deep-watch-state.d.ts +0 -25
- package/dist/reactive/deep-watch-state.d.ts.map +0 -1
- package/dist/reactive/deep-watch.d.ts +0 -19
- package/dist/reactive/deep-watch.d.ts.map +0 -1
- package/dist/reactive/effect-context.d.ts +0 -7
- package/dist/reactive/effect-context.d.ts.map +0 -1
- package/dist/reactive/effects.d.ts +0 -151
- package/dist/reactive/effects.d.ts.map +0 -1
- package/dist/reactive/index.d.ts +0 -20
- package/dist/reactive/index.d.ts.map +0 -1
- package/dist/reactive/interface.d.ts +0 -64
- package/dist/reactive/interface.d.ts.map +0 -1
- package/dist/reactive/map.d.ts +0 -30
- package/dist/reactive/map.d.ts.map +0 -1
- package/dist/reactive/memoize.d.ts +0 -5
- package/dist/reactive/memoize.d.ts.map +0 -1
- package/dist/reactive/non-reactive-state.d.ts +0 -9
- package/dist/reactive/non-reactive-state.d.ts.map +0 -1
- package/dist/reactive/non-reactive.d.ts +0 -11
- package/dist/reactive/non-reactive.d.ts.map +0 -1
- package/dist/reactive/project.d.ts +0 -41
- package/dist/reactive/project.d.ts.map +0 -1
- package/dist/reactive/proxy-state.d.ts +0 -8
- package/dist/reactive/proxy-state.d.ts.map +0 -1
- package/dist/reactive/proxy.d.ts +0 -23
- package/dist/reactive/proxy.d.ts.map +0 -1
- package/dist/reactive/record.d.ts +0 -116
- package/dist/reactive/record.d.ts.map +0 -1
- package/dist/reactive/register.d.ts +0 -64
- package/dist/reactive/register.d.ts.map +0 -1
- package/dist/reactive/registry.d.ts +0 -20
- package/dist/reactive/registry.d.ts.map +0 -1
- package/dist/reactive/set.d.ts +0 -28
- package/dist/reactive/set.d.ts.map +0 -1
- package/dist/reactive/tracking.d.ts +0 -7
- package/dist/reactive/tracking.d.ts.map +0 -1
- package/dist/reactive/types.d.ts.map +0 -1
- package/dist/std-decorators.d.ts +0 -50
- package/dist/std-decorators.d.ts.map +0 -1
- package/dist/utils.d.ts +0 -49
- package/dist/utils.d.ts.map +0 -1
- package/dist/zone.d.ts +0 -40
- package/dist/zone.d.ts.map +0 -1
- package/docs/reactive/project.md +0 -93
- package/src/async/browser.ts +0 -319
- package/src/async/index.ts +0 -23
- package/src/async/node.ts +0 -104
- 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 -77
- 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 -507
- package/src/reactive/buffer.ts +0 -168
- package/src/reactive/change.ts +0 -138
- package/src/reactive/debug.ts +0 -517
- package/src/reactive/deep-touch.ts +0 -268
- package/src/reactive/deep-watch-state.ts +0 -82
- package/src/reactive/deep-watch.ts +0 -168
- package/src/reactive/effect-context.ts +0 -18
- package/src/reactive/effects.ts +0 -1382
- package/src/reactive/index.ts +0 -75
- package/src/reactive/interface.ts +0 -210
- package/src/reactive/map.ts +0 -158
- package/src/reactive/memoize.ts +0 -163
- package/src/reactive/non-reactive-state.ts +0 -49
- package/src/reactive/non-reactive.ts +0 -43
- package/src/reactive/project.ts +0 -453
- package/src/reactive/proxy-state.ts +0 -27
- package/src/reactive/proxy.ts +0 -264
- package/src/reactive/record.ts +0 -196
- package/src/reactive/register.ts +0 -419
- package/src/reactive/registry.ts +0 -59
- package/src/reactive/set.ts +0 -130
- package/src/reactive/tracking.ts +0 -44
- package/src/reactive/types.ts +0 -440
- package/src/std-decorators.ts +0 -217
- package/src/utils.ts +0 -301
- package/src/zone.ts +0 -138
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Reactive Enumeration (`attend`)
|
|
2
|
+
|
|
3
|
+
The `attend` utility reactively iterates over the entries of a collection, running a per-key effect that is automatically created when a key appears and disposed when it disappears.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
`attend` is the reactive equivalent of `forEach`. It:
|
|
8
|
+
- Tracks the **keys** (or indices, or values) of a collection inside an outer effect.
|
|
9
|
+
- **Creates** an inner effect for each key, via `ascend`.
|
|
10
|
+
- **Disposes** the inner effect when the key is removed from the collection.
|
|
11
|
+
- Allows the callback to return a **cleanup function** (like a regular effect closer).
|
|
12
|
+
|
|
13
|
+
This is the foundational lifecycle primitive that `organized` is built on.
|
|
14
|
+
|
|
15
|
+
## API
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
// Raw enumeration callback
|
|
19
|
+
function attend<Key>(
|
|
20
|
+
enumerate: () => Iterable<Key>,
|
|
21
|
+
callback: (key: Key) => EffectCloser | void
|
|
22
|
+
): ScopedCallback
|
|
23
|
+
|
|
24
|
+
// Collection shorthands
|
|
25
|
+
function attend<T>(source: readonly T[], callback: (index: number) => EffectCloser | void): ScopedCallback
|
|
26
|
+
function attend<K, V>(source: Map<K, V>, callback: (key: K) => EffectCloser | void): ScopedCallback
|
|
27
|
+
function attend<T>(source: Set<T>, callback: (value: T) => EffectCloser | void): ScopedCallback
|
|
28
|
+
function attend<S extends Record<PropertyKey, any>>(source: S, callback: (key: keyof S & string) => EffectCloser | void): ScopedCallback
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Parameters
|
|
32
|
+
|
|
33
|
+
- **`source`** or **`enumerate`**: Either a collection (array, record, Map, Set) or a callback returning an `Iterable<Key>`. The enumeration runs inside the outer effect, so reactive reads (e.g. `source.length`, `Object.keys(source)`) are tracked automatically.
|
|
34
|
+
- **`callback`**: Called per key inside an inner effect. May return a cleanup function that runs when the key is removed or before the inner effect re-executes.
|
|
35
|
+
|
|
36
|
+
### Returns
|
|
37
|
+
|
|
38
|
+
A `ScopedCallback` that tears down all inner effects and the outer effect.
|
|
39
|
+
|
|
40
|
+
## Basic Usage
|
|
41
|
+
|
|
42
|
+
### Record
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { reactive, attend } from 'mutts'
|
|
46
|
+
|
|
47
|
+
const config = reactive({ theme: 'dark', lang: 'en' })
|
|
48
|
+
|
|
49
|
+
const stop = attend(config, (key) => {
|
|
50
|
+
console.log(`${key} = ${config[key]}`)
|
|
51
|
+
return () => console.log(`cleanup: ${key}`)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// Logs: "theme = dark", "lang = en"
|
|
55
|
+
|
|
56
|
+
config.debug = true
|
|
57
|
+
// Logs: "debug = true"
|
|
58
|
+
|
|
59
|
+
delete config.lang
|
|
60
|
+
// Logs: "cleanup: lang"
|
|
61
|
+
|
|
62
|
+
config.theme = 'light'
|
|
63
|
+
// Logs: "cleanup: theme" then "theme = light"
|
|
64
|
+
// (inner effect re-runs: previous cleanup fires, then new execution)
|
|
65
|
+
|
|
66
|
+
stop()
|
|
67
|
+
// Disposes everything
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Array
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
const items = reactive([10, 20, 30])
|
|
74
|
+
|
|
75
|
+
attend(items, (i) => {
|
|
76
|
+
console.log(`[${i}] = ${items[i]}`)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
items.push(40)
|
|
80
|
+
// Logs: "[3] = 40"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Map
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
const registry = reactive(new Map([['a', 1]]))
|
|
87
|
+
|
|
88
|
+
attend(registry, (key) => {
|
|
89
|
+
console.log(`${key} -> ${registry.get(key)}`)
|
|
90
|
+
return () => console.log(`removed: ${key}`)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
registry.set('b', 2) // Logs: "b -> 2"
|
|
94
|
+
registry.delete('a') // Logs: "removed: a"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Set
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
const tags = reactive(new Set(['alpha', 'beta']))
|
|
101
|
+
|
|
102
|
+
attend(tags, (value) => {
|
|
103
|
+
console.log(`tag: ${value}`)
|
|
104
|
+
return () => console.log(`untagged: ${value}`)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
tags.add('gamma') // Logs: "tag: gamma"
|
|
108
|
+
tags.delete('alpha') // Logs: "untagged: alpha"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Raw enumeration callback
|
|
112
|
+
|
|
113
|
+
For custom iteration logic or non-standard collections:
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
const source = reactive({ a: 1, b: 2 })
|
|
117
|
+
|
|
118
|
+
attend(
|
|
119
|
+
() => Reflect.ownKeys(source),
|
|
120
|
+
(key) => {
|
|
121
|
+
console.log(key, source[key])
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## How it Works
|
|
127
|
+
|
|
128
|
+
1. An **outer effect** calls `enumerate()` (or derives it from the collection type), collecting the current keys into a `Set`.
|
|
129
|
+
2. For each **new key**, `ascend` creates an inner effect that runs the callback.
|
|
130
|
+
3. For each **removed key**, the corresponding inner effect is stopped (which triggers its cleanup).
|
|
131
|
+
4. The **inner effect** tracks its own reactive dependencies — so if a value changes for an existing key, only that key's effect re-runs.
|
|
132
|
+
|
|
133
|
+
## Relationship to Other Primitives
|
|
134
|
+
|
|
135
|
+
| Primitive | Uses `attend`? | Purpose |
|
|
136
|
+
|---|---|---|
|
|
137
|
+
| `attend` | — | Reactive lifecycle per key |
|
|
138
|
+
| `organized` | ✅ | Reactive record mapping with access objects |
|
|
139
|
+
| `project` | ❌ | Reactive collection mapping (manages a target + projection context) |
|
|
140
|
+
| `scan` | ❌ | Reactive accumulation (sequential key dependency) |
|
|
141
|
+
|
|
142
|
+
`project` and `scan` have additional concerns (target management, sequential dependencies) that go beyond `attend`'s independent-key lifecycle model.
|
|
@@ -347,26 +347,81 @@ The `Register` exposes additional methods and behaviors that standard arrays do
|
|
|
347
347
|
- `toArray(): T[]` materializes the current values into a plain array.
|
|
348
348
|
- `toString(): string` returns a concise description like `[Register length=3]`.
|
|
349
349
|
|
|
350
|
+
### Register CRUD Events
|
|
351
|
+
|
|
352
|
+
`Register` emits lifecycle events for add, delete, update, and rekey operations. This enables side effects like logging, syncing with external systems, or triggering derived updates.
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
const list = register(({id}: { id: number }) => id)
|
|
356
|
+
|
|
357
|
+
// Listen to individual events
|
|
358
|
+
list.on('add', (item, key, index) => {
|
|
359
|
+
console.log(`Added ${key} at index ${index}:`, item)
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
list.on('delete', (item, key, index) => {
|
|
363
|
+
console.log(`Removed ${key} from index ${index}:`, item)
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
list.on('update', (oldItem, newItem, key, index) => {
|
|
367
|
+
console.log(`Updated ${key} at index ${index}`)
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
list.on('rekey', (item, oldKey, newKey, index) => {
|
|
371
|
+
console.log(`Key changed from ${oldKey} to ${newKey}`)
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
// Bulk event registration
|
|
375
|
+
list.on({
|
|
376
|
+
add: (item) => console.log('Added:', item),
|
|
377
|
+
delete: (item) => console.log('Deleted:', item),
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
// Global hook - receive all events
|
|
381
|
+
const unhook = list.hook((event, ...args) => {
|
|
382
|
+
console.log(`Event: ${String(event)}`, args)
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
// Unsubscribe
|
|
386
|
+
const unsubscribe = list.on('add', handler)
|
|
387
|
+
unsubscribe()
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
**Event Types:**
|
|
391
|
+
|
|
392
|
+
| Event | Arguments | Description |
|
|
393
|
+
|-------|-----------|-------------|
|
|
394
|
+
| `add` | `(item, key, index)` | New item added to register |
|
|
395
|
+
| `delete` | `(item, key, index)` | Item removed from register |
|
|
396
|
+
| `update` | `(oldItem, newItem, key, index)` | Item value updated (same key) |
|
|
397
|
+
| `rekey` | `(item, oldKey, newKey, index)` | Item's key changed |
|
|
398
|
+
|
|
399
|
+
**Use Cases:**
|
|
400
|
+
- Audit logging
|
|
401
|
+
- Syncing with databases
|
|
402
|
+
- Triggering notifications
|
|
403
|
+
- Cascading updates to dependent systems
|
|
404
|
+
|
|
350
405
|
Notes:
|
|
351
406
|
- Direct length modification via `list.length = n` is not supported; use `splice` instead.
|
|
352
407
|
- Assigning to an index (`list[i] = value`) uses the key function to bind that slot to `value`’s key.
|
|
353
408
|
|
|
354
409
|
## Class Reactivity
|
|
355
410
|
|
|
356
|
-
##
|
|
411
|
+
## Morphing
|
|
357
412
|
|
|
358
|
-
### `
|
|
413
|
+
### `morph()`
|
|
359
414
|
|
|
360
|
-
`
|
|
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.
|
|
361
416
|
|
|
362
417
|
#### Basic Usage
|
|
363
418
|
|
|
364
419
|
```typescript
|
|
365
|
-
import { cleanup,
|
|
420
|
+
import { cleanup, morph, reactive } from 'mutts/reactive'
|
|
366
421
|
|
|
367
422
|
// Arrays
|
|
368
423
|
const users = reactive([{ name: 'John', age: 30 }, { name: 'Jane', age: 25 }])
|
|
369
|
-
const names =
|
|
424
|
+
const names = morph(users, (user) => user.name.toUpperCase())
|
|
370
425
|
|
|
371
426
|
console.log(names) // ['JOHN', 'JANE']
|
|
372
427
|
|
|
@@ -375,8 +430,7 @@ console.log(names[0]) // 'JOHNNY' - only index 0 recomputed
|
|
|
375
430
|
|
|
376
431
|
// Records
|
|
377
432
|
const scores = reactive({ math: 90, science: 85 })
|
|
378
|
-
const grades =
|
|
379
|
-
const score = get()
|
|
433
|
+
const grades = morph(scores, (score) => {
|
|
380
434
|
return score >= 90 ? 'A' : score >= 80 ? 'B' : 'C'
|
|
381
435
|
})
|
|
382
436
|
|
|
@@ -389,7 +443,7 @@ const inventory = reactive(new Map([
|
|
|
389
443
|
['apples', { count: 10 }],
|
|
390
444
|
['oranges', { count: 5 }]
|
|
391
445
|
]))
|
|
392
|
-
const totals =
|
|
446
|
+
const totals = morph(inventory, (item) => item.count)
|
|
393
447
|
|
|
394
448
|
console.log(totals.get('apples')) // 10
|
|
395
449
|
inventory.get('apples')!.count = 15
|
|
@@ -398,41 +452,25 @@ console.log(totals.get('apples')) // 15 - only 'apples' key recomputed
|
|
|
398
452
|
|
|
399
453
|
#### Automatic Type Selection
|
|
400
454
|
|
|
401
|
-
|
|
455
|
+
The `morph()` function automatically selects the appropriate helper based on the source type:
|
|
402
456
|
|
|
403
457
|
```typescript
|
|
404
|
-
//
|
|
405
|
-
const doubled =
|
|
458
|
+
// Arrays
|
|
459
|
+
const doubled = morph([1, 2, 3], (x) => x * 2)
|
|
406
460
|
|
|
407
|
-
//
|
|
408
|
-
const upper =
|
|
461
|
+
// Records
|
|
462
|
+
const upper = morph({ a: 'hello', b: 'world' }, (s) => s.toUpperCase())
|
|
409
463
|
|
|
410
|
-
//
|
|
411
|
-
const counts =
|
|
464
|
+
// Maps
|
|
465
|
+
const counts = morph(new Map([['x', 1], ['y', 2]]), (v) => v * 2)
|
|
412
466
|
```
|
|
413
467
|
|
|
414
|
-
####
|
|
415
|
-
|
|
416
|
-
The callback receives a `ProjectAccess` object with:
|
|
468
|
+
#### Callback Signature
|
|
417
469
|
|
|
418
|
-
|
|
419
|
-
- **`set(value)`**: Function to update the source value (if the source is mutable)
|
|
420
|
-
- **`key`**: The current key or index
|
|
421
|
-
- **`source`**: Reference to the original source collection
|
|
422
|
-
- **`old`**: Previously computed result for this entry (undefined on first run)
|
|
423
|
-
- **`value`**: Computed property that mirrors `get()` (for convenience)
|
|
470
|
+
The callback function receives the current value of the entry being processed:
|
|
424
471
|
|
|
425
472
|
```typescript
|
|
426
|
-
const transformed =
|
|
427
|
-
// Access the source value
|
|
428
|
-
const item = access.get()
|
|
429
|
-
|
|
430
|
-
// Access the key/index
|
|
431
|
-
console.log(`Processing index ${access.key}`)
|
|
432
|
-
|
|
433
|
-
// Leverage previous result
|
|
434
|
-
console.log(`Previous result: ${access.old}`)
|
|
435
|
-
|
|
473
|
+
const transformed = morph(items, (item) => {
|
|
436
474
|
// Transform and return
|
|
437
475
|
return item.value * 2
|
|
438
476
|
})
|
|
@@ -450,9 +488,8 @@ const users = reactive([
|
|
|
450
488
|
])
|
|
451
489
|
|
|
452
490
|
let computeCount = 0
|
|
453
|
-
const summaries =
|
|
491
|
+
const summaries = morph(users, (user) => {
|
|
454
492
|
computeCount++
|
|
455
|
-
const user = get()
|
|
456
493
|
return `${user.name}: ${user.score}`
|
|
457
494
|
})
|
|
458
495
|
|
|
@@ -470,11 +507,11 @@ console.log(computeCount) // 5 (only new index 3 computed)
|
|
|
470
507
|
|
|
471
508
|
#### Key Addition and Removal
|
|
472
509
|
|
|
473
|
-
`
|
|
510
|
+
`morph()` automatically handles keys being added or removed from the source:
|
|
474
511
|
|
|
475
512
|
```typescript
|
|
476
513
|
const source = reactive({ a: 1, b: 2 })
|
|
477
|
-
const doubled =
|
|
514
|
+
const doubled = morph(source, (val) => val * 2)
|
|
478
515
|
|
|
479
516
|
console.log(doubled.a) // 2
|
|
480
517
|
console.log(doubled.b) // 4
|
|
@@ -493,7 +530,7 @@ console.log('a' in doubled) // false (automatically removed)
|
|
|
493
530
|
The returned object includes a `cleanup` symbol that stops all reactive effects:
|
|
494
531
|
|
|
495
532
|
```typescript
|
|
496
|
-
const result =
|
|
533
|
+
const result = morph(items, (item) => item * 2)
|
|
497
534
|
|
|
498
535
|
// Later, when done
|
|
499
536
|
result[cleanup]() // Stops all effects and cleans up
|
package/docs/reactive/core.md
CHANGED
|
@@ -7,19 +7,17 @@
|
|
|
7
7
|
- [5-Minute Quick Start](#5-minute-quick-start)
|
|
8
8
|
- [Core API](#core-api)
|
|
9
9
|
- [Effect System](#effect-system)
|
|
10
|
+
- [Error Handling](./error-handling.md)
|
|
10
11
|
- [Atomic Operations](./advanced.md#atomic-operations)
|
|
11
12
|
- [Advanced Effects](./advanced.md#advanced-effects)
|
|
12
13
|
- [Evolution Tracking](./advanced.md#evolution-tracking)
|
|
13
14
|
- [Prototype Chains and Pure Objects](./advanced.md#prototype-chains-and-pure-objects)
|
|
14
15
|
- [Recursive Touching](./advanced.md#recursive-touching)
|
|
15
|
-
|
|
16
|
-
- [
|
|
17
|
-
|
|
18
|
-
- [
|
|
19
|
-
- [
|
|
20
|
-
- [Array Mapping](./advanced.md#array-mapping)
|
|
21
|
-
- [Projection](./advanced.md#projection)
|
|
22
|
-
- [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)
|
|
23
21
|
- [Memoization](./advanced.md#memoization)
|
|
24
22
|
- [Debugging and Development](./advanced.md#debugging-and-development)
|
|
25
23
|
- [Cycle Detection](./advanced.md#cycle-detection)
|
|
@@ -105,7 +103,7 @@ const result = memoized(user)
|
|
|
105
103
|
**5. Map over arrays:**
|
|
106
104
|
```typescript
|
|
107
105
|
const source = reactive([1, 2, 3])
|
|
108
|
-
const doubled =
|
|
106
|
+
const doubled = morph(source, ({ value }) => value * 2)
|
|
109
107
|
// [2, 4, 6]
|
|
110
108
|
|
|
111
109
|
source.push(4) // doubled automatically becomes [2, 4, 6, 8]
|
|
@@ -202,7 +200,7 @@ reactiveObj.count = 5 // Triggers effect
|
|
|
202
200
|
- Objects with prototypes: `reactive(Object.create(parent))`
|
|
203
201
|
- Class instances: `reactive(new MyClass())`
|
|
204
202
|
|
|
205
|
-
See [Prototype Chains and Pure Objects](#prototype-chains-and-pure-objects) for detailed information about prototype chain handling.
|
|
203
|
+
See [Prototype Chains and Pure Objects](./advanced.md#prototype-chains-and-pure-objects) for detailed information about prototype chain handling.
|
|
206
204
|
|
|
207
205
|
### `effect()`
|
|
208
206
|
|
|
@@ -448,7 +446,7 @@ state.c = 15 // Does NOT trigger effect
|
|
|
448
446
|
|
|
449
447
|
The `effect` function provides a special `access` parameter with `tracked` and `ascend` functions that restore the active effect context for dependency tracking in asynchronous operations.
|
|
450
448
|
|
|
451
|
-
In modern `mutts`, this is powered by the **Zone system**. When
|
|
449
|
+
In modern `mutts`, this is powered by the **Zone system**. When zones are registered in `asyncZone`, the active effect context is automatically preserved across `await` points and timers, making manual use of `tracked` optional for these cases.
|
|
452
450
|
|
|
453
451
|
#### The Problem with Async Effects
|
|
454
452
|
|
|
@@ -466,7 +464,7 @@ effect(() => {
|
|
|
466
464
|
const another = state.name // ✅ Tracked (active effect is still set)
|
|
467
465
|
})
|
|
468
466
|
|
|
469
|
-
// Async effect WITHOUT
|
|
467
|
+
// Async effect WITHOUT zone registration - context is lost after await
|
|
470
468
|
effect(async () => {
|
|
471
469
|
const value = state.count // ✅ Tracked
|
|
472
470
|
|
|
@@ -476,7 +474,7 @@ effect(async () => {
|
|
|
476
474
|
const another = state.name // ❌ NOT tracked
|
|
477
475
|
})
|
|
478
476
|
|
|
479
|
-
// Async effect WITH
|
|
477
|
+
// Async effect WITH zone registration - context is preserved
|
|
480
478
|
effect(async () => {
|
|
481
479
|
const value = state.count // ✅ Tracked
|
|
482
480
|
|
|
@@ -497,7 +495,7 @@ effect(async ({ tracked }) => {
|
|
|
497
495
|
|
|
498
496
|
#### Key Benefits of the Zone System
|
|
499
497
|
|
|
500
|
-
1. **Automatic Restoration**: With `
|
|
498
|
+
1. **Automatic Restoration**: With zones registered in `asyncZone`, most native async APIs (Promises, timers) automatically preserve the reactive context.
|
|
501
499
|
2. **Manual Control**: `access.tracked()` allows you to manually "passport" the context into third-party libraries or unmanaged callbacks.
|
|
502
500
|
|
|
503
501
|
### Using `ascend` for Parent Effect Tracking
|
|
@@ -603,6 +601,94 @@ reactiveOptions.leave = (effect) => console.log('Leaving effect:', effect)
|
|
|
603
601
|
reactiveOptions.chain = (caller, target) => console.log('Chaining:', caller, '->', target)
|
|
604
602
|
```
|
|
605
603
|
|
|
604
|
+
### Effect Modifiers
|
|
605
|
+
|
|
606
|
+
The `effect` function provides convenient shortcut modifiers for common options. These can be chained for more concise syntax:
|
|
607
|
+
|
|
608
|
+
#### `.opaque`
|
|
609
|
+
|
|
610
|
+
Creates an opaque effect that tracks object references rather than deep content. This is useful when you want effects to re-run only when the object identity changes, not when its properties change.
|
|
611
|
+
|
|
612
|
+
```typescript
|
|
613
|
+
import { effect, reactive } from 'mutts/reactive'
|
|
614
|
+
|
|
615
|
+
const item = reactive({ id: 1, data: { value: 10 } })
|
|
616
|
+
|
|
617
|
+
// Regular effect - triggers on any property change
|
|
618
|
+
effect(() => {
|
|
619
|
+
console.log('Item data:', item.data.value) // Triggers on item.data.value changes
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
// Opaque effect - only triggers when item.data reference changes
|
|
623
|
+
effect.opaque(() => {
|
|
624
|
+
console.log('Data object:', item.data) // Only triggers when item.data is replaced
|
|
625
|
+
})
|
|
626
|
+
|
|
627
|
+
item.data.value = 20 // Triggers regular effect, NOT opaque effect
|
|
628
|
+
item.data = { value: 30 } // Triggers BOTH effects
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
**Use cases for opaque effects:**
|
|
632
|
+
- When you only care about object identity (e.g., cache keys, memoization)
|
|
633
|
+
- When deep watching would be too expensive
|
|
634
|
+
- When working with external data that shouldn't trigger deep reactivity
|
|
635
|
+
|
|
636
|
+
#### `.named(name)`
|
|
637
|
+
|
|
638
|
+
Creates a named effect for easier debugging and profiling. The name appears in DevTools and debug logs.
|
|
639
|
+
|
|
640
|
+
```typescript
|
|
641
|
+
import { effect, reactive } from 'mutts/reactive'
|
|
642
|
+
|
|
643
|
+
const state = reactive({ count: 0 })
|
|
644
|
+
|
|
645
|
+
// Create a named effect
|
|
646
|
+
effect.named('counter-effect')(() => {
|
|
647
|
+
console.log('Count:', state.count)
|
|
648
|
+
})
|
|
649
|
+
|
|
650
|
+
// Named effects can also be combined with other options
|
|
651
|
+
effect.named('data-loader').opaque(() => {
|
|
652
|
+
console.log('Loading data...')
|
|
653
|
+
})
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
**Benefits of named effects:**
|
|
657
|
+
- Easier identification in DevTools
|
|
658
|
+
- Better stack traces during debugging
|
|
659
|
+
- Helpful for performance profiling
|
|
660
|
+
|
|
661
|
+
#### Combining Modifiers
|
|
662
|
+
|
|
663
|
+
Modifiers can be chained in any order:
|
|
664
|
+
|
|
665
|
+
```typescript
|
|
666
|
+
// Named opaque effect
|
|
667
|
+
effect.named('my-effect').opaque(() => {
|
|
668
|
+
// Effect code
|
|
669
|
+
})
|
|
670
|
+
|
|
671
|
+
// These are equivalent - order doesn't matter for the result
|
|
672
|
+
effect.opaque.named('my-effect')(() => {
|
|
673
|
+
// Effect code
|
|
674
|
+
})
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
Note: The modifiers return new effect functions with the options pre-applied, so they can be stored and reused:
|
|
678
|
+
|
|
679
|
+
```typescript
|
|
680
|
+
// Create a reusable named effect factory
|
|
681
|
+
const createDataEffect = effect.named('data-layer')
|
|
682
|
+
|
|
683
|
+
createDataEffect(() => {
|
|
684
|
+
console.log('Effect 1')
|
|
685
|
+
})
|
|
686
|
+
|
|
687
|
+
createDataEffect(() => {
|
|
688
|
+
console.log('Effect 2')
|
|
689
|
+
})
|
|
690
|
+
```
|
|
691
|
+
|
|
606
692
|
|
|
607
693
|
### `@reactive` Decorator
|
|
608
694
|
|