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
package/docs/flavored.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# Flavored Functions
|
|
2
|
+
|
|
3
|
+
The `flavored` utility creates extensible functions with chainable property modifiers. It enables a fluent API where properties return specialized variants of the base function.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Flavored functions allow you to:
|
|
8
|
+
- Create property-based modifiers that transform how the function is called
|
|
9
|
+
- Chain multiple modifiers together
|
|
10
|
+
- Use either automatic option merging (`flavorOptions`) or custom argument transformation (`createFlavor`)
|
|
11
|
+
- Return hand-made functions for complete control (the generic case)
|
|
12
|
+
|
|
13
|
+
## Basic Usage
|
|
14
|
+
|
|
15
|
+
### Creating a Flavored Function
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { flavored, flavorOptions } from 'mutts'
|
|
19
|
+
|
|
20
|
+
const greet = flavored(
|
|
21
|
+
(name: string, options?: { loud?: boolean }) => {
|
|
22
|
+
const greeting = `Hello, ${name}!`
|
|
23
|
+
return options?.loud ? greeting.toUpperCase() : greeting
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
get loud() {
|
|
27
|
+
return flavorOptions(this, { loud: true })
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
greet('World') // "Hello, World!"
|
|
33
|
+
greet.loud('World') // "HELLO, WORLD!"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## API Reference
|
|
37
|
+
|
|
38
|
+
### `flavored(fn, flavors)`
|
|
39
|
+
|
|
40
|
+
Creates a flavored function with chainable property modifiers.
|
|
41
|
+
|
|
42
|
+
**Parameters:**
|
|
43
|
+
- `fn` - The base function to flavor
|
|
44
|
+
- `flavors` - Object defining the flavor properties (getters or methods)
|
|
45
|
+
|
|
46
|
+
**Returns:** A proxy of the function with the flavor properties attached.
|
|
47
|
+
|
|
48
|
+
**Example:**
|
|
49
|
+
```typescript
|
|
50
|
+
const effect = flavored(baseEffect, {
|
|
51
|
+
get opaque() {
|
|
52
|
+
return flavorOptions(this, { opaque: true })
|
|
53
|
+
},
|
|
54
|
+
named(name: string) {
|
|
55
|
+
return flavorOptions(this, { name })
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
effect(fn) // Basic usage
|
|
60
|
+
effect.opaque(fn) // With opaque option
|
|
61
|
+
effect.named('myEffect')(fn) // With name option
|
|
62
|
+
effect.opaque.named('x')(fn) // Chained
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### `flavorOptions(fn, defaultOptions)`
|
|
66
|
+
|
|
67
|
+
Creates a flavored variant that merges options with the last argument.
|
|
68
|
+
|
|
69
|
+
**Use when:** The last argument is an options object that should be merged.
|
|
70
|
+
|
|
71
|
+
**Parameters:**
|
|
72
|
+
- `fn` - The base flavored function
|
|
73
|
+
- `defaultOptions` - Options to merge with provided options
|
|
74
|
+
|
|
75
|
+
**Example:**
|
|
76
|
+
```typescript
|
|
77
|
+
const loudGreet = flavorOptions(greet, { loud: true })
|
|
78
|
+
loudGreet('World') // Uses { loud: true }
|
|
79
|
+
loudGreet('World', { prefix: 'Hi' }) // Merges both options
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### `createFlavor(fn, transform)`
|
|
83
|
+
|
|
84
|
+
Creates a flavored variant with custom argument transformation.
|
|
85
|
+
|
|
86
|
+
**Use when:** You need to transform arguments before calling the base function.
|
|
87
|
+
|
|
88
|
+
**Parameters:**
|
|
89
|
+
- `fn` - The base flavored function
|
|
90
|
+
- `transform` - Function that receives and returns the arguments tuple
|
|
91
|
+
|
|
92
|
+
**Example:**
|
|
93
|
+
```typescript
|
|
94
|
+
const doubleArgs = createFlavor(add, (a: number, b: number): [number, number] => {
|
|
95
|
+
return [a * 2, b * 2]
|
|
96
|
+
})
|
|
97
|
+
doubleArgs(3, 4) // Returns 14 (6 + 8)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## The Generic Case: Hand-Made Functions
|
|
101
|
+
|
|
102
|
+
When `flavorOptions` or `createFlavor` don't fit your needs, you can return a **hand-made function** directly. This gives you complete control over the behavior.
|
|
103
|
+
|
|
104
|
+
### Basic Hand-Made Function
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
const fetch = flavored(fetchData, {
|
|
108
|
+
// Returns a completely custom function
|
|
109
|
+
withTimeout(timeout: number) {
|
|
110
|
+
return (url: string, options?: { retries?: number }) => {
|
|
111
|
+
return fetchData(url, { ...options, timeout })
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
// Usage
|
|
117
|
+
const fetchWith500ms = fetch.withTimeout(500)
|
|
118
|
+
fetchWith500ms('api.com') // Uses timeout: 500
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Re-Flavoring Hand-Made Functions
|
|
122
|
+
|
|
123
|
+
Hand-made functions can be re-flavored to enable further chaining:
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
const process = flavored(baseProcess, {
|
|
127
|
+
presetMultiplier(multiplier: number) {
|
|
128
|
+
// Create the preset function
|
|
129
|
+
const presetFn = (value: number, options?: { offset?: number }) => {
|
|
130
|
+
return baseProcess(value, { ...options, multiplier })
|
|
131
|
+
}
|
|
132
|
+
// Re-flavor it to enable chaining
|
|
133
|
+
return flavored(presetFn, {
|
|
134
|
+
get withOffset() {
|
|
135
|
+
return flavorOptions(this, { offset: 10 })
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
// Usage
|
|
142
|
+
const times3 = process.presetMultiplier(3)
|
|
143
|
+
times3(5) // 15 (5 * 3)
|
|
144
|
+
times3.withOffset(5) // 20 (5 * 3 + 5)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### When to Use Each Approach
|
|
148
|
+
|
|
149
|
+
| Approach | Use When |
|
|
150
|
+
|----------|----------|
|
|
151
|
+
| `flavorOptions` | You have an options object as the last argument |
|
|
152
|
+
| `createFlavor` | You need to transform arguments structurally |
|
|
153
|
+
| **Hand-made function** | You need complete control, different signature, or side effects |
|
|
154
|
+
|
|
155
|
+
## Advanced Patterns
|
|
156
|
+
|
|
157
|
+
### Chaining Multiple Modifiers
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
const createUser = flavored(createUserBase, {
|
|
161
|
+
get admin() {
|
|
162
|
+
return flavorOptions(this, { admin: true })
|
|
163
|
+
},
|
|
164
|
+
get verified() {
|
|
165
|
+
return flavorOptions(this, { verified: true })
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
createUser.admin.verified('Alice') // Both options applied
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Combining Approaches
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
const calculator = flavored(
|
|
176
|
+
(a: number, b: number, opts?: { multiply?: boolean }) => {
|
|
177
|
+
return opts?.multiply ? a * b : a + b
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
get multiply() {
|
|
181
|
+
return flavorOptions(this, { multiply: true })
|
|
182
|
+
},
|
|
183
|
+
double() {
|
|
184
|
+
return createFlavor(this, (a, b, opts?): [number, number, typeof opts] => {
|
|
185
|
+
return [a * 2, b * 2, opts]
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
calculator.multiply(3, 4) // 12
|
|
192
|
+
calculator.double()(3, 4) // 14 (6 + 8)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## TypeScript Considerations
|
|
196
|
+
|
|
197
|
+
Flavored functions use proxies and require type assertions for complex chaining scenarios:
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
const effectWithModifiers = flavored(effect, {
|
|
201
|
+
get opaque(): EffectWithModifiers {
|
|
202
|
+
return flavorOptions(this, { opaque: true }) as EffectWithModifiers
|
|
203
|
+
},
|
|
204
|
+
named(name: string): EffectWithModifiers {
|
|
205
|
+
return flavorOptions(this, { name }) as EffectWithModifiers
|
|
206
|
+
}
|
|
207
|
+
})
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
The `as EffectWithModifiers` cast enables proper type inference for chained modifiers.
|
|
@@ -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
|
|
@@ -1169,8 +1479,7 @@ For a full guide on debugging, including cycle detection and memoization discrep
|
|
|
1169
1479
|
|
|
1170
1480
|
### Quick Summary
|
|
1171
1481
|
|
|
1172
|
-
- **Cycle Detection**: Automatically catch circular dependencies via `reactiveOptions.cycleHandling`. Note: Instant mathematical detection requires choosing
|
|
1173
|
-
- **
|
|
1482
|
+
- **Cycle Detection**: Automatically catch circular dependencies via `reactiveOptions.cycleHandling`. Note: Instant mathematical detection requires choosing `'development'` or `'debug'` mode.
|
|
1483
|
+
- **Production Mode**: The default `reactiveOptions.cycleHandling` is set to `'production'`, providing maximum performance in high-frequency update scenarios by disabling graph maintenance.
|
|
1174
1484
|
- **Memoization Discrepancy**: Detect "missing dependencies" by running computations twice during development using `reactiveOptions.onMemoizationDiscrepancy`.
|
|
1175
1485
|
- **Global Hooks**: Use `reactiveOptions.touched`, `enter`, and `leave` to observe system activity.
|
|
1176
|
-
|