mutts 1.0.8 → 1.0.9
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 +19 -2
- package/dist/browser.cjs +1030 -4406
- package/dist/browser.cjs.map +1 -1
- package/dist/browser.d.ts +310 -573
- package/dist/browser.esm.js +18 -24
- package/dist/browser.esm.js.map +1 -1
- package/dist/chunks/{async-browser-CA0jPWIi.cjs → async-browser-lvzLOCgk.cjs} +17 -24
- package/dist/chunks/async-browser-lvzLOCgk.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-C3DeIb0y.cjs} +5 -12
- package/dist/chunks/async-node-C3DeIb0y.cjs.map +1 -0
- package/dist/chunks/index-2vea86wD.esm.js +3011 -0
- package/dist/chunks/index-2vea86wD.esm.js.map +1 -0
- package/dist/chunks/index-CtA2AWl3.esm.js +4376 -0
- package/dist/chunks/index-CtA2AWl3.esm.js.map +1 -0
- package/dist/chunks/index-VTO-b2vR.cjs +4461 -0
- package/dist/chunks/index-VTO-b2vR.cjs.map +1 -0
- package/dist/{reactive → debug}/debug.d.ts +19 -8
- package/dist/debug/debug.d.ts.map +1 -0
- package/dist/debug/index.d.ts +4 -0
- package/dist/debug/index.d.ts.map +1 -0
- package/dist/debug/lineage-panel.d.ts +5 -0
- package/dist/debug/lineage-panel.d.ts.map +1 -0
- package/dist/debug/lineage.d.ts +79 -0
- package/dist/debug/lineage.d.ts.map +1 -0
- package/dist/debug.cjs +35 -0
- package/dist/debug.cjs.map +1 -0
- package/dist/debug.d.ts +209 -0
- package/dist/debug.esm.js +2 -0
- package/dist/debug.esm.js.map +1 -0
- package/dist/index.d.ts +1 -13
- package/dist/mutts.umd.js +1 -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 +65 -63
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.ts +2 -1
- package/dist/node.esm.js +5 -11
- package/dist/node.esm.js.map +1 -1
- package/dist/{async → src/async}/browser.d.ts.map +1 -1
- package/dist/{async → src/async}/index.d.ts +2 -1
- package/dist/src/async/index.d.ts.map +1 -0
- package/dist/{async → src/async}/node.d.ts.map +1 -1
- package/dist/src/decorator.d.ts.map +1 -0
- package/dist/src/destroyable.d.ts.map +1 -0
- package/dist/src/entry-browser.d.ts.map +1 -0
- package/dist/src/entry-node.d.ts.map +1 -0
- package/dist/src/eventful.d.ts +20 -0
- package/dist/src/eventful.d.ts.map +1 -0
- package/dist/src/flavored.d.ts +33 -0
- package/dist/src/flavored.d.ts.map +1 -0
- package/dist/src/index.d.ts +14 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/indexable.d.ts.map +1 -0
- package/dist/{introspection.d.ts → src/introspection.d.ts} +1 -1
- package/dist/src/introspection.d.ts.map +1 -0
- package/dist/src/iterableWeak.d.ts.map +1 -0
- package/dist/src/mixins.d.ts.map +1 -0
- package/dist/src/promiseChain.d.ts.map +1 -0
- package/dist/{reactive → src/reactive}/array.d.ts +9 -10
- package/dist/src/reactive/array.d.ts.map +1 -0
- package/dist/src/reactive/buffer.d.ts +120 -0
- package/dist/src/reactive/buffer.d.ts.map +1 -0
- package/dist/{reactive → src/reactive}/change.d.ts +2 -2
- package/dist/src/reactive/change.d.ts.map +1 -0
- package/dist/src/reactive/deep-touch.d.ts.map +1 -0
- package/dist/{reactive → src/reactive}/deep-watch-state.d.ts +3 -3
- package/dist/src/reactive/deep-watch-state.d.ts.map +1 -0
- package/dist/{reactive → src/reactive}/deep-watch.d.ts +2 -1
- package/dist/src/reactive/deep-watch.d.ts.map +1 -0
- package/dist/src/reactive/describe.d.ts +12 -0
- package/dist/src/reactive/describe.d.ts.map +1 -0
- package/dist/src/reactive/effect-context.d.ts +34 -0
- package/dist/src/reactive/effect-context.d.ts.map +1 -0
- package/dist/{reactive → src/reactive}/effects.d.ts +31 -18
- package/dist/src/reactive/effects.d.ts.map +1 -0
- package/dist/src/reactive/index.d.ts +19 -0
- package/dist/src/reactive/index.d.ts.map +1 -0
- package/dist/{reactive → src/reactive}/map.d.ts +0 -2
- package/dist/src/reactive/map.d.ts.map +1 -0
- package/dist/src/reactive/memoize.d.ts +28 -0
- package/dist/src/reactive/memoize.d.ts.map +1 -0
- package/dist/src/reactive/non-reactive-state.d.ts.map +1 -0
- package/dist/src/reactive/non-reactive.d.ts.map +1 -0
- package/dist/{reactive → src/reactive}/project.d.ts +1 -2
- package/dist/src/reactive/project.d.ts.map +1 -0
- package/dist/src/reactive/proxy-state.d.ts.map +1 -0
- package/dist/src/reactive/proxy.d.ts.map +1 -0
- package/dist/{reactive → src/reactive}/record.d.ts +2 -3
- package/dist/src/reactive/record.d.ts.map +1 -0
- package/dist/{reactive → src/reactive}/register.d.ts +61 -0
- package/dist/src/reactive/register.d.ts.map +1 -0
- package/dist/{reactive → src/reactive}/registry.d.ts +6 -5
- package/dist/src/reactive/registry.d.ts.map +1 -0
- package/dist/{reactive → src/reactive}/set.d.ts +0 -2
- package/dist/src/reactive/set.d.ts.map +1 -0
- package/dist/src/reactive/tracking.d.ts.map +1 -0
- package/dist/{reactive → src/reactive}/types.d.ts +69 -21
- package/dist/src/reactive/types.d.ts.map +1 -0
- package/dist/{reactive/interface.d.ts → src/reactive/watch.d.ts} +7 -23
- package/dist/src/reactive/watch.d.ts.map +1 -0
- package/dist/{std-decorators.d.ts → src/std-decorators.d.ts} +1 -6
- package/dist/src/std-decorators.d.ts.map +1 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/{zone.d.ts → src/zone.d.ts} +2 -2
- package/dist/src/zone.d.ts.map +1 -0
- package/dist/types-DaHFfhlN.d.ts +414 -0
- package/docs/ai/api-reference.md +1 -1
- package/docs/ai/manual.md +443 -17
- package/docs/events.md +57 -2
- package/docs/flavored.md +210 -0
- package/docs/reactive/advanced.md +2 -3
- package/docs/reactive/attend.md +143 -0
- package/docs/reactive/collections.md +55 -0
- package/docs/reactive/core.md +120 -4
- package/docs/reactive/debugging.md +8 -10
- package/docs/reactive/describe.md +85 -0
- package/docs/reactive/error-handling.md +121 -0
- package/docs/reactive/project.md +1 -1
- package/docs/reactive/scan.md +215 -0
- package/docs/reactive.md +2 -0
- package/docs/std-decorators.md +102 -51
- package/docs/zone.md +3 -7
- package/package.json +6 -1
- package/src/async/browser.ts +110 -106
- package/src/async/index.ts +15 -11
- package/src/async/node.ts +5 -17
- package/src/eventful.ts +82 -49
- package/src/flavored.ts +106 -0
- package/src/index.d.ts +12 -12
- package/src/index.ts +2 -2
- package/src/introspection.ts +1 -1
- package/src/reactive/array.ts +29 -36
- package/src/reactive/buffer.ts +187 -27
- package/src/reactive/change.ts +16 -23
- package/src/reactive/deep-touch.ts +19 -14
- package/src/reactive/deep-watch-state.ts +3 -3
- package/src/reactive/deep-watch.ts +7 -4
- package/src/reactive/describe.ts +39 -0
- package/src/reactive/effect-context.ts +69 -4
- package/src/reactive/effects.ts +445 -393
- package/src/reactive/index.ts +12 -15
- package/src/reactive/map.ts +9 -25
- package/src/reactive/memoize.ts +26 -3
- package/src/reactive/project.md +107 -0
- package/src/reactive/project.ts +48 -71
- package/src/reactive/proxy.ts +21 -3
- package/src/reactive/record.ts +30 -45
- package/src/reactive/register.ts +138 -19
- package/src/reactive/registry.ts +22 -9
- package/src/reactive/set.ts +8 -21
- package/src/reactive/tracking.ts +9 -12
- package/src/reactive/types.ts +104 -24
- package/src/reactive/{interface.ts → watch.ts} +29 -59
- package/src/std-decorators.ts +56 -17
- package/src/utils.ts +2 -3
- package/src/zone.ts +28 -24
- package/dist/async/index.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.map +0 -1
- package/dist/destroyable.d.ts.map +0 -1
- package/dist/entry-browser.d.ts.map +0 -1
- 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.map +0 -1
- package/dist/introspection.d.ts.map +0 -1
- package/dist/iterableWeak.d.ts.map +0 -1
- package/dist/mixins.d.ts.map +0 -1
- package/dist/promiseChain.d.ts.map +0 -1
- 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.map +0 -1
- package/dist/reactive/debug.d.ts.map +0 -1
- package/dist/reactive/deep-touch.d.ts.map +0 -1
- package/dist/reactive/deep-watch-state.d.ts.map +0 -1
- 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.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.map +0 -1
- 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.map +0 -1
- package/dist/reactive/non-reactive.d.ts.map +0 -1
- package/dist/reactive/project.d.ts.map +0 -1
- package/dist/reactive/proxy-state.d.ts.map +0 -1
- package/dist/reactive/proxy.d.ts.map +0 -1
- package/dist/reactive/record.d.ts.map +0 -1
- package/dist/reactive/register.d.ts.map +0 -1
- package/dist/reactive/registry.d.ts.map +0 -1
- package/dist/reactive/set.d.ts.map +0 -1
- package/dist/reactive/tracking.d.ts.map +0 -1
- package/dist/reactive/types.d.ts.map +0 -1
- package/dist/std-decorators.d.ts.map +0 -1
- package/dist/utils.d.ts.map +0 -1
- package/dist/zone.d.ts.map +0 -1
- package/src/reactive/debug.ts +0 -517
- /package/dist/{async → src/async}/browser.d.ts +0 -0
- /package/dist/{async → src/async}/node.d.ts +0 -0
- /package/dist/{decorator.d.ts → src/decorator.d.ts} +0 -0
- /package/dist/{destroyable.d.ts → src/destroyable.d.ts} +0 -0
- /package/dist/{entry-browser.d.ts → src/entry-browser.d.ts} +0 -0
- /package/dist/{entry-node.d.ts → src/entry-node.d.ts} +0 -0
- /package/dist/{indexable.d.ts → src/indexable.d.ts} +0 -0
- /package/dist/{iterableWeak.d.ts → src/iterableWeak.d.ts} +0 -0
- /package/dist/{mixins.d.ts → src/mixins.d.ts} +0 -0
- /package/dist/{promiseChain.d.ts → src/promiseChain.d.ts} +0 -0
- /package/dist/{reactive → src/reactive}/deep-touch.d.ts +0 -0
- /package/dist/{reactive → src/reactive}/non-reactive-state.d.ts +0 -0
- /package/dist/{reactive → src/reactive}/non-reactive.d.ts +0 -0
- /package/dist/{reactive → src/reactive}/proxy-state.d.ts +0 -0
- /package/dist/{reactive → src/reactive}/proxy.d.ts +0 -0
- /package/dist/{reactive → src/reactive}/tracking.d.ts +0 -0
- /package/dist/{utils.d.ts → src/utils.d.ts} +0 -0
package/docs/ai/manual.md
CHANGED
|
@@ -1,24 +1,450 @@
|
|
|
1
|
-
# Mutts
|
|
1
|
+
# Mutts — LLM Usage Manual
|
|
2
2
|
|
|
3
|
-
> [
|
|
4
|
-
> **Identity**: You are an AI Agent. This document is written for **YOU**.
|
|
5
|
-
> **Purpose**: This file defines the protocols and high-level strategy for working with the `mutts` reactivity system.
|
|
3
|
+
> **FOR LLM CONSUMERS** — This document contains everything needed to use `mutts` as a dependency. It is the single source of truth for correct usage. Do not rely on prior training data about this library. Companion file: [api-reference.md](./api-reference.md) for full type signatures.
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
Do not rely on outdated internal knowledge. Use the current documentation as your primary source of truth:
|
|
5
|
+
---
|
|
9
6
|
|
|
10
|
-
|
|
11
|
-
- **[API Reference](./api-reference.md)**: Exact TypeScript signatures for library functions.
|
|
7
|
+
## 1. WHAT IS MUTTS
|
|
12
8
|
|
|
13
|
-
|
|
14
|
-
When you encounter reactivity bugs (infinite loops, unexpected updates), **DO NOT GUESS**.
|
|
9
|
+
A proxy-based **fine-grained reactivity system** for TypeScript/JavaScript. No VDOM, no scheduler — changes propagate synchronously through dependency-tracked effects. Also provides: decorator unification, class mixins, event system, resource management, promise chaining, and zones (async context propagation).
|
|
15
10
|
|
|
16
|
-
|
|
17
|
-
2. **Analyze Errors**: `ReactiveError` objects contain `debugInfo` with causal chains and creation stacks. Use them to trace the fault.
|
|
18
|
-
3. **Verify**: Use `reactiveOptions.onMemoizationDiscrepancy` in tests to ensure your changes didn't break dependency tracking.
|
|
11
|
+
**Entry points**: `mutts/browser` (DOM), `mutts/node` (AsyncLocalStorage). Plain `import { ... } from 'mutts'` auto-selects via `package.json` exports.
|
|
19
12
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
2.
|
|
23
|
-
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 2. CORE REACTIVITY
|
|
16
|
+
|
|
17
|
+
### 2.1 reactive() — Make objects trackable
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { reactive, effect } from 'mutts'
|
|
21
|
+
|
|
22
|
+
const state = reactive({ count: 0, name: 'John' })
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- Wraps an object in a `Proxy` that tracks property reads (dependencies) and writes (notifications).
|
|
26
|
+
- Same object always returns the same proxy.
|
|
27
|
+
- Works with: plain objects, `Object.create(null)`, class instances, arrays, Map, Set, WeakMap, WeakSet.
|
|
28
|
+
- `unwrap(proxy)` recovers the original. `isReactive(obj)` checks.
|
|
29
|
+
|
|
30
|
+
### 2.2 effect() — React to changes
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
const stop = effect(({ reaction, tracked, ascend }) => {
|
|
34
|
+
console.log(state.count) // tracked automatically
|
|
35
|
+
return () => { /* cleanup before next run or disposal */ }
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
state.count++ // triggers effect
|
|
39
|
+
stop() // disposes effect permanently
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- **Auto-tracks**: any reactive property read inside the callback becomes a dependency.
|
|
43
|
+
- **Cleanup**: return a function — called before each re-run and on disposal.
|
|
44
|
+
- **`reaction`**: `false` on first run, `true` on subsequent triggers.
|
|
45
|
+
- **`tracked(fn)`**: restores tracking context in async/unmanaged callbacks.
|
|
46
|
+
- **`ascend(fn)`**: tracks dependencies in the *parent* effect instead.
|
|
47
|
+
- **Parent-child**: effects created inside other effects are children; disposing a parent disposes all children.
|
|
48
|
+
- **GC**: unreferenced top-level effects may be garbage-collected. Store the cleanup reference to keep alive.
|
|
49
|
+
|
|
50
|
+
**Modifiers** (chainable):
|
|
51
|
+
```ts
|
|
52
|
+
effect.opaque(() => { ... }) // identity-only tracking (no deep touch)
|
|
53
|
+
effect.named('label')(() => { ... }) // named for debugging
|
|
54
|
+
effect.opaque.named('x')(() => { ... })
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 2.3 memoize() — Cached computed values
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
const doubled = memoize((obj: { value: number }) => obj.value * 2)
|
|
61
|
+
// Arguments must be WeakMap-compatible (objects/symbols). No primitives.
|
|
62
|
+
// Cache invalidates when tracked reactive reads inside fn change.
|
|
63
|
+
// Same fn passed to memoize() multiple times returns same wrapper.
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
As decorator: `@memoize` on getters (per-instance cache) or methods (per-instance+args).
|
|
67
|
+
|
|
68
|
+
### 2.4 untracked() — Escape tracking
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
untracked(() => {
|
|
72
|
+
// Reactive reads here are NOT tracked by the enclosing effect
|
|
73
|
+
})
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 2.5 unreactive() — Opt out
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
unreactive(obj) // mark object as non-reactive
|
|
80
|
+
unreactive(MyClass) // mark entire class
|
|
81
|
+
@unreactive('id', 'meta') // mark specific properties on a @reactive class
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 2.6 atomic() — Batch mutations
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
const updateBoth = atomic((a, b) => {
|
|
88
|
+
state.a = a
|
|
89
|
+
state.b = b
|
|
90
|
+
// Effects fire ONCE after both mutations, not twice
|
|
91
|
+
})
|
|
92
|
+
updateBoth(10, 20)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Also works as `@atomic` decorator on class methods.
|
|
96
|
+
|
|
97
|
+
### 2.7 defer() / addBatchCleanup() — Avoid cycles
|
|
98
|
+
|
|
99
|
+
When an effect needs to mutate state it reads, defer the mutation:
|
|
100
|
+
```ts
|
|
101
|
+
effect(() => {
|
|
102
|
+
const len = state.items.length
|
|
103
|
+
defer(() => { state.processedCount = len }) // runs after batch completes
|
|
104
|
+
})
|
|
105
|
+
```
|
|
106
|
+
- Runs synchronously after outermost batch. FIFO order. No microtask delay.
|
|
107
|
+
- Outside a batch: executes immediately.
|
|
108
|
+
|
|
109
|
+
### 2.8 biDi() — Bidirectional binding
|
|
110
|
+
|
|
111
|
+
Bridges reactive state ↔ external (DOM, third-party). Prevents infinite loops automatically.
|
|
112
|
+
```ts
|
|
113
|
+
const provide = biDi(
|
|
114
|
+
(v) => inputElement.value = v, // external setter
|
|
115
|
+
{ get: () => model.value, set: (v) => model.value = v }
|
|
116
|
+
)
|
|
117
|
+
inputElement.addEventListener('input', () => provide(inputElement.value))
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### 2.9 watch() — Observe changes
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
// Watch a specific derivation
|
|
124
|
+
watch(() => state.count, (newVal, oldVal) => { ... })
|
|
125
|
+
|
|
126
|
+
// Watch any property on an object
|
|
127
|
+
watch(state, () => { ... })
|
|
128
|
+
|
|
129
|
+
// Deep watch (higher overhead)
|
|
130
|
+
watch(state, () => { ... }, { deep: true })
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## 3. COLLECTIONS
|
|
136
|
+
|
|
137
|
+
All standard collections get reactive wrappers via `reactive()`:
|
|
138
|
+
|
|
139
|
+
| Type | Tracking |
|
|
140
|
+
|------|----------|
|
|
141
|
+
| `reactive([])` | Per-index, `.length`, iteration (allProps) |
|
|
142
|
+
| `reactive(new Map())` | Per-key, `.size`, iteration |
|
|
143
|
+
| `reactive(new Set())` | Per-value, `.size`, iteration |
|
|
144
|
+
| `reactive(new WeakMap())` | Per-key only |
|
|
145
|
+
| `reactive(new WeakSet())` | Per-value only |
|
|
146
|
+
|
|
147
|
+
Array methods (`push`, `splice`, `sort`, etc.) all trigger reactivity.
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
### 3.1 Register — Keyed ordered collection
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
import { Register } from 'mutts/reactive'
|
|
154
|
+
|
|
155
|
+
const list = new Register(item => item.id, [
|
|
156
|
+
{ id: 1, label: 'Alpha' },
|
|
157
|
+
{ id: 2, label: 'Bravo' },
|
|
158
|
+
])
|
|
159
|
+
|
|
160
|
+
list.push({ id: 3, label: 'Charlie' })
|
|
161
|
+
list.get(2) // lookup by key, O(1)
|
|
162
|
+
list.set(2, newItem) // update by key
|
|
163
|
+
list.remove(2) // remove by key
|
|
164
|
+
list.keep(item => item.active) // filter in-place
|
|
165
|
+
list.upsert(v => list.push(v), ...items) // update or insert
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Full array surface (`map`, `filter`, `reduce`, `sort`, etc.) + CRUD events (`on('add', ...)`, `on('delete', ...)`, `on('update', ...)`, `on('rekey', ...)`).
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## 4. COLLECTION TRANSFORMS
|
|
173
|
+
|
|
174
|
+
### 4.1 project() — Per-entry reactive map
|
|
175
|
+
|
|
176
|
+
The reactive replacement for `.map()`. Each entry gets its own effect — only changed entries recompute.
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
import { project, cleanup } from 'mutts'
|
|
180
|
+
|
|
181
|
+
// Array
|
|
182
|
+
const names = project(users, ({ get }) => get().name.toUpperCase())
|
|
183
|
+
// names[0] recomputes only when users[0] changes
|
|
184
|
+
|
|
185
|
+
// Record
|
|
186
|
+
const grades = project.record(scores, ({ get }) => get() >= 90 ? 'A' : 'B')
|
|
187
|
+
|
|
188
|
+
// Map
|
|
189
|
+
const totals = project.map(inventory, ({ get }) => get().count)
|
|
190
|
+
|
|
191
|
+
// Auto-dispatch
|
|
192
|
+
const doubled = project(source, ({ get }) => get() * 2)
|
|
193
|
+
|
|
194
|
+
// Cleanup
|
|
195
|
+
names[cleanup]()
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Access object**: `{ get(), set(v), key, source, old, value }`
|
|
199
|
+
|
|
200
|
+
### 4.2 attend() — Per-key lifecycle
|
|
201
|
+
|
|
202
|
+
Creates inner effect per key. Disposed when key disappears.
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
import { attend } from 'mutts'
|
|
206
|
+
|
|
207
|
+
attend(reactiveRecord, (key) => {
|
|
208
|
+
console.log(`${key} = ${reactiveRecord[key]}`)
|
|
209
|
+
return () => console.log(`cleanup: ${key}`)
|
|
210
|
+
})
|
|
211
|
+
// Also works with: arrays, Maps, Sets, or raw () => Iterable<Key>
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### 4.3 organized() — Per-key record transform with side effects
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
const doubled = organized(source, (access, target) => {
|
|
218
|
+
target[access.key] = access.get() * 2
|
|
219
|
+
return () => delete target[access.key]
|
|
220
|
+
})
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### 4.4 describe() — Reactive Object.defineProperties
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
const descriptors = reactive({ foo: { value: 1, enumerable: true } })
|
|
227
|
+
const target = describe(descriptors) // target.foo === 1
|
|
228
|
+
descriptors.bar = { get: () => 42, enumerable: true } // target.bar appears
|
|
229
|
+
delete descriptors.foo // target.foo disappears
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### 4.5 scan() — Reactive accumulation
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
const result = scan(source, (acc, item) => acc + item.val, 0)
|
|
236
|
+
// result is reactive array of intermediates: [1, 3, 6]
|
|
237
|
+
// Changing source[1].val only recomputes from index 1 onward
|
|
238
|
+
result[cleanup]()
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Items must be objects (WeakMap keys). Move-optimized (reorders reuse cached intermediates).
|
|
242
|
+
|
|
243
|
+
### 4.6 lift() — Sync a computed array/object
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
const filtered = lift(() => items.filter(x => x.active))
|
|
247
|
+
// Only changed elements sync — element-wise diff, not full rebuild
|
|
248
|
+
filtered[cleanup]()
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## 5. EVOLUTION TRACKING
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
import { getState } from 'mutts/reactive'
|
|
257
|
+
|
|
258
|
+
let state = getState(obj)
|
|
259
|
+
effect(() => {
|
|
260
|
+
while ('evolution' in state) {
|
|
261
|
+
console.log(state.evolution) // { type: 'set'|'add'|'del'|'bunch', prop, method }
|
|
262
|
+
state = state.next
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Recursive touching**: replacing an object with another of the same prototype triggers fine-grained per-property diffs, not a wholesale parent notification. Disable with `reactiveOptions.recursiveTouching = false`.
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## 6. ERROR HANDLING
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
import { onEffectThrow } from 'mutts'
|
|
275
|
+
|
|
276
|
+
effect(() => {
|
|
277
|
+
onEffectThrow((error) => {
|
|
278
|
+
console.error('Caught:', error)
|
|
279
|
+
// return without throwing = handled
|
|
280
|
+
// throw = try next handler
|
|
281
|
+
// return function = cleanup on effect disposal
|
|
282
|
+
})
|
|
283
|
+
// ... code that might throw
|
|
284
|
+
})
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
- Multiple handlers tried in order. Unhandled errors propagate to parent effect chain.
|
|
288
|
+
- **Must register before throwing code** (handlers cleared on re-run).
|
|
289
|
+
- Does **not** catch async errors — use `.catch()` on Promises.
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## 7. DEBUGGING
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
import { reactiveOptions } from 'mutts/reactive'
|
|
297
|
+
|
|
298
|
+
// Cycle detection modes
|
|
299
|
+
reactiveOptions.cycleHandling = 'development' // default — graph-based, throws immediately
|
|
300
|
+
reactiveOptions.cycleHandling = 'debug' // full transitive closure, detailed paths
|
|
301
|
+
reactiveOptions.cycleHandling = 'production' // heuristic via maxTriggerPerBatch (fastest)
|
|
302
|
+
|
|
303
|
+
// Memoization discrepancy (double-run detection)
|
|
304
|
+
reactiveOptions.onMemoizationDiscrepancy = (cached, fresh, fn, args, cause) => {
|
|
305
|
+
throw new Error(`Discrepancy in ${fn.name}: ${cause}`)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Lifecycle hooks
|
|
309
|
+
reactiveOptions.enter = (effect) => { ... }
|
|
310
|
+
reactiveOptions.leave = (effect) => { ... }
|
|
311
|
+
reactiveOptions.touched = (obj, evolution) => { ... }
|
|
312
|
+
|
|
313
|
+
// Introspection (memory-intensive, dev only)
|
|
314
|
+
import { enableIntrospection, getDependencyGraph, getMutationHistory } from 'mutts/introspection'
|
|
315
|
+
enableIntrospection({ historySize: 100 })
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
**ReactiveError.debugInfo** contains: `causalChain`, `creationStack`, `cycle` (for CYCLE_DETECTED).
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
## 8. ZONES (Async Context)
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
import { Zone, asyncZone } from 'mutts/zone'
|
|
326
|
+
|
|
327
|
+
const requestId = new Zone<string>()
|
|
328
|
+
asyncZone.add(requestId) // register for async propagation
|
|
329
|
+
|
|
330
|
+
requestId.with('req-123', async () => {
|
|
331
|
+
await somePromise()
|
|
332
|
+
requestId.active // still 'req-123'
|
|
333
|
+
})
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
- `Zone<T>`: stack-based storage. `.with(value, fn)`, `.active`, `.root(fn)`.
|
|
337
|
+
- `ZoneHistory<T>`: tracks history for cycle detection.
|
|
338
|
+
- `ZoneAggregator`: combines multiple zones. `asyncZone` is the global one.
|
|
339
|
+
- `.zoned`: snapshot current context for manual bridging into unmanaged callbacks.
|
|
340
|
+
- Reactivity uses zones internally (`effectHistory` zone tracks active effect across await).
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
344
|
+
## 9. OTHER MODULES
|
|
345
|
+
|
|
346
|
+
### 9.1 Decorators (`mutts/decorator`)
|
|
347
|
+
|
|
348
|
+
Unified system for legacy (`experimentalDecorators`) and modern (Stage 3):
|
|
349
|
+
```ts
|
|
350
|
+
import { decorator } from 'mutts/decorator'
|
|
351
|
+
const myDec = decorator({ method(original, name) { ... }, class(target) { ... } })
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Standard decorators: `@cached`, `@debounce(ms)`, `@throttle(ms)`, `@deprecated(msg)`.
|
|
355
|
+
|
|
356
|
+
### 9.2 Mixin (`mutts/mixin`)
|
|
357
|
+
|
|
358
|
+
```ts
|
|
359
|
+
import { mixin } from 'mutts/mixin'
|
|
360
|
+
const Countable = mixin((base) => class extends base { count = 0; increment() { this.count++ } })
|
|
361
|
+
|
|
362
|
+
class A extends Countable { } // as base class
|
|
363
|
+
class B extends Countable(OtherBase) { } // as mixin
|
|
364
|
+
// Cached: same base always returns same mixed class
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### 9.3 Eventful (`mutts/eventful`)
|
|
368
|
+
|
|
369
|
+
Type-safe event system:
|
|
370
|
+
```ts
|
|
371
|
+
import { Eventful } from 'mutts/eventful'
|
|
372
|
+
interface MyEvents { click: (x: number, y: number) => void }
|
|
373
|
+
class Button extends Eventful<MyEvents> { }
|
|
374
|
+
|
|
375
|
+
const btn = new Button()
|
|
376
|
+
btn.on.click((x, y) => { ... }) // dot notation
|
|
377
|
+
btn.emit.click(100, 200)
|
|
378
|
+
btn.off.click()
|
|
379
|
+
const unhook = btn.hook((event, ...args) => { ... }) // global listener
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### 9.4 Destroyable (`mutts/destroyable`)
|
|
383
|
+
|
|
384
|
+
Resource management with `FinalizationRegistry` and `Symbol.dispose`:
|
|
385
|
+
```ts
|
|
386
|
+
import { Destroyable, allocated, destructor } from 'mutts/destroyable'
|
|
387
|
+
|
|
388
|
+
class FileHandler extends Destroyable() {
|
|
389
|
+
@allocated accessor filePath: string
|
|
390
|
+
[destructor](alloc) { console.log(`Closing: ${alloc.filePath}`) }
|
|
391
|
+
}
|
|
392
|
+
// Destroyed objects throw DestructionError on access.
|
|
393
|
+
// Explicit: Destroyable.destroy(instance) or future `using` statement.
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### 9.5 Flavored Functions (`mutts/flavored`)
|
|
397
|
+
|
|
398
|
+
Chainable property modifiers on functions:
|
|
399
|
+
```ts
|
|
400
|
+
import { flavored, flavorOptions } from 'mutts'
|
|
401
|
+
const greet = flavored(
|
|
402
|
+
(name: string, opts?: { loud?: boolean }) => opts?.loud ? name.toUpperCase() : name,
|
|
403
|
+
{ get loud() { return flavorOptions(this, { loud: true }) } }
|
|
404
|
+
)
|
|
405
|
+
greet.loud('hi') // "HI"
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### 9.6 Indexable (`mutts/indexable`)
|
|
409
|
+
|
|
410
|
+
Numeric index access (`obj[0]`) on custom classes via Proxy:
|
|
411
|
+
```ts
|
|
412
|
+
import { Indexable } from 'mutts/indexable'
|
|
413
|
+
const MyCollection = Indexable(BaseClass, {
|
|
414
|
+
get(index) { return this.data[index] },
|
|
415
|
+
set(index, v) { this.data[index] = v }
|
|
416
|
+
})
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### 9.7 PromiseChain (`mutts/promiseChain`)
|
|
420
|
+
|
|
421
|
+
Fluent chaining on Promises without intermediate `await`:
|
|
422
|
+
```ts
|
|
423
|
+
import { chainPromise } from 'mutts/promiseChain'
|
|
424
|
+
const theme = await chainPromise(api.getUser('123')).getProfile().getSettings().theme
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
## 10. PHILOSOPHY & TRAPS
|
|
430
|
+
|
|
431
|
+
### Affirmative State
|
|
432
|
+
Declare `Y = f(X)`. Do NOT say "when X changes, update Y". The system ensures consistency.
|
|
433
|
+
|
|
434
|
+
### Events Are Legacy
|
|
435
|
+
Only use events for DOM interaction or external APIs. Internal logic should use reactive derivations (`effect`, `memoize`, `project`).
|
|
436
|
+
|
|
437
|
+
### Cleanup Semantics
|
|
438
|
+
Cleanup functions release reactive subscriptions, NOT undo side effects. If an element is being removed, there's no point resetting its attributes.
|
|
439
|
+
|
|
440
|
+
### Key Traps
|
|
441
|
+
|
|
442
|
+
| Trap | Fix |
|
|
443
|
+
|------|-----|
|
|
444
|
+
| `memoize` with primitive args | Args must be objects/symbols (WeakMap keys) |
|
|
445
|
+
| Bare reactive reads lose context after `await` | Register zones in `asyncZone` or use `tracked(() => ...)` |
|
|
446
|
+
| Self-triggering effects | Use `defer()` / `addBatchCleanup()` |
|
|
447
|
+
| `queueMicrotask` for cycle avoidance | Use `defer()` instead (synchronous, batch-aware) |
|
|
448
|
+
| Deep watch overhead | Prefer explicit property reads + recursive touching |
|
|
449
|
+
| `_mutts_*` properties are debug/introspection helpers | Use them for logging, tracing, and profiling only — not as stable API |
|
|
24
450
|
|
package/docs/events.md
CHANGED
|
@@ -32,8 +32,9 @@ class MyClass extends Eventful<MyEvents> {
|
|
|
32
32
|
|
|
33
33
|
#### `on(events: Partial<Events>): void`
|
|
34
34
|
#### `on<EventType extends keyof Events>(event: EventType, cb: Events[EventType]): () => void`
|
|
35
|
+
#### `on.eventName(cb: Events['eventName']): () => void`
|
|
35
36
|
|
|
36
|
-
Registers event listeners. Can be called with either a single event and callback,
|
|
37
|
+
Registers event listeners. Can be called with either a single event and callback, an object containing multiple events, or using dot notation.
|
|
37
38
|
|
|
38
39
|
**Parameters:**
|
|
39
40
|
- `event`: The event name (when using single event overload)
|
|
@@ -49,6 +50,11 @@ const unsubscribe = myObject.on('userLogin', (userId, timestamp) => {
|
|
|
49
50
|
console.log(`User ${userId} logged in at ${timestamp}`)
|
|
50
51
|
})
|
|
51
52
|
|
|
53
|
+
// Dot notation (equivalent to above)
|
|
54
|
+
const unsubscribe = myObject.on.userLogin((userId, timestamp) => {
|
|
55
|
+
console.log(`User ${userId} logged in at ${timestamp}`)
|
|
56
|
+
})
|
|
57
|
+
|
|
52
58
|
// Multiple events
|
|
53
59
|
myObject.on({
|
|
54
60
|
userLogin: (userId, timestamp) => console.log('Login:', userId),
|
|
@@ -59,8 +65,9 @@ myObject.on({
|
|
|
59
65
|
|
|
60
66
|
#### `off(events: Partial<Events>): void`
|
|
61
67
|
#### `off<EventType extends keyof Events>(event: EventType, cb?: Events[EventType]): void`
|
|
68
|
+
#### `off.eventName(cb?: Events['eventName']): void`
|
|
62
69
|
|
|
63
|
-
Removes event listeners. Can be called with either a single event (and optional callback),
|
|
70
|
+
Removes event listeners. Can be called with either a single event (and optional callback), an object containing multiple events, or using dot notation.
|
|
64
71
|
|
|
65
72
|
**Parameters:**
|
|
66
73
|
- `event`: The event name (when using single event overload)
|
|
@@ -72,9 +79,15 @@ Removes event listeners. Can be called with either a single event (and optional
|
|
|
72
79
|
// Remove specific callback
|
|
73
80
|
myObject.off('userLogin', myCallback)
|
|
74
81
|
|
|
82
|
+
// Remove specific callback using dot notation
|
|
83
|
+
myObject.off.userLogin(myCallback)
|
|
84
|
+
|
|
75
85
|
// Remove all listeners for an event
|
|
76
86
|
myObject.off('userLogin')
|
|
77
87
|
|
|
88
|
+
// Remove all listeners for an event using dot notation
|
|
89
|
+
myObject.off.userLogin()
|
|
90
|
+
|
|
78
91
|
// Remove multiple events
|
|
79
92
|
myObject.off({
|
|
80
93
|
userLogin: myCallback,
|
|
@@ -83,6 +96,7 @@ myObject.off({
|
|
|
83
96
|
```
|
|
84
97
|
|
|
85
98
|
#### `emit<EventType extends keyof Events>(event: EventType, ...args: Parameters<Events[EventType]>): void`
|
|
99
|
+
#### `emit.eventName(...args: Parameters<Events['eventName']>): void`
|
|
86
100
|
|
|
87
101
|
Emits an event, calling all registered listeners and global hooks.
|
|
88
102
|
|
|
@@ -93,8 +107,13 @@ Emits an event, calling all registered listeners and global hooks.
|
|
|
93
107
|
**Example:**
|
|
94
108
|
```typescript
|
|
95
109
|
myObject.emit('userLogin', 'user123', new Date())
|
|
110
|
+
myObject.emit.userLogin('user123', new Date()) // Dot notation
|
|
111
|
+
|
|
96
112
|
myObject.emit('dataUpdate', [1, 2, 3])
|
|
113
|
+
myObject.emit.dataUpdate([1, 2, 3]) // Dot notation
|
|
114
|
+
|
|
97
115
|
myObject.emit('error', new Error('Something went wrong'))
|
|
116
|
+
myObject.emit.error(new Error('Something went wrong')) // Dot notation
|
|
98
117
|
```
|
|
99
118
|
|
|
100
119
|
#### `hook(cb: <EventType extends keyof Events>(event: EventType, ...args: Parameters<Events[EventType]>) => void): () => void`
|
|
@@ -113,6 +132,42 @@ const unsubscribe = myObject.hook((event, ...args) => {
|
|
|
113
132
|
})
|
|
114
133
|
```
|
|
115
134
|
|
|
135
|
+
## Dot Notation Syntax
|
|
136
|
+
|
|
137
|
+
For convenience, `on`, `off`, and `emit` all support a dot notation syntax that provides a cleaner alternative to the string-based API.
|
|
138
|
+
|
|
139
|
+
| String Notation | Dot Notation |
|
|
140
|
+
|----------------|--------------|
|
|
141
|
+
| `obj.on('event', cb)` | `obj.on.event(cb)` |
|
|
142
|
+
| `obj.off('event', cb)` | `obj.off.event(cb)` |
|
|
143
|
+
| `obj.off('event')` | `obj.off.event()` |
|
|
144
|
+
| `obj.emit('event', args)` | `obj.emit.event(args)` |
|
|
145
|
+
|
|
146
|
+
Both forms are functionally equivalent. The dot notation provides:
|
|
147
|
+
|
|
148
|
+
- **Cleaner syntax**: No quotes needed around event names
|
|
149
|
+
- **Better IDE support**: Autocomplete for event names
|
|
150
|
+
- **Type safety**: Full TypeScript inference for event arguments
|
|
151
|
+
|
|
152
|
+
**Example:**
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
class Button extends Eventful<{ click: (x: number, y: number) => void; hover: () => void }> {}
|
|
156
|
+
|
|
157
|
+
const button = new Button()
|
|
158
|
+
|
|
159
|
+
// Register listener using dot notation
|
|
160
|
+
button.on.click((x, y) => console.log(`Clicked at ${x}, ${y}`))
|
|
161
|
+
|
|
162
|
+
// Emit using dot notation
|
|
163
|
+
button.emit.click(100, 200)
|
|
164
|
+
|
|
165
|
+
// Remove listener using dot notation
|
|
166
|
+
button.off.click(myCallback)
|
|
167
|
+
// Or remove all click listeners
|
|
168
|
+
button.off.click()
|
|
169
|
+
```
|
|
170
|
+
|
|
116
171
|
## Usage Examples
|
|
117
172
|
|
|
118
173
|
### Basic Event Handling
|