mutts 1.0.12 → 1.0.14
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/BROWSER_ASYNC_POLYFILL.md +79 -0
- package/README.md +7 -4
- package/dist/browser.cjs +150 -27
- package/dist/browser.cjs.map +1 -1
- package/dist/browser.d.ts +1440 -2
- package/dist/browser.dev.cjs +17 -3
- package/dist/browser.dev.cjs.map +1 -1
- package/dist/browser.dev.d.ts +2 -2
- package/dist/browser.dev.esm.js +2 -2
- package/dist/browser.esm.js +137 -28
- package/dist/browser.esm.js.map +1 -1
- package/dist/chunks/{index-yK0HVxHv.cjs → index-BnTNC9eC.cjs} +347 -156
- package/dist/chunks/index-BnTNC9eC.cjs.map +1 -0
- package/dist/chunks/{index-BUop6B2U.esm.js → index-CAWVZL7P.esm.js} +345 -154
- package/dist/chunks/index-CAWVZL7P.esm.js.map +1 -0
- package/dist/chunks/node-Df_5r_WA.cjs +187 -0
- package/dist/chunks/node-Df_5r_WA.cjs.map +1 -0
- package/dist/chunks/node-DuIduHw3.esm.js +185 -0
- package/dist/chunks/node-DuIduHw3.esm.js.map +1 -0
- package/dist/chunks/{proxy-D2C49sXH.esm.js → proxy-C2lnvvbx.esm.js} +943 -272
- package/dist/chunks/proxy-C2lnvvbx.esm.js.map +1 -0
- package/dist/chunks/{proxy-BvM4yewA.cjs → proxy-HA_QQnd5.cjs} +959 -273
- package/dist/chunks/proxy-HA_QQnd5.cjs.map +1 -0
- package/dist/debug.cjs +571 -173
- package/dist/debug.cjs.map +1 -1
- package/dist/debug.d.ts +96 -80
- package/dist/debug.esm.js +567 -173
- package/dist/debug.esm.js.map +1 -1
- package/dist/devtools/panel.js.map +1 -1
- package/dist/mutts.umd.js +4351 -3366
- 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 +18 -4
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.ts +2 -2
- package/dist/node.dev.cjs +18 -4
- package/dist/node.dev.cjs.map +1 -1
- package/dist/node.dev.d.ts +2 -2
- package/dist/node.dev.esm.js +3 -3
- package/dist/node.esm.js +3 -3
- package/dist/{types-Bx2PhORg.d.ts → types.d.ts} +42 -15
- package/docs/ai/api-reference.md +105 -13
- package/docs/ai/manual.md +77 -29
- package/docs/debug-getReason.md +161 -0
- package/docs/flavored.md +98 -1
- package/docs/reactive/advanced.md +184 -12
- package/docs/reactive/attend.md +32 -0
- package/docs/reactive/core.md +40 -6
- package/docs/reactive/debugging.md +40 -15
- package/docs/reactive.md +4 -1
- package/package.json +13 -9
- package/dist/chunks/index-BUop6B2U.esm.js.map +0 -1
- package/dist/chunks/index-yK0HVxHv.cjs.map +0 -1
- package/dist/chunks/node-Bo7WU5S2.esm.js +0 -96
- package/dist/chunks/node-Bo7WU5S2.esm.js.map +0 -1
- package/dist/chunks/node-Dd0esp5F.cjs +0 -98
- package/dist/chunks/node-Dd0esp5F.cjs.map +0 -1
- package/dist/chunks/proxy-BvM4yewA.cjs.map +0 -1
- package/dist/chunks/proxy-D2C49sXH.esm.js.map +0 -1
- package/dist/index.d.ts +0 -1322
package/docs/ai/manual.md
CHANGED
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
|
|
7
7
|
## 1. MENTAL MODEL
|
|
8
8
|
|
|
9
|
-
Proxy-based **fine-grained reactivity** — changes propagate synchronously through dependency-tracked effects. No VDOM, no scheduler, no hooks.
|
|
9
|
+
Proxy-based **fine-grained reactivity** — changes propagate synchronously through dependency-tracked effects. No VDOM, no async render scheduler, no hooks.
|
|
10
10
|
|
|
11
11
|
| Concept | Mutts |
|
|
12
12
|
|---------|-------|
|
|
13
13
|
| State | `reactive(obj)` — proxy wraps, tracks reads/writes |
|
|
14
14
|
| Reactions | `effect(() => {})` — auto-tracks deps, re-runs on change |
|
|
15
15
|
| Computed | `memoize(fn)` — cached, invalidates on tracked deps |
|
|
16
|
-
| Collections | `
|
|
16
|
+
| Collections | `morph(arr, fn)` — per-entry reactive transform |
|
|
17
17
|
| Async context | `Zone` + `asyncZone` — propagates across await |
|
|
18
18
|
| Batching | `atomic(() => {})` wraps, `atom(() => {})` runs immediately — fire effects once after all mutations |
|
|
19
19
|
| Cleanup | Return fn from effect — runs before re-run/disposal |
|
|
@@ -22,11 +22,11 @@ Proxy-based **fine-grained reactivity** — changes propagate synchronously thro
|
|
|
22
22
|
|
|
23
23
|
```ts
|
|
24
24
|
import { reactive, effect, memoize, morph, attend, lift, cleanup,
|
|
25
|
-
atom, atomic, defer, untracked, unreactive, watch, when, biDi, caught,
|
|
26
|
-
|
|
25
|
+
atom, atomic, defer, untracked, unreactive, watch, when, biDi, caught,
|
|
26
|
+
organized, organize, resource, root,
|
|
27
27
|
Zone, asyncZone, ZoneHistory, ZoneAggregator,
|
|
28
28
|
decorator, mixin, Eventful, Destroyable, flavored, Indexable, chainPromise,
|
|
29
|
-
reactiveOptions, isReactive, unwrap, getState
|
|
29
|
+
reactiveOptions, isReactive, unwrap, getState, ReactiveBase
|
|
30
30
|
} from 'mutts'
|
|
31
31
|
```
|
|
32
32
|
|
|
@@ -107,14 +107,14 @@ array.splice(0) // also works — explicit clear
|
|
|
107
107
|
```
|
|
108
108
|
|
|
109
109
|
### TRAP 6: Using .map() for reactive transforms
|
|
110
|
-
`.map()` is static — full rebuild on any change. Use `
|
|
110
|
+
`.map()` is static — full rebuild on any change. Use `morph()` for per-entry reactivity.
|
|
111
111
|
|
|
112
112
|
```ts
|
|
113
113
|
// BAD — full rebuild
|
|
114
114
|
const doubled = items.map(x => x.value * 2)
|
|
115
115
|
|
|
116
116
|
// GOOD — per-entry effects
|
|
117
|
-
const doubled =
|
|
117
|
+
const doubled = morph(items, (value) => value * 2)
|
|
118
118
|
```
|
|
119
119
|
|
|
120
120
|
### TRAP 7: Deep watch overhead
|
|
@@ -164,7 +164,11 @@ state.count++ // triggers
|
|
|
164
164
|
stop() // disposes
|
|
165
165
|
```
|
|
166
166
|
|
|
167
|
-
- **Access object**: `{ reaction, tracked(fn), ascend(fn)
|
|
167
|
+
- **Access object**: `{ reaction, tracked(fn), ascend(fn), signal }`
|
|
168
|
+
- `reaction` is `false` on first run, then `true` or a `CleanupReason` on re-runs.
|
|
169
|
+
- `tracked` restores effect tracking in async or escaped callbacks.
|
|
170
|
+
- `ascend` records dependencies on the parent effect context.
|
|
171
|
+
- `signal` aborts when the effect is cleaned up or invalidated.
|
|
168
172
|
- **Parent-child**: effects inside effects are children — parent disposal cascades.
|
|
169
173
|
- **GC**: unreferenced top-level effects may GC — store `stop` to keep alive.
|
|
170
174
|
- **Modifiers**: `effect.opaque(() => {})` (identity-only), `effect.named('x')(() => {})` (debug label). Chainable.
|
|
@@ -198,6 +202,8 @@ const updateBoth = atomic((a, b) => { state.a = a; state.b = b }) // effects fir
|
|
|
198
202
|
// Decorator: @atomic on methods
|
|
199
203
|
```
|
|
200
204
|
|
|
205
|
+
`atomic()` wraps for later invocation but still executes synchronously when called. Internally it behaves like `batch(fn, { immediate: true })`, so nested atomic calls join the active batch by default instead of creating an implicit child batch.
|
|
206
|
+
|
|
201
207
|
### 3.7 atom() — Immediate atomic execution
|
|
202
208
|
|
|
203
209
|
```ts
|
|
@@ -205,6 +211,16 @@ atom(() => { state.a = 1; state.b = 2 }) // runs now, effects fire once
|
|
|
205
211
|
// Unlike atomic() which wraps for later, atom() executes immediately
|
|
206
212
|
```
|
|
207
213
|
|
|
214
|
+
The explicit batching API is now:
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
batch(fnOrEffects, { immediate?: boolean, contained?: boolean, caller?: EffectTrigger })
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
- `batch(fn)` joins the current batch when nested
|
|
221
|
+
- `batch(fn, { immediate: true })` runs `fn` now and queues its consequences into the active batch
|
|
222
|
+
- `batch(fn, { contained: true })` creates an isolated nested batch that flushes before returning
|
|
223
|
+
|
|
208
224
|
### 3.8 defer() — Avoid cycles
|
|
209
225
|
|
|
210
226
|
```ts
|
|
@@ -231,16 +247,15 @@ input.addEventListener('input', () => provide(input.value))
|
|
|
231
247
|
```ts
|
|
232
248
|
watch(() => state.count, (newVal, oldVal) => {}) // specific derivation
|
|
233
249
|
watch(state, () => {}) // any property
|
|
234
|
-
watch(state, () => {}
|
|
250
|
+
watch.deep(state, () => {}) // deep (expensive)
|
|
251
|
+
watch.immediate(() => state.count, (newVal, oldVal) => {}) // immediate first call
|
|
235
252
|
```
|
|
236
253
|
|
|
237
|
-
|
|
254
|
+
`watch` has 3 useful shapes:
|
|
238
255
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
// Chains: if obj already has a cleanup, both run
|
|
243
|
-
```
|
|
256
|
+
- `watch(() => value, callback)`
|
|
257
|
+
- `watch.deep(() => object, callback)`
|
|
258
|
+
- `watch(object, callback)`
|
|
244
259
|
|
|
245
260
|
### 3.11 Lazy computed values
|
|
246
261
|
|
|
@@ -258,9 +273,25 @@ effect(() => console.log(d.value)) // recomputes immediately, .value is reactive
|
|
|
258
273
|
|
|
259
274
|
```ts
|
|
260
275
|
await when(() => state.loaded) // resolves when truthy
|
|
261
|
-
await when(() => state.ready,
|
|
276
|
+
await when(() => state.ready, 5000) // rejects after 5s
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### 3.13 resource() — Async reactive state
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
const user = resource(async () => fetchUser(state.userId), { initialValue: null })
|
|
283
|
+
|
|
284
|
+
effect(() => {
|
|
285
|
+
if (user.loading) return
|
|
286
|
+
console.log(user.value)
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
user.reload()
|
|
290
|
+
await user.promise
|
|
262
291
|
```
|
|
263
292
|
|
|
293
|
+
`resource()` returns `{ value, loading, error, latest, reload(), promise }`.
|
|
294
|
+
|
|
264
295
|
---
|
|
265
296
|
|
|
266
297
|
## 4. COLLECTIONS
|
|
@@ -280,15 +311,22 @@ Array methods (`push`, `splice`, `sort`, etc.) all trigger reactivity.
|
|
|
280
311
|
|
|
281
312
|
All transforms return reactive results. Cleanup via `result[cleanup]()`.
|
|
282
313
|
|
|
283
|
-
### 5.1
|
|
314
|
+
### 5.1 morph() — Per-entry reactive map
|
|
284
315
|
|
|
285
316
|
```ts
|
|
286
|
-
const names =
|
|
317
|
+
const names = morph(users, (user) => user.name.toUpperCase())
|
|
287
318
|
// names[0] recomputes ONLY when users[0] changes
|
|
288
|
-
//
|
|
319
|
+
// Use morph.pure(...) when the callback has no reactive reads
|
|
289
320
|
```
|
|
290
321
|
|
|
291
|
-
|
|
322
|
+
For arrays the callback is `(value, access?) => mapped`.
|
|
323
|
+
For Maps and records it also receives the key.
|
|
324
|
+
|
|
325
|
+
The mapper can also be captioned, even though it is the second argument:
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
const names = morph`user:names`(users, (user) => user.name.toUpperCase())
|
|
329
|
+
```
|
|
292
330
|
|
|
293
331
|
### 5.2 attend() — Per-key lifecycle
|
|
294
332
|
|
|
@@ -297,9 +335,17 @@ attend(reactiveRecord, (key) => {
|
|
|
297
335
|
console.log(`${key} = ${reactiveRecord[key]}`)
|
|
298
336
|
return () => console.log(`cleanup: ${key}`) // disposed when key disappears
|
|
299
337
|
})
|
|
300
|
-
// Works with: arrays, Maps, Sets, or raw () => Iterable<Key>
|
|
301
338
|
```
|
|
302
339
|
|
|
340
|
+
`attend` also supports callback captions on its second argument:
|
|
341
|
+
|
|
342
|
+
```ts
|
|
343
|
+
attend`record:entries`(reactiveRecord, (key) => {
|
|
344
|
+
console.log(`${key} = ${reactiveRecord[key]}`)
|
|
345
|
+
})
|
|
346
|
+
```
|
|
347
|
+
Works with: arrays, Maps, Sets, or raw `() => Iterable<Key>`.
|
|
348
|
+
|
|
303
349
|
### 5.3 organized() — Per-key record transform
|
|
304
350
|
|
|
305
351
|
```ts
|
|
@@ -309,7 +355,7 @@ const doubled = organized(source, (access, target) => {
|
|
|
309
355
|
})
|
|
310
356
|
```
|
|
311
357
|
|
|
312
|
-
### 5.
|
|
358
|
+
### 5.4 lift() — Sync a computed array/object
|
|
313
359
|
|
|
314
360
|
```ts
|
|
315
361
|
const filtered = lift(() => items.filter(x => x.active))
|
|
@@ -318,7 +364,7 @@ const filtered = lift(() => items.filter(x => x.active))
|
|
|
318
364
|
|
|
319
365
|
**lift vs deep touching**: Deep touching handles `state.items = newArray` (replacement diffs). `lift` is for **derived collections** (filter/map/reshape) where there's no single assignment — the output is recomputed from scratch.
|
|
320
366
|
|
|
321
|
-
**lift vs memoize**: `memoize` is lazy (invalidate → recompute on next read), returns raw values, keyed by args. `lift` is eager (recompute immediately), returns a **stable reactive proxy** with per-element diffing. Use `lift` for derived collections consumed by
|
|
367
|
+
**lift vs memoize**: `memoize` is lazy (invalidate → recompute on next read), returns raw values, keyed by args. `lift` is eager (recompute immediately), returns a **stable reactive proxy** with per-element diffing. Use `lift` for derived collections consumed by effects; use `memoize` for parameterized caching or lazy evaluation.
|
|
322
368
|
|
|
323
369
|
---
|
|
324
370
|
|
|
@@ -362,10 +408,10 @@ effect(() => {
|
|
|
362
408
|
```ts
|
|
363
409
|
import { reactiveOptions } from 'mutts'
|
|
364
410
|
|
|
365
|
-
//
|
|
366
|
-
reactiveOptions.
|
|
367
|
-
reactiveOptions.
|
|
368
|
-
reactiveOptions.
|
|
411
|
+
// Scheduler / cycle detection
|
|
412
|
+
reactiveOptions.scheduler = 'ordered' // default: causal graph ordering, eager cycles
|
|
413
|
+
reactiveOptions.scheduler = 'raw' // fastest FIFO mode, heuristic cycles
|
|
414
|
+
reactiveOptions.scheduler = 'debug' // ordered mode plus heavier diagnostics
|
|
369
415
|
|
|
370
416
|
// Lifecycle hooks (all wrapped via optionCall for safety)
|
|
371
417
|
reactiveOptions.enter = (effect) => {} // before effect runs
|
|
@@ -387,6 +433,8 @@ const graph = buildReactivityGraph()
|
|
|
387
433
|
|
|
388
434
|
**ReactiveError.debugInfo**: `causalChain`, `creationStack`, `cycle` (for CYCLE_DETECTED).
|
|
389
435
|
|
|
436
|
+
`onEffectThrow` is still exported as a deprecated alias of `caught`.
|
|
437
|
+
|
|
390
438
|
---
|
|
391
439
|
|
|
392
440
|
## 9. ZONES (Async Context)
|
|
@@ -484,7 +532,7 @@ const theme = await chainPromise(api.getUser('123')).getProfile().getSettings().
|
|
|
484
532
|
## 11. PHILOSOPHY
|
|
485
533
|
|
|
486
534
|
- **Affirmative state**: Declare `Y = f(X)`. Don't say "when X changes, update Y".
|
|
487
|
-
- **Events are legacy**: Use reactive derivations (`effect`, `memoize`, `
|
|
535
|
+
- **Events are legacy**: Use reactive derivations (`effect`, `memoize`, `morph`) for internal logic. Events only for DOM/external APIs.
|
|
488
536
|
- **Cleanup ≠ undo**: Cleanup releases subscriptions, does NOT undo side effects.
|
|
489
537
|
|
|
490
538
|
---
|
|
@@ -504,5 +552,5 @@ const theme = await chainPromise(api.getUser('123')).getProfile().getSettings().
|
|
|
504
552
|
| `isReactive(obj)` to check | `obj._mutts_isReactive` |
|
|
505
553
|
| Store `stop = effect(...)` | letting effect GC unintentionally |
|
|
506
554
|
| `import { x } from 'mutts'` | `import { x } from 'mutts/reactive'` (no subpaths) |
|
|
507
|
-
| `
|
|
555
|
+
| `caught(handler)` for effect-local error handling | ad-hoc try/catch around reactive graph edges |
|
|
508
556
|
| `memoize(() => a + b)` for computed | `effect` + manual state sync |
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# `__MUTTS_DEBUG__.getReason()`
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The `getReason()` function allows you to access the reason why the current effect is being executed from within the effect itself. This is useful for debugging and understanding what triggered an effect to re-run.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import * as mutts from 'mutts'
|
|
11
|
+
|
|
12
|
+
// DevTools are automatically enabled in development mode
|
|
13
|
+
const debug = (globalThis as any).__MUTTS_DEBUG__
|
|
14
|
+
|
|
15
|
+
const state = mutts.reactive({ count: 0 })
|
|
16
|
+
|
|
17
|
+
mutts.effect(() => {
|
|
18
|
+
const reason = debug.getReason()
|
|
19
|
+
|
|
20
|
+
if (!reason) {
|
|
21
|
+
console.log('First run - no reason')
|
|
22
|
+
} else if (reason.type === 'propChange') {
|
|
23
|
+
console.log(`Re-run due to ${reason.triggers.length} property changes:`)
|
|
24
|
+
for (const trigger of reason.triggers) {
|
|
25
|
+
console.log(` - ${trigger.evolution.type} on ${trigger.evolution.prop}`)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
console.log('Count:', state.count)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// Trigger changes
|
|
33
|
+
state.count = 1 // Shows propChange reason
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Return Value
|
|
37
|
+
|
|
38
|
+
Returns `undefined` or a `CleanupReason` object:
|
|
39
|
+
|
|
40
|
+
### `undefined`
|
|
41
|
+
- Returned on the first run of an effect
|
|
42
|
+
- No cleanup/reason is available for initial execution
|
|
43
|
+
|
|
44
|
+
### `CleanupReason` types
|
|
45
|
+
|
|
46
|
+
#### `{ type: 'propChange', triggers: PropTrigger[] }`
|
|
47
|
+
The effect was re-run because one or more reactive properties changed.
|
|
48
|
+
|
|
49
|
+
- `triggers`: Array of property changes that triggered this re-run
|
|
50
|
+
- `obj`: The reactive object that changed
|
|
51
|
+
- `evolution`: Details about what changed
|
|
52
|
+
- `type`: `'set' | 'del' | 'add' | 'invalidate' | 'bunch'`
|
|
53
|
+
- `prop`: The property that changed (or method name for 'bunch')
|
|
54
|
+
- `dependency`: Stack trace from when the dependency was created (if lineage tracking enabled)
|
|
55
|
+
- `touch`: Stack trace from when the property was modified (if lineage tracking enabled)
|
|
56
|
+
|
|
57
|
+
#### `{ type: 'stopped', detail?: string }`
|
|
58
|
+
The effect was explicitly stopped via its cleanup function.
|
|
59
|
+
|
|
60
|
+
#### `{ type: 'gc' }`
|
|
61
|
+
The effect was cleaned up by garbage collection.
|
|
62
|
+
|
|
63
|
+
#### `{ type: 'error', error: unknown }`
|
|
64
|
+
The effect is being re-run due to an error in a previous run.
|
|
65
|
+
|
|
66
|
+
#### `{ type: 'lineage', parent: CleanupReason }`
|
|
67
|
+
A parent effect was cleaned up, causing this child effect to also be cleaned up.
|
|
68
|
+
|
|
69
|
+
#### `{ type: 'invalidate', cause: CleanupReason }`
|
|
70
|
+
The effect was invalidated for some other reason.
|
|
71
|
+
|
|
72
|
+
#### `{ type: 'multiple', reasons: CleanupReason[] }`
|
|
73
|
+
Multiple reasons combined (rare, usually from complex cleanup scenarios).
|
|
74
|
+
|
|
75
|
+
## Examples
|
|
76
|
+
|
|
77
|
+
### Debugging Multiple Dependencies
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
const state = mutts.reactive({
|
|
81
|
+
user: { name: 'John' },
|
|
82
|
+
posts: [{ title: 'Hello' }]
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
mutts.effect(() => {
|
|
86
|
+
const reason = debug.getReason()
|
|
87
|
+
|
|
88
|
+
if (reason?.type === 'propChange') {
|
|
89
|
+
console.log(`Effect triggered by ${reason.triggers.length} changes:`)
|
|
90
|
+
reason.triggers.forEach(trigger => {
|
|
91
|
+
if (trigger.evolution.type === 'set') {
|
|
92
|
+
console.log(` Property '${trigger.evolution.prop}' changed`)
|
|
93
|
+
} else if (trigger.evolution.type === 'add') {
|
|
94
|
+
console.log(` Property '${trigger.evolution.prop}' added`)
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log(`User: ${state.user.name}, Posts: ${state.posts.length}`)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
// Multiple simultaneous changes
|
|
103
|
+
mutts.untracked(() => {
|
|
104
|
+
state.user.name = 'Jane'
|
|
105
|
+
state.posts.push({ title: 'New Post' })
|
|
106
|
+
})
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Conditional Logic Based on Reason
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
mutts.effect(() => {
|
|
113
|
+
const reason = debug.getReason()
|
|
114
|
+
|
|
115
|
+
if (!reason) {
|
|
116
|
+
// First run - expensive initialization
|
|
117
|
+
console.log('Initializing...')
|
|
118
|
+
// setup expensive resources
|
|
119
|
+
} else if (reason.type === 'propChange') {
|
|
120
|
+
// Re-run - can optimize based on what changed
|
|
121
|
+
const userChanged = reason.triggers.some(t =>
|
|
122
|
+
t.evolution.prop === 'name'
|
|
123
|
+
)
|
|
124
|
+
if (userChanged) {
|
|
125
|
+
console.log('User name changed - updating UI')
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
console.log(`User: ${state.user.name}`)
|
|
130
|
+
})
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Notes
|
|
134
|
+
|
|
135
|
+
- The reason is only available during the effect's execution
|
|
136
|
+
- After the effect completes, the reason is cleared
|
|
137
|
+
- The reason reflects why the *previous* run was cleaned up, not why the current run started
|
|
138
|
+
- In TypeScript, you can type the return value as:
|
|
139
|
+
```typescript
|
|
140
|
+
type CleanupReason = import('mutts').CleanupReason
|
|
141
|
+
const reason: CleanupReason | undefined = debug.getReason()
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Configuration
|
|
145
|
+
|
|
146
|
+
Reason gathering is controlled by `options.introspection.gatherReasons`:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
import { reactiveOptions } from 'mutts'
|
|
150
|
+
|
|
151
|
+
// Disable reason gathering for production (performance)
|
|
152
|
+
reactiveOptions.introspection = null
|
|
153
|
+
|
|
154
|
+
// Or customize what lineage information is captured
|
|
155
|
+
reactiveOptions.introspection = {
|
|
156
|
+
gatherReasons: { lineages: 'touch' }, // 'none' | 'touch' | 'dependency' | 'both'
|
|
157
|
+
logErrors: true,
|
|
158
|
+
enableHistory: true,
|
|
159
|
+
historySize: 50,
|
|
160
|
+
}
|
|
161
|
+
```
|
package/docs/flavored.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
# Flavored Functions
|
|
1
|
+
# Flavored and Captioned Functions
|
|
2
2
|
|
|
3
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
4
|
|
|
5
|
+
The `captioned` utility is a sibling concept for callback-oriented APIs. It adds a tagged-template call form that can attach a runtime caption to one callback argument before delegating to the base function.
|
|
6
|
+
|
|
5
7
|
## Overview
|
|
6
8
|
|
|
7
9
|
Flavored functions allow you to:
|
|
@@ -10,6 +12,13 @@ Flavored functions allow you to:
|
|
|
10
12
|
- Use either automatic option merging (`flavorOptions`) or custom argument transformation (`createFlavor`)
|
|
11
13
|
- Return hand-made functions for complete control (the generic case)
|
|
12
14
|
|
|
15
|
+
Captioned functions allow you to:
|
|
16
|
+
- Keep the normal callback-first call form
|
|
17
|
+
- Add a tagged-template call form like `` effect`render:${id}`(fn) ``
|
|
18
|
+
- Warn when a callback-first API receives an anonymous callback without a caption
|
|
19
|
+
- Preserve captioning across flavored variants created with `createFlavor` or `flavorOptions`
|
|
20
|
+
- Target callbacks that are not in argument position `0`
|
|
21
|
+
|
|
13
22
|
## Basic Usage
|
|
14
23
|
|
|
15
24
|
### Creating a Flavored Function
|
|
@@ -62,6 +71,57 @@ effect.named('myEffect')(fn) // With name option
|
|
|
62
71
|
effect.opaque.named('x')(fn) // Chained
|
|
63
72
|
```
|
|
64
73
|
|
|
74
|
+
### `captioned(fn, options?)`
|
|
75
|
+
|
|
76
|
+
Creates a callback-oriented function that also accepts a tagged-template call form. The caption is applied to the configured callback argument before the base function runs.
|
|
77
|
+
|
|
78
|
+
**Use when:** Your API takes a callback argument and you want ergonomic call-site naming without turning naming itself into a flavor.
|
|
79
|
+
|
|
80
|
+
**Parameters:**
|
|
81
|
+
- `fn` - The callback-first base function
|
|
82
|
+
- `options.callbackIndex` - Which argument should be treated as the callback to rename/warn about. Defaults to `0`
|
|
83
|
+
- `options.name` - Human-readable label used in warning messages
|
|
84
|
+
- `options.rename` - Optional function to customize how the caption is applied to the callback
|
|
85
|
+
- `options.warn` - Optional warning sink for anonymous uncaptained callbacks
|
|
86
|
+
- `options.shouldWarnAnonymous` - Optional predicate to suppress warnings for specific argument shapes
|
|
87
|
+
|
|
88
|
+
**Example:**
|
|
89
|
+
```typescript
|
|
90
|
+
import { captioned } from 'mutts'
|
|
91
|
+
|
|
92
|
+
const run = captioned(
|
|
93
|
+
(callback: () => void) => {
|
|
94
|
+
callback()
|
|
95
|
+
},
|
|
96
|
+
{ name: 'run' }
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
run(function namedTask() {})
|
|
100
|
+
run`task:${42}`(() => {})
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The two call forms are:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
run(callback)
|
|
107
|
+
run`caption`(callback)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
If `run(callback)` receives an anonymous callback, `captioned` may warn depending on its `shouldWarnAnonymous` policy.
|
|
111
|
+
|
|
112
|
+
You can also target callbacks that are not the first argument:
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
const attendLike = captioned(
|
|
116
|
+
(source: string[], callback: (value: string) => void) => {
|
|
117
|
+
for (const value of source) callback(value)
|
|
118
|
+
},
|
|
119
|
+
{ name: 'attendLike', callbackIndex: 1 }
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
attendLike`items`(['a', 'b'], () => {})
|
|
123
|
+
```
|
|
124
|
+
|
|
65
125
|
### `flavorOptions(fn, defaultOptions)`
|
|
66
126
|
|
|
67
127
|
Creates a flavored variant that merges options with the last argument.
|
|
@@ -192,6 +252,43 @@ calculator.multiply(3, 4) // 12
|
|
|
192
252
|
calculator.double()(3, 4) // 14 (6 + 8)
|
|
193
253
|
```
|
|
194
254
|
|
|
255
|
+
## Combining `flavored` and `captioned`
|
|
256
|
+
|
|
257
|
+
They solve different problems:
|
|
258
|
+
|
|
259
|
+
- `flavored` changes how a function is configured via chainable properties
|
|
260
|
+
- `captioned` changes how a callback-first function can be called
|
|
261
|
+
|
|
262
|
+
They compose naturally:
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
const watch = captioned(
|
|
266
|
+
flavored(baseWatch, {
|
|
267
|
+
get immediate() {
|
|
268
|
+
return flavorOptions(this, { immediate: true })
|
|
269
|
+
}
|
|
270
|
+
}),
|
|
271
|
+
{ name: 'watch' }
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
watch(() => state.count, changed)
|
|
275
|
+
watch`counter:watch`(() => state.count, changed)
|
|
276
|
+
watch.immediate`counter:watch`(() => state.count, changed)
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
For callback arguments in another position, use `callbackIndex`:
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
const attend = captioned(baseAttend, {
|
|
283
|
+
name: 'attend',
|
|
284
|
+
callbackIndex: 1,
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
attend`entries`(source, (key) => {
|
|
288
|
+
console.log(key)
|
|
289
|
+
})
|
|
290
|
+
```
|
|
291
|
+
|
|
195
292
|
## TypeScript Considerations
|
|
196
293
|
|
|
197
294
|
Flavored functions use proxies and require type assertions for complex chaining scenarios:
|