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
|
@@ -45,7 +45,7 @@ The wrapped function preserves its signature (parameters and return value), and
|
|
|
45
45
|
|
|
46
46
|
### `atom()` - Immediate Atomic Execution
|
|
47
47
|
|
|
48
|
-
While `atomic()` **wraps** a function for later calls, `atom()` **runs** a function immediately and atomically. It
|
|
48
|
+
While `atomic()` **wraps** a function for later calls, `atom()` **runs** a function immediately and atomically. It uses `batch(fn, { immediate: true })`, so it executes right away even inside an existing batch while still contributing its triggered effects to the active transaction.
|
|
49
49
|
|
|
50
50
|
```typescript
|
|
51
51
|
import { atom, reactive, effect } from 'mutts'
|
|
@@ -80,6 +80,122 @@ const update = atomic((a, b) => { state.a = a; state.b = b })
|
|
|
80
80
|
update(1, 2) // runs when called
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
+
### `batch()` - Explicit Batch Control
|
|
84
|
+
|
|
85
|
+
`batch()` now uses an options object:
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
batch(effectOrEffects, {
|
|
89
|
+
immediate?: boolean,
|
|
90
|
+
contained?: boolean,
|
|
91
|
+
caller?: EffectTrigger,
|
|
92
|
+
})
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The main modes are:
|
|
96
|
+
|
|
97
|
+
- default nested behavior: if a batch is already active, `batch(fn)` joins that existing batch instead of creating an implicit child batch
|
|
98
|
+
- `immediate: true`: execute the provided function now, but keep all triggered effects inside the current batch
|
|
99
|
+
- `contained: true`: force a fresh local batch that drains before returning, even when called from inside another batch
|
|
100
|
+
- `caller`: advanced internal override for causal chaining; most user code should omit it
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
import { batch, effect, reactive } from 'mutts'
|
|
104
|
+
|
|
105
|
+
const state = reactive({ a: 0, b: 0 })
|
|
106
|
+
|
|
107
|
+
effect(() => {
|
|
108
|
+
console.log(state.a, state.b)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
batch(() => {
|
|
112
|
+
state.a = 1
|
|
113
|
+
state.b = 2
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
batch(() => {
|
|
117
|
+
state.a = 3
|
|
118
|
+
}, { immediate: true })
|
|
119
|
+
|
|
120
|
+
batch(() => {
|
|
121
|
+
state.b = 4
|
|
122
|
+
}, { immediate: true, contained: true })
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
#### Nested semantics
|
|
126
|
+
|
|
127
|
+
Nested batching is no longer implicitly contained.
|
|
128
|
+
|
|
129
|
+
- `batch(fn)` inside another batch joins the parent batch
|
|
130
|
+
- `batch(fn, { immediate: true })` runs `fn` now and queues its consequences into the parent batch
|
|
131
|
+
- `batch(fn, { contained: true })` creates an isolated sub-batch and flushes it before returning
|
|
132
|
+
|
|
133
|
+
Use `contained: true` only when you explicitly need sub-transaction behavior.
|
|
134
|
+
|
|
135
|
+
### Effect Ordering with Phase Tokens
|
|
136
|
+
|
|
137
|
+
Effects are usually ordered by their data dependencies: an effect that reads a reactive property is eligible to re-run after another effect changes that property. Mutts uses those causal links as an execution graph by default. The `reactiveOptions.scheduler` option controls how much of that graph is maintained for scheduling and diagnostics:
|
|
138
|
+
|
|
139
|
+
- `scheduler: 'ordered'` (default): effects that are already queued together are processed in dependency order when possible, so consequences run after the effects that caused them. Parent effects also run before their queued child effects, which lets parent cleanup stop stale children before they re-run.
|
|
140
|
+
- `scheduler: 'debug'`: ordered scheduling plus heavier diagnostics for investigation.
|
|
141
|
+
- `scheduler: 'raw'`: graph maintenance is disabled for speed, and already-queued effects use FIFO ordering with heuristic cycle protection.
|
|
142
|
+
|
|
143
|
+
When you need to make a render phase or other side-effect phase explicit, use an ordinary reactive "phase token": one effect advances the token, and later effects read it.
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
import { effect, reactive } from 'mutts'
|
|
147
|
+
|
|
148
|
+
const phase = reactive({
|
|
149
|
+
measured: 0,
|
|
150
|
+
positioned: 0,
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
effect`render:measure`(() => {
|
|
154
|
+
measureDom()
|
|
155
|
+
phase.measured++
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
effect`render:position`(() => {
|
|
159
|
+
phase.measured
|
|
160
|
+
positionDom()
|
|
161
|
+
phase.positioned++
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
effect`render:paint`(() => {
|
|
165
|
+
phase.positioned
|
|
166
|
+
paintDom()
|
|
167
|
+
})
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
This is sometimes called a beacon, barrier, or phase marker in application code, but it does not need a special primitive: the token is just reactive state. The important part is that the dependency is visible. The second effect does not merely rely on incidental registration order; it declares "I am downstream of `phase.measured`."
|
|
171
|
+
|
|
172
|
+
This phase-token pattern also works in `scheduler: 'raw'` when the token is the downstream effect's actual scheduling dependency: the producer runs, advances the token, and that write queues the consumer afterward. That ordering comes from normal queuing, not from the dependency graph. What raw mode does not do is topologically reorder two effects that were already queued independently by some other write.
|
|
173
|
+
|
|
174
|
+
If the downstream effect should be scheduled only by the phase token, and not directly by the raw state used by the producer, read the raw payload untracked:
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
import { effect, reactive, untracked } from 'mutts'
|
|
178
|
+
|
|
179
|
+
const state = reactive({ input: '' })
|
|
180
|
+
const phase = reactive({ rendered: 0 })
|
|
181
|
+
|
|
182
|
+
effect`render:first-pass`(() => {
|
|
183
|
+
state.input
|
|
184
|
+
renderFirstPass()
|
|
185
|
+
phase.rendered++
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
effect`render:second-pass`(() => {
|
|
189
|
+
phase.rendered
|
|
190
|
+
|
|
191
|
+
untracked`render:second-pass:payload`(() => {
|
|
192
|
+
renderSecondPass(state.input)
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Use `defer()` instead when the desired ordering is "after the whole current batch has settled" rather than "after this producer effect advances this phase."
|
|
198
|
+
|
|
83
199
|
### `addBatchCleanup()` / `defer()` - Deferring Work to Avoid Cycles
|
|
84
200
|
|
|
85
201
|
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.
|
|
@@ -238,7 +354,7 @@ addBatchCleanup(() => {
|
|
|
238
354
|
|
|
239
355
|
**Nested Batches:**
|
|
240
356
|
|
|
241
|
-
Callbacks added in nested batches are collected and run when the **outermost batch** completes:
|
|
357
|
+
Callbacks added in inherited nested batches are collected and run when the **outermost batch** completes:
|
|
242
358
|
|
|
243
359
|
```typescript
|
|
244
360
|
effect(() => {
|
|
@@ -252,7 +368,15 @@ effect(() => {
|
|
|
252
368
|
// Output:
|
|
253
369
|
// Outer deferred
|
|
254
370
|
// Inner deferred
|
|
255
|
-
// (Both run after outer batch completes)
|
|
371
|
+
// (Both run after outer batch completes because `atomic()` joins the active batch)
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
If you need deferred callbacks to flush within an isolated nested batch, use an explicitly contained batch:
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
batch(() => {
|
|
378
|
+
addBatchCleanup(() => console.log('Contained deferred'))
|
|
379
|
+
}, { contained: true })
|
|
256
380
|
```
|
|
257
381
|
|
|
258
382
|
**Error Handling:**
|
|
@@ -526,6 +650,13 @@ watch.immediate.deep(() => state.nested, (v) => {
|
|
|
526
650
|
})
|
|
527
651
|
```
|
|
528
652
|
|
|
653
|
+
As a callback-first API, `watch` also supports tagged-template captions:
|
|
654
|
+
|
|
655
|
+
```typescript
|
|
656
|
+
watch`count:watch`(() => state.count, (v) => console.log(v))
|
|
657
|
+
watch.immediate`count:watch`(() => state.count, (v) => console.log(v))
|
|
658
|
+
```
|
|
659
|
+
|
|
529
660
|
These flavors are a shorthand for passing options:
|
|
530
661
|
- `watch.immediate(...)` is equivalent to `watch(..., { immediate: true })`
|
|
531
662
|
- `watch.deep(...)` is equivalent to `watch(..., { deep: true })`
|
|
@@ -928,7 +1059,7 @@ When you replace a reactive object with another object that shares the same prot
|
|
|
928
1059
|
|
|
929
1060
|
- Watchers attached to the container are *not* re-fired if the container's prototype did not change (this avoids unnecessary parent effect re-runs).
|
|
930
1061
|
- Watchers attached to nested properties are re-evaluated only for keys that actually changed (added, removed, or whose values differ).
|
|
931
|
-
- For arrays, the
|
|
1062
|
+
- For arrays, the behavior stays index-oriented: replacing an element at index `i` fires a touch for that index (and `length` if needed) rather than diffing the element recursively. This preserves reorder detection.
|
|
932
1063
|
- Prototype chain properties are compared when both objects have prototype chains, ensuring changes to prototype-level properties are detected.
|
|
933
1064
|
|
|
934
1065
|
**Integration with Prototype Chains:**
|
|
@@ -974,7 +1105,7 @@ expect(titleWatcher).toHaveBeenCalledTimes(2)
|
|
|
974
1105
|
expect(viewsWatcher).toHaveBeenCalledTimes(2)
|
|
975
1106
|
```
|
|
976
1107
|
|
|
977
|
-
This
|
|
1108
|
+
This behavior keeps container-level watchers stable while still delivering fine-grained updates to nested effects—ideal when you replace data structures with freshly fetched objects that share the same prototype.
|
|
978
1109
|
|
|
979
1110
|
### Origin Filtering
|
|
980
1111
|
|
|
@@ -1170,8 +1301,14 @@ effect(() => console.log(profile.displayName)) // tracks .displayName only
|
|
|
1170
1301
|
|
|
1171
1302
|
**Derived filtered collection**:
|
|
1172
1303
|
```typescript
|
|
1173
|
-
const
|
|
1174
|
-
//
|
|
1304
|
+
const filtered = lift(() => items.filter(x => x.active))
|
|
1305
|
+
// Element-wise diff — only changed elements sync, not full rebuild
|
|
1306
|
+
```
|
|
1307
|
+
|
|
1308
|
+
`lift` also supports the same tagged-template caption form:
|
|
1309
|
+
|
|
1310
|
+
```ts
|
|
1311
|
+
const filtered = lift`active:items`(() => items.filter(x => x.active))
|
|
1175
1312
|
```
|
|
1176
1313
|
|
|
1177
1314
|
**Per-element transform**:
|
|
@@ -1230,17 +1367,36 @@ state.items = fetchedItems // deep touch diffs old vs new per-index — no lift
|
|
|
1230
1367
|
```typescript
|
|
1231
1368
|
import { morph } from 'mutts'
|
|
1232
1369
|
|
|
1370
|
+
// Arrays: (item, position, access?) => O
|
|
1233
1371
|
function morph<I, O>(
|
|
1234
1372
|
source: readonly I[] | (() => readonly I[]),
|
|
1235
|
-
fn: (arg: I) => O,
|
|
1373
|
+
fn: (arg: I, position: { index: number }, access?: EffectAccess) => O,
|
|
1236
1374
|
options?: { pure?: boolean | ((i: I) => boolean) }
|
|
1237
1375
|
): O[]
|
|
1376
|
+
|
|
1377
|
+
// Maps: (value, key, access?) => O
|
|
1378
|
+
function morph<K, V, O>(
|
|
1379
|
+
source: Map<K, V>,
|
|
1380
|
+
fn: (arg: V, key: K, access?: EffectAccess) => O,
|
|
1381
|
+
options?: { pure?: boolean | ((i: V) => boolean) }
|
|
1382
|
+
): Map<K, O>
|
|
1383
|
+
|
|
1384
|
+
// Records: (value, key, access?) => O
|
|
1385
|
+
function morph<S extends Record<PropertyKey, any>, O>(
|
|
1386
|
+
source: S,
|
|
1387
|
+
fn: (arg: S[keyof S], key: keyof S, access?: EffectAccess) => O,
|
|
1388
|
+
options?: { pure?: boolean | ((i: S[keyof S]) => boolean) }
|
|
1389
|
+
): { [K in keyof S]: O }
|
|
1238
1390
|
```
|
|
1239
1391
|
|
|
1240
1392
|
**Parameters**
|
|
1241
1393
|
|
|
1242
|
-
- `source`: a reactive array or a function returning one. Array mutations are tracked via `arrayDiff`.
|
|
1243
|
-
- `fn`: mapping callback.
|
|
1394
|
+
- `source`: a reactive array, Map, or record, or a function returning one. Array mutations are tracked via `arrayDiff`.
|
|
1395
|
+
- `fn`: mapping callback. Signature varies by source type:
|
|
1396
|
+
- Arrays: `(item, position, access?) => O` where `position.index` is the current index
|
|
1397
|
+
- Maps: `(value, key, access?) => O`
|
|
1398
|
+
- Records: `(value, key, access?) => O`
|
|
1399
|
+
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.
|
|
1244
1400
|
- `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.
|
|
1245
1401
|
|
|
1246
1402
|
**Behaviour**
|
|
@@ -1248,6 +1404,7 @@ function morph<I, O>(
|
|
|
1248
1404
|
- **Lazy**: elements are only computed when accessed (e.g. `result[0]`). Unaccessed indices remain `undefined` in the cache.
|
|
1249
1405
|
- **Identity stable**: the returned reactive array proxy is the same object across source mutations. Only affected indices are invalidated.
|
|
1250
1406
|
- **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.
|
|
1407
|
+
- **Reactive position**: For arrays, the `position` object is stable per item and its `index` updates reactively when items move due to shifts/reorders.
|
|
1251
1408
|
- **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.
|
|
1252
1409
|
|
|
1253
1410
|
**Basic usage**
|
|
@@ -1268,6 +1425,21 @@ console.log(upper[3]) // "DAVE"
|
|
|
1268
1425
|
items.splice(1, 1) // Remove 'bob' — indices shift, cache invalidated for affected positions
|
|
1269
1426
|
```
|
|
1270
1427
|
|
|
1428
|
+
**Using position.index**
|
|
1429
|
+
|
|
1430
|
+
```typescript
|
|
1431
|
+
const items = reactive(['a', 'b', 'c'])
|
|
1432
|
+
const indexed = morph(items, (item, position) => `${item}@${position.index}`)
|
|
1433
|
+
|
|
1434
|
+
console.log(indexed[0]) // "a@0"
|
|
1435
|
+
console.log(indexed[1]) // "b@1"
|
|
1436
|
+
|
|
1437
|
+
items.unshift('x') // Insert at beginning
|
|
1438
|
+
console.log(indexed[0]) // "x@0"
|
|
1439
|
+
console.log(indexed[1]) // "a@1" — position.index updated reactively
|
|
1440
|
+
console.log(indexed[2]) // "b@2"
|
|
1441
|
+
```
|
|
1442
|
+
|
|
1271
1443
|
**With reactive callback dependencies**
|
|
1272
1444
|
|
|
1273
1445
|
```typescript
|
|
@@ -1478,7 +1650,7 @@ For a full guide on debugging, including cycle detection and memoization discrep
|
|
|
1478
1650
|
|
|
1479
1651
|
### Quick Summary
|
|
1480
1652
|
|
|
1481
|
-
- **Cycle Detection**: Automatically catch circular dependencies via `reactiveOptions.
|
|
1482
|
-
- **
|
|
1653
|
+
- **Cycle Detection**: Automatically catch circular dependencies via `reactiveOptions.scheduler`. Note: Instant mathematical detection requires choosing `'ordered'` or `'debug'` mode.
|
|
1654
|
+
- **Ordered Mode**: The default `reactiveOptions.scheduler` is set to `'ordered'`, preserving causal ordering and parent/child effect lifecycle ordering.
|
|
1483
1655
|
- **Memoization Discrepancy**: Detect "missing dependencies" by running computations twice during development using `reactiveOptions.onMemoizationDiscrepancy`.
|
|
1484
1656
|
- **Global Hooks**: Use `reactiveOptions.touched`, `enter`, and `leave` to observe system activity.
|
package/docs/reactive/attend.md
CHANGED
|
@@ -9,6 +9,7 @@ The `attend` utility reactively iterates over the entries of a collection, runni
|
|
|
9
9
|
- **Creates** an inner effect for each key, via `ascend`.
|
|
10
10
|
- **Disposes** the inner effect when the key is removed from the collection.
|
|
11
11
|
- Allows the callback to return a **cleanup function** (like a regular effect closer).
|
|
12
|
+
- Supports tagged-template captions on its callback argument.
|
|
12
13
|
|
|
13
14
|
This is the foundational lifecycle primitive that `organized` is built on.
|
|
14
15
|
|
|
@@ -33,6 +34,18 @@ function attend<S extends Record<PropertyKey, any>>(source: S, callback: (key: k
|
|
|
33
34
|
- **`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
35
|
- **`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
|
|
|
37
|
+
### Captioned callback form
|
|
38
|
+
|
|
39
|
+
Unlike `effect` or `lift`, `attend` receives its callback as the **second** argument. It still supports tagged-template captioning:
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
attend`entries`(config, (key) => {
|
|
43
|
+
console.log(`${key} = ${config[key]}`)
|
|
44
|
+
})
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The caption is applied to the callback argument and contributes to the runtime names of the inner per-key effects.
|
|
48
|
+
|
|
36
49
|
### Returns
|
|
37
50
|
|
|
38
51
|
A `ScopedCallback` that tears down all inner effects and the outer effect.
|
|
@@ -67,6 +80,14 @@ stop()
|
|
|
67
80
|
// Disposes everything
|
|
68
81
|
```
|
|
69
82
|
|
|
83
|
+
The same record form also works with a caption:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
const stop = attend`config:entries`(config, (key) => {
|
|
87
|
+
console.log(`${key} = ${config[key]}`)
|
|
88
|
+
})
|
|
89
|
+
```
|
|
90
|
+
|
|
70
91
|
### Array
|
|
71
92
|
|
|
72
93
|
```typescript
|
|
@@ -123,6 +144,17 @@ attend(
|
|
|
123
144
|
)
|
|
124
145
|
```
|
|
125
146
|
|
|
147
|
+
And likewise with a caption:
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
attend`ownKeys`(
|
|
151
|
+
() => Reflect.ownKeys(source),
|
|
152
|
+
(key) => {
|
|
153
|
+
console.log(key, source[key])
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
```
|
|
157
|
+
|
|
126
158
|
## How it Works
|
|
127
159
|
|
|
128
160
|
1. An **outer effect** calls `enumerate()` (or derives it from the collection type), collecting the current keys into a `Set`.
|
package/docs/reactive/core.md
CHANGED
|
@@ -242,6 +242,18 @@ function effect(
|
|
|
242
242
|
|
|
243
243
|
**Returns:** A cleanup function to stop the effect
|
|
244
244
|
|
|
245
|
+
**Captioned call form:**
|
|
246
|
+
|
|
247
|
+
`effect` also supports a tagged-template naming form for callback-first calls:
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
effect`counter:main`(() => {
|
|
251
|
+
console.log(state.count)
|
|
252
|
+
})
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
This is the preferred way to attach a runtime/debug name to a new effect.
|
|
256
|
+
|
|
245
257
|
**Example:**
|
|
246
258
|
|
|
247
259
|
```typescript
|
|
@@ -264,6 +276,8 @@ state.mood = 'surprised' // Does not trigger the effect
|
|
|
264
276
|
cleanup() // Stops the effect
|
|
265
277
|
```
|
|
266
278
|
|
|
279
|
+
If you use the plain `effect(fn)` form with an anonymous callback, `mutts` may warn and suggest either a named function or the tagged-template caption form.
|
|
280
|
+
|
|
267
281
|
You can also branch on the `reaction` flag to separate initialisation logic from update logic:
|
|
268
282
|
|
|
269
283
|
```typescript
|
|
@@ -647,6 +661,8 @@ const stopOuter = effect(() => {
|
|
|
647
661
|
|
|
648
662
|
The `untracked()` function allows you to run code without tracking dependencies, which can be useful for creating effects or performing operations that shouldn't be part of the current effect's dependency graph.
|
|
649
663
|
|
|
664
|
+
`untracked` is captioned. When you use it as a reactive execution root, prefer the template form so debug output and chained `CleanupReason.external` frames stay descriptive.
|
|
665
|
+
|
|
650
666
|
```typescript
|
|
651
667
|
import { effect, untracked, reactive } from 'mutts'
|
|
652
668
|
|
|
@@ -661,7 +677,7 @@ effect(() => {
|
|
|
661
677
|
// Create an inner effect without tracking the creation under the outer effect
|
|
662
678
|
let stopInner: (() => void) | undefined
|
|
663
679
|
|
|
664
|
-
untracked(() => {
|
|
680
|
+
untracked`outer:inner-effect`(() => {
|
|
665
681
|
stopInner = effect(() => {
|
|
666
682
|
state.b
|
|
667
683
|
})
|
|
@@ -739,6 +755,8 @@ item.data = { value: 30 } // Triggers BOTH effects
|
|
|
739
755
|
|
|
740
756
|
#### `.named(name)`
|
|
741
757
|
|
|
758
|
+
**Obsolete:** prefer `` effect`name`(fn) `` for new code.
|
|
759
|
+
|
|
742
760
|
Creates a named effect for easier debugging and profiling. The name appears in DevTools and debug logs.
|
|
743
761
|
|
|
744
762
|
```typescript
|
|
@@ -748,18 +766,26 @@ const state = reactive({
|
|
|
748
766
|
count: 0
|
|
749
767
|
})
|
|
750
768
|
|
|
751
|
-
//
|
|
769
|
+
// Legacy named effect
|
|
752
770
|
effect.named('counter-effect')(() => {
|
|
753
771
|
console.log('Count:', state.count)
|
|
754
772
|
})
|
|
755
773
|
|
|
756
|
-
//
|
|
774
|
+
// Legacy named effects can also be combined with other options
|
|
757
775
|
effect.named('data-loader').opaque(() => {
|
|
758
776
|
console.log('Loading data...')
|
|
759
777
|
})
|
|
760
778
|
```
|
|
761
779
|
|
|
762
|
-
|
|
780
|
+
For new code, prefer:
|
|
781
|
+
|
|
782
|
+
```typescript
|
|
783
|
+
effect`counter-effect`(() => {
|
|
784
|
+
console.log('Count:', state.count)
|
|
785
|
+
})
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
**Benefits of captioned/named effects:**
|
|
763
789
|
- Easier identification in DevTools
|
|
764
790
|
- Better stack traces during debugging
|
|
765
791
|
- Helpful for performance profiling
|
|
@@ -769,7 +795,7 @@ effect.named('data-loader').opaque(() => {
|
|
|
769
795
|
Modifiers can be chained in any order:
|
|
770
796
|
|
|
771
797
|
```typescript
|
|
772
|
-
//
|
|
798
|
+
// Legacy named opaque effect
|
|
773
799
|
effect.named('my-effect').opaque(() => {
|
|
774
800
|
// Effect code
|
|
775
801
|
})
|
|
@@ -783,7 +809,7 @@ effect.opaque.named('my-effect')(() => {
|
|
|
783
809
|
Note: The modifiers return new effect functions with the options pre-applied, so they can be stored and reused:
|
|
784
810
|
|
|
785
811
|
```typescript
|
|
786
|
-
// Create a reusable named effect factory
|
|
812
|
+
// Create a reusable legacy named effect factory
|
|
787
813
|
const createDataEffect = effect.named('data-layer')
|
|
788
814
|
|
|
789
815
|
createDataEffect(() => {
|
|
@@ -795,6 +821,14 @@ createDataEffect(() => {
|
|
|
795
821
|
})
|
|
796
822
|
```
|
|
797
823
|
|
|
824
|
+
For single call sites, the tagged-template form is usually shorter:
|
|
825
|
+
|
|
826
|
+
```typescript
|
|
827
|
+
effect`data-layer`(() => {
|
|
828
|
+
console.log('Effect 1')
|
|
829
|
+
})
|
|
830
|
+
```
|
|
831
|
+
|
|
798
832
|
## Class Reactivity
|
|
799
833
|
|
|
800
834
|
### `@reactive` Decorator
|
|
@@ -25,7 +25,7 @@ These hooks are called during the execution of effects and computed values.
|
|
|
25
25
|
|
|
26
26
|
- **`beginChain(targets: Function[]) / endChain()`**: Called when a batch of effects starts and ends its execution.
|
|
27
27
|
- **`maxEffectChain`**: (Default: `100`) Limits the depth of synchronous effect triggering to prevent stack overflows.
|
|
28
|
-
- **`maxTriggerPerBatch`**: (Default: `10`) Limits how many times a single effect can be triggered within the same batch. Useful for detecting aggressive re-computation or infinite cycles in `
|
|
28
|
+
- **`maxTriggerPerBatch`**: (Default: `10`) Limits how many times a single effect can be triggered within the same batch. Useful for detecting aggressive re-computation or infinite cycles, especially in `scheduler: 'raw'` mode.
|
|
29
29
|
|
|
30
30
|
## Cycle Detection
|
|
31
31
|
|
|
@@ -33,25 +33,27 @@ These hooks are called during the execution of effects and computed values.
|
|
|
33
33
|
|
|
34
34
|
### Configuration
|
|
35
35
|
|
|
36
|
-
You can control how cycles are handled via `reactiveOptions.
|
|
36
|
+
You can control how cycles are handled via `reactiveOptions.scheduler`:
|
|
37
37
|
|
|
38
|
-
- **`'
|
|
39
|
-
- **`'
|
|
40
|
-
- **`'debug'`**:
|
|
38
|
+
- **`'ordered'`** (Default): Maintains the causal effect graph for dependency ordering, parent/child lifecycle ordering, and early cycle detection. Throws immediately with basic path information.
|
|
39
|
+
- **`'raw'`**: High-performance FIFO mode. Disables the dependency graph and topological sorting. Uses heuristic detection via `maxEffectChain`.
|
|
40
|
+
- **`'debug'`**: Ordered scheduling plus the heaviest diagnostics. Provides detailed cycle path reporting.
|
|
41
|
+
|
|
42
|
+
`reactiveOptions.cycleHandling` is still accepted as a deprecated alias: `'production'` maps to `'raw'`, and `'development'` maps to `'ordered'`.
|
|
41
43
|
|
|
42
44
|
### Topological vs. Flat Mode Detection
|
|
43
45
|
- **Detection**: Cycles are detected when the execution depth exceeds `maxEffectChain` (default 100).
|
|
44
46
|
- **Diagnostics**: The resulting `ReactiveError` includes a `trace` property (the recent execution sequence) and attempts to identify a repeating `cycle`.
|
|
45
|
-
- **Recommendation**: Use
|
|
47
|
+
- **Recommendation**: Use `raw` only when FIFO scheduling is sufficient and you want minimum overhead.
|
|
46
48
|
|
|
47
49
|
### Cycle Handling Modes
|
|
48
50
|
|
|
49
|
-
You can configure how the system handles cycles via `reactiveOptions.
|
|
51
|
+
You can configure how the system handles cycles via `reactiveOptions.scheduler`:
|
|
50
52
|
|
|
51
53
|
| Mode | Detection Timing | Cycle Information | Performance |
|
|
52
54
|
|------|-----------------|-------------------|-------------|
|
|
53
|
-
| `'
|
|
54
|
-
| `'
|
|
55
|
+
| `'ordered'` | Eager (On edge) | Exact path (DFS) | Moderate |
|
|
56
|
+
| `'raw'` | Late (Heuristic) | Trace of last N effects | Fastest |
|
|
55
57
|
| `'debug'` | Structural | Transitive closures | Slowest |
|
|
56
58
|
|
|
57
59
|
#### Finding Cycle Information
|
|
@@ -71,7 +73,7 @@ try {
|
|
|
71
73
|
|
|
72
74
|
- **`error.cycle`**: An array of effect names forming the cycle.
|
|
73
75
|
- **`error.causalChain`**: The sequence of triggers that led to the current effect.
|
|
74
|
-
- **`error.lineage`**: The creation stack of the effect (available in `
|
|
76
|
+
- **`error.lineage`**: The creation stack of the effect (available in `ordered` or `debug` modes).
|
|
75
77
|
|
|
76
78
|
### Memoization Discrepancy Detection
|
|
77
79
|
|
|
@@ -190,13 +192,13 @@ Since these are runtime options, you can toggle them based on your environment:
|
|
|
190
192
|
|
|
191
193
|
```typescript
|
|
192
194
|
if (process.env.NODE_ENV === 'development') {
|
|
193
|
-
reactiveOptions.
|
|
195
|
+
reactiveOptions.scheduler = 'debug';
|
|
194
196
|
reactiveOptions.onMemoizationDiscrepancy = myHandler;
|
|
195
197
|
enableIntrospection();
|
|
196
198
|
} else {
|
|
197
|
-
//
|
|
199
|
+
// Keep heavy diagnostics off in production for performance
|
|
198
200
|
reactiveOptions.onMemoizationDiscrepancy = undefined;
|
|
199
|
-
reactiveOptions.
|
|
201
|
+
reactiveOptions.scheduler = 'ordered'; // or 'raw' when FIFO scheduling is enough
|
|
200
202
|
}
|
|
201
203
|
```
|
|
202
204
|
|
|
@@ -212,6 +214,8 @@ import 'mutts/debug';
|
|
|
212
214
|
|
|
213
215
|
When an effect or watcher re-runs, it receives a `reaction` property (in `EffectAccess`) that describes *why* it was triggered. This is also passed to the `cleanup` function.
|
|
214
216
|
|
|
217
|
+
Reasons may be chained. For example, a `propChange` can carry an `external` chain entry when the reactive work ultimately originated from a captioned `root` or `untracked` call such as ``root`event:click`(...)``.
|
|
218
|
+
|
|
215
219
|
```typescript
|
|
216
220
|
effect(({ reaction }) => {
|
|
217
221
|
if (reaction && typeof reaction === 'object') {
|
|
@@ -246,10 +250,11 @@ effect(()=> {
|
|
|
246
250
|
|
|
247
251
|
Lineage tracking allows you to see the "causal path" of an effect—not just the current stack trace, but the stack traces of all parent effects that created the current execution.
|
|
248
252
|
|
|
249
|
-
When an effect is created, it is assigned a
|
|
253
|
+
When an effect is created, it is assigned a lineage signature. The raw stack/effect data is captured up front, but digestion into human-readable segments is deferred until display.
|
|
250
254
|
|
|
251
255
|
- **`logLineage()`**: Prints a formatted, interactive tree of the current effect's lineage to the console.
|
|
252
256
|
- **`captureLineage()`**: Captures the current lineage as a structured object.
|
|
257
|
+
- **`digestLineage()`**: Converts a captured lineage signature into display-ready segments on demand.
|
|
253
258
|
|
|
254
259
|
#### Lineage Options
|
|
255
260
|
|
|
@@ -266,8 +271,28 @@ When `mutts/debug` is active (or after calling `enableDevTools()`), a global `__
|
|
|
266
271
|
This object provides low-level access to the graph, lineage capture, and renaming utilities:
|
|
267
272
|
- `__MUTTS_DEBUG__.getGraph()`: Returns the full reactivity graph.
|
|
268
273
|
- `__MUTTS_DEBUG__.logLineage()`: logs the current lineage.
|
|
269
|
-
- `__MUTTS_DEBUG__.
|
|
274
|
+
- `__MUTTS_DEBUG__.logReason()`: logs the current reasons chain.
|
|
275
|
+
- `__MUTTS_DEBUG__.reason`: the current `CleanupReason` for the active effect re-run, if any.
|
|
276
|
+
- `__MUTTS_DEBUG__.lineage`: the current execution lineage, already digested into user-facing segments.
|
|
270
277
|
|
|
271
278
|
### Custom DevTools Formatters
|
|
272
279
|
|
|
273
280
|
`mutts/debug` automatically registers [Custom Formatters](https://bit.ly/chrome-extension-custom-formatters) in Chrome. This makes lineage objects and reactive proxies appear as clean, structured trees in the console instead of opaque Proxy objects.
|
|
281
|
+
|
|
282
|
+
#### Debugger / DevTools how-to
|
|
283
|
+
|
|
284
|
+
To inspect reactive debugging data directly in Chrome DevTools:
|
|
285
|
+
|
|
286
|
+
1. Allow **Custom formatters** in DevTools settings.
|
|
287
|
+
2. Import `mutts/debug` somewhere in your application source during development:
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
import 'mutts/debug'
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
3. Open DevTools and inspect the global `__MUTTS_DEBUG__` helper.
|
|
294
|
+
|
|
295
|
+
The most useful live entry points you can keep on watch are:
|
|
296
|
+
|
|
297
|
+
- `__MUTTS_DEBUG__.lineage`: Gives you the static "call stack" that produced this effect (without the cuts of batching)
|
|
298
|
+
- `__MUTTS_DEBUG__.reason`: Gives you the chain of reasons who lead the code who is run to be run - starting from initialization or events
|
package/docs/reactive.md
CHANGED
|
@@ -5,6 +5,7 @@ The Mutts Reactive System documentation has been split into focused sections for
|
|
|
5
5
|
## [Core Concepts](./reactive/core.md)
|
|
6
6
|
* **[Core API](./reactive/core.md#core-api)**: `reactive`, `effect`, `unwrap`
|
|
7
7
|
* **[Effect System](./reactive/core.md#effect-system)**: Dependency tracking, cleanups, async effects
|
|
8
|
+
* **[Captioned Calls](./reactive/core.md#effect)**: tagged-template naming for callback-first APIs such as `effect`
|
|
8
9
|
* **[Class Reactivity](./reactive/core.md#class-reactivity)**: Decorators and functional syntax
|
|
9
10
|
|
|
10
11
|
## [Collections](./reactive/collections.md)
|
|
@@ -12,11 +13,13 @@ The Mutts Reactive System documentation has been split into focused sections for
|
|
|
12
13
|
* **[Reactive Arrays](./reactive/collections.md#reactivearray)**: Full array method support
|
|
13
14
|
* **[Morphing](./reactive/collections.md#morph)**: `morph`, `organized`
|
|
14
15
|
* **[Attend](./reactive/attend.md)**: Reactive enumeration (`attend`)
|
|
16
|
+
* **[Captioned Collection Callbacks](./reactive/attend.md#captioned-callback-form)**: tagged-template naming for second-argument callbacks like `attend`
|
|
15
17
|
* **[Resource](./reactive/resource.md)**: Async state tracking (`resource`)
|
|
16
18
|
|
|
17
19
|
## [Advanced Topics](./reactive/advanced.md)
|
|
18
20
|
* **[Choosing the Right Primitive](./reactive/advanced.md#choosing-the-right-reactive-primitive)**: Comparison table of effect-value functions (memoize, lift, project, etc.)
|
|
19
21
|
* **[Atomic Operations](./reactive/advanced.md#atomic-operations)**: Batching and Bidirectional binding
|
|
22
|
+
* **[Effect Ordering](./reactive/advanced.md#effect-ordering-with-phase-tokens)**: Use reactive phase tokens to make dependency ordering explicit
|
|
20
23
|
* **[Evolution Tracking](./reactive/advanced.md#evolution-tracking)**: History introspection
|
|
21
24
|
* **[Prototype Chains](./reactive/advanced.md#prototype-chains-and-pure-objects)**: Advanced inheritance patterns
|
|
22
25
|
* **[Memoization](./reactive/advanced.md#memoization)**: Caching strategies
|
|
@@ -28,4 +31,4 @@ The Mutts Reactive System documentation has been split into focused sections for
|
|
|
28
31
|
* **[Memoization Discrepancy](./reactive/debugging.md#memoization-discrepancy-detection)**: Identifying missing dependencies
|
|
29
32
|
* **[Introspection API](./reactive/debugging.md#introspection-api)**: Programmatic analysis and dependency graphs
|
|
30
33
|
|
|
31
|
-
* **[Performance](./reactive/debugging.md#performance-cost)**: Understanding the cost of debugging tools
|
|
34
|
+
* **[Performance](./reactive/debugging.md#performance-cost)**: Understanding the cost of debugging tools
|