mutts 1.0.8 → 1.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -35
- package/dist/browser.cjs +2144 -5355
- package/dist/browser.cjs.map +1 -1
- package/dist/browser.d.ts +2 -1655
- package/dist/browser.dev.cjs +110 -0
- package/dist/browser.dev.cjs.map +1 -0
- package/dist/browser.dev.d.ts +2 -0
- package/dist/browser.dev.esm.js +5 -0
- package/dist/browser.dev.esm.js.map +1 -0
- package/dist/browser.esm.js +20 -107
- package/dist/browser.esm.js.map +1 -1
- package/dist/chunks/{async-browser-CA0jPWIi.cjs → async-browser-BU_IfxYD.cjs} +19 -107
- package/dist/chunks/async-browser-BU_IfxYD.cjs.map +1 -0
- package/dist/chunks/{async-core-UqHzvJ-S.cjs → async-core-CRLKP3l-.cjs} +8 -4
- package/dist/chunks/async-core-CRLKP3l-.cjs.map +1 -0
- package/dist/chunks/{async-node-BYHuGTni.cjs → async-node-3PrbVAbB.cjs} +7 -12
- package/dist/chunks/async-node-3PrbVAbB.cjs.map +1 -0
- package/dist/chunks/index-CaaQQlPJ.esm.js +3174 -0
- package/dist/chunks/index-CaaQQlPJ.esm.js.map +1 -0
- package/dist/chunks/node-nKJBk8iJ.esm.js +96 -0
- package/dist/chunks/node-nKJBk8iJ.esm.js.map +1 -0
- package/dist/chunks/proxy-Dtg-bJ3T.cjs +3763 -0
- package/dist/chunks/proxy-Dtg-bJ3T.cjs.map +1 -0
- package/dist/chunks/proxy-r7lARftl.esm.js +3701 -0
- package/dist/chunks/proxy-r7lARftl.esm.js.map +1 -0
- package/dist/debug.cjs +980 -0
- package/dist/debug.cjs.map +1 -0
- package/dist/debug.d.ts +208 -0
- package/dist/debug.esm.js +950 -0
- package/dist/debug.esm.js.map +1 -0
- package/dist/devtools/manifest.json +1 -1
- package/dist/devtools/panel.html +1 -1
- package/dist/devtools/panel.js +107 -94
- package/dist/devtools/panel.js.map +1 -1
- package/dist/index.d.ts +1447 -13
- package/dist/mutts.umd.js +7167 -1
- package/dist/mutts.umd.js.map +1 -1
- package/dist/mutts.umd.min.js +1 -1
- package/dist/mutts.umd.min.js.map +1 -1
- package/dist/node.cjs +70 -66
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.ts +2 -1
- package/dist/node.dev.cjs +110 -0
- package/dist/node.dev.cjs.map +1 -0
- package/dist/node.dev.d.ts +2 -0
- package/dist/node.dev.esm.js +6 -0
- package/dist/node.dev.esm.js.map +1 -0
- package/dist/node.esm.js +4 -103
- package/dist/node.esm.js.map +1 -1
- package/dist/{reactive/types.d.ts → types-W5vD6m2n.d.ts} +186 -93
- package/docs/ai/api-reference.md +12 -19
- package/docs/ai/manual.md +521 -17
- package/docs/events.md +57 -2
- package/docs/flavored.md +210 -0
- package/docs/reactive/advanced.md +314 -5
- package/docs/reactive/attend.md +142 -0
- package/docs/reactive/collections.md +76 -39
- package/docs/reactive/core.md +100 -14
- package/docs/reactive/debugging.md +123 -18
- package/docs/reactive/error-handling.md +121 -0
- package/docs/reactive/resource.md +125 -0
- package/docs/reactive/scan.md +247 -1
- package/docs/reactive.md +4 -1
- package/docs/std-decorators.md +102 -51
- package/docs/utils.md +70 -0
- package/docs/zone.md +3 -7
- package/package.json +64 -30
- package/dist/async/browser.d.ts +0 -2
- package/dist/async/browser.d.ts.map +0 -1
- package/dist/async/index.d.ts +0 -18
- package/dist/async/index.d.ts.map +0 -1
- package/dist/async/node.d.ts +0 -2
- package/dist/async/node.d.ts.map +0 -1
- package/dist/chunks/async-browser-CA0jPWIi.cjs.map +0 -1
- package/dist/chunks/async-core-UqHzvJ-S.cjs.map +0 -1
- package/dist/chunks/async-node-BYHuGTni.cjs.map +0 -1
- package/dist/chunks/index-DhaOVusv.esm.js +0 -6411
- package/dist/chunks/index-DhaOVusv.esm.js.map +0 -1
- package/dist/decorator.d.ts +0 -106
- package/dist/decorator.d.ts.map +0 -1
- package/dist/destroyable.d.ts +0 -87
- package/dist/destroyable.d.ts.map +0 -1
- package/dist/entry-browser.d.ts +0 -3
- package/dist/entry-browser.d.ts.map +0 -1
- package/dist/entry-node.d.ts +0 -3
- package/dist/entry-node.d.ts.map +0 -1
- package/dist/eventful.d.ts +0 -18
- package/dist/eventful.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/indexable.d.ts +0 -243
- package/dist/indexable.d.ts.map +0 -1
- package/dist/introspection.d.ts +0 -27
- package/dist/introspection.d.ts.map +0 -1
- package/dist/iterableWeak.d.ts +0 -53
- package/dist/iterableWeak.d.ts.map +0 -1
- package/dist/mixins.d.ts +0 -25
- package/dist/mixins.d.ts.map +0 -1
- package/dist/promiseChain.d.ts +0 -20
- package/dist/promiseChain.d.ts.map +0 -1
- package/dist/reactive/array.d.ts +0 -49
- package/dist/reactive/array.d.ts.map +0 -1
- package/dist/reactive/buffer.d.ts +0 -44
- package/dist/reactive/buffer.d.ts.map +0 -1
- package/dist/reactive/change.d.ts +0 -29
- package/dist/reactive/change.d.ts.map +0 -1
- package/dist/reactive/debug.d.ts +0 -111
- package/dist/reactive/debug.d.ts.map +0 -1
- package/dist/reactive/deep-touch.d.ts +0 -28
- package/dist/reactive/deep-touch.d.ts.map +0 -1
- package/dist/reactive/deep-watch-state.d.ts +0 -25
- package/dist/reactive/deep-watch-state.d.ts.map +0 -1
- package/dist/reactive/deep-watch.d.ts +0 -19
- package/dist/reactive/deep-watch.d.ts.map +0 -1
- package/dist/reactive/effect-context.d.ts +0 -7
- package/dist/reactive/effect-context.d.ts.map +0 -1
- package/dist/reactive/effects.d.ts +0 -151
- package/dist/reactive/effects.d.ts.map +0 -1
- package/dist/reactive/index.d.ts +0 -20
- package/dist/reactive/index.d.ts.map +0 -1
- package/dist/reactive/interface.d.ts +0 -64
- package/dist/reactive/interface.d.ts.map +0 -1
- package/dist/reactive/map.d.ts +0 -30
- package/dist/reactive/map.d.ts.map +0 -1
- package/dist/reactive/memoize.d.ts +0 -5
- package/dist/reactive/memoize.d.ts.map +0 -1
- package/dist/reactive/non-reactive-state.d.ts +0 -9
- package/dist/reactive/non-reactive-state.d.ts.map +0 -1
- package/dist/reactive/non-reactive.d.ts +0 -11
- package/dist/reactive/non-reactive.d.ts.map +0 -1
- package/dist/reactive/project.d.ts +0 -41
- package/dist/reactive/project.d.ts.map +0 -1
- package/dist/reactive/proxy-state.d.ts +0 -8
- package/dist/reactive/proxy-state.d.ts.map +0 -1
- package/dist/reactive/proxy.d.ts +0 -23
- package/dist/reactive/proxy.d.ts.map +0 -1
- package/dist/reactive/record.d.ts +0 -116
- package/dist/reactive/record.d.ts.map +0 -1
- package/dist/reactive/register.d.ts +0 -64
- package/dist/reactive/register.d.ts.map +0 -1
- package/dist/reactive/registry.d.ts +0 -20
- package/dist/reactive/registry.d.ts.map +0 -1
- package/dist/reactive/set.d.ts +0 -28
- package/dist/reactive/set.d.ts.map +0 -1
- package/dist/reactive/tracking.d.ts +0 -7
- package/dist/reactive/tracking.d.ts.map +0 -1
- package/dist/reactive/types.d.ts.map +0 -1
- package/dist/std-decorators.d.ts +0 -50
- package/dist/std-decorators.d.ts.map +0 -1
- package/dist/utils.d.ts +0 -49
- package/dist/utils.d.ts.map +0 -1
- package/dist/zone.d.ts +0 -40
- package/dist/zone.d.ts.map +0 -1
- package/docs/reactive/project.md +0 -93
- package/src/async/browser.ts +0 -319
- package/src/async/index.ts +0 -23
- package/src/async/node.ts +0 -104
- package/src/decorator.ts +0 -272
- package/src/destroyable.ts +0 -199
- package/src/entry-browser.ts +0 -5
- package/src/entry-node.ts +0 -5
- package/src/eventful.ts +0 -77
- package/src/index.d.ts +0 -12
- package/src/index.ts +0 -64
- package/src/indexable.ts +0 -526
- package/src/introspection.ts +0 -59
- package/src/iterableWeak.ts +0 -233
- package/src/mixins.ts +0 -123
- package/src/promiseChain.ts +0 -110
- package/src/reactive/array.ts +0 -507
- package/src/reactive/buffer.ts +0 -168
- package/src/reactive/change.ts +0 -138
- package/src/reactive/debug.ts +0 -517
- package/src/reactive/deep-touch.ts +0 -268
- package/src/reactive/deep-watch-state.ts +0 -82
- package/src/reactive/deep-watch.ts +0 -168
- package/src/reactive/effect-context.ts +0 -18
- package/src/reactive/effects.ts +0 -1382
- package/src/reactive/index.ts +0 -75
- package/src/reactive/interface.ts +0 -210
- package/src/reactive/map.ts +0 -158
- package/src/reactive/memoize.ts +0 -163
- package/src/reactive/non-reactive-state.ts +0 -49
- package/src/reactive/non-reactive.ts +0 -43
- package/src/reactive/project.ts +0 -453
- package/src/reactive/proxy-state.ts +0 -27
- package/src/reactive/proxy.ts +0 -264
- package/src/reactive/record.ts +0 -196
- package/src/reactive/register.ts +0 -419
- package/src/reactive/registry.ts +0 -59
- package/src/reactive/set.ts +0 -130
- package/src/reactive/tracking.ts +0 -44
- package/src/reactive/types.ts +0 -440
- package/src/std-decorators.ts +0 -217
- package/src/utils.ts +0 -301
- package/src/zone.ts +0 -138
package/docs/ai/manual.md
CHANGED
|
@@ -1,24 +1,528 @@
|
|
|
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** — Everything needed to use `mutts` as a dependency. Single source of truth. Do not rely on prior training data. Companion: [api-reference.md](./api-reference.md) for types.
|
|
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. MENTAL MODEL
|
|
12
8
|
|
|
13
|
-
|
|
14
|
-
When you encounter reactivity bugs (infinite loops, unexpected updates), **DO NOT GUESS**.
|
|
9
|
+
Proxy-based **fine-grained reactivity** — changes propagate synchronously through dependency-tracked effects. No VDOM, no scheduler, no hooks.
|
|
15
10
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
| Concept | Mutts |
|
|
12
|
+
|---------|-------|
|
|
13
|
+
| State | `reactive(obj)` — proxy wraps, tracks reads/writes |
|
|
14
|
+
| Reactions | `effect(() => {})` — auto-tracks deps, re-runs on change |
|
|
15
|
+
| Computed | `memoize(fn)` — cached, invalidates on tracked deps |
|
|
16
|
+
| Collections | `project(arr, fn)` — per-entry effects, not `.map()` |
|
|
17
|
+
| Async context | `Zone` + `asyncZone` — propagates across await |
|
|
18
|
+
| Batching | `atomic(() => {})` wraps, `atom(() => {})` runs immediately — fire effects once after all mutations |
|
|
19
|
+
| Cleanup | Return fn from effect — runs before re-run/disposal |
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
1. **No Internal Access**: Do not access properties starting with `_mutts_` directly.
|
|
22
|
-
2. **Explicit Naming**: Always name your effects (`effect(fn, { name: '...' })`) to make future debugging easier for yourself or other agents.
|
|
23
|
-
3. **Affirmative State**: Prefer derived state and effects over imperative event-driven updates.
|
|
21
|
+
**All exports come from `'mutts'`** — no subpath imports like `mutts/reactive` or `mutts/zone`.
|
|
24
22
|
|
|
23
|
+
```ts
|
|
24
|
+
import { reactive, effect, memoize, morph, attend, lift, scan, cleanup,
|
|
25
|
+
atom, atomic, defer, untracked, unreactive, watch, when, biDi, caught, why,
|
|
26
|
+
cleanedBy, organized, Register,
|
|
27
|
+
Zone, asyncZone, ZoneHistory, ZoneAggregator,
|
|
28
|
+
decorator, mixin, Eventful, Destroyable, flavored, Indexable, chainPromise,
|
|
29
|
+
reactiveOptions, isReactive, unwrap, getState
|
|
30
|
+
} from 'mutts'
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Entry points**: `mutts` (auto-selects), `mutts/browser` (DOM), `mutts/node` (AsyncLocalStorage), `mutts/debug`.
|
|
34
|
+
|
|
35
|
+
**Singleton guard**: mutts throws if loaded twice (different bundles/versions). Ensure your bundler externalizes or aliases `mutts` to a single source.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 2. CRITICAL TRAPS
|
|
40
|
+
|
|
41
|
+
### TRAP 1: Memoize with primitives
|
|
42
|
+
`memoize` uses WeakMap — args MUST be objects/symbols, NOT primitives.
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
// BAD
|
|
46
|
+
const double = memoize((n: number) => n * 2) // WeakMap can't key on number
|
|
47
|
+
|
|
48
|
+
// GOOD
|
|
49
|
+
const double = memoize((obj: { n: number }) => obj.n * 2)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### TRAP 2: Reactive reads lose context after await
|
|
53
|
+
Effect tracking context is automatically propagated across async boundaries in Node.js (via `AsyncLocalStorage`). In browsers, monkey-patching is less robust — use `tracked` to be safe.
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
// Node.js — works automatically (effectHistory is pre-registered in asyncZone)
|
|
57
|
+
effect(async () => {
|
|
58
|
+
await fetch('/api')
|
|
59
|
+
console.log(state.count) // tracked in Node.js
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// Browser or explicit — use access.tracked to restore context
|
|
63
|
+
effect(({ tracked }) => {
|
|
64
|
+
someCallback(() => {
|
|
65
|
+
tracked(() => console.log(state.count)) // restore tracking context
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### TRAP 3: Self-triggering effects (cycles)
|
|
71
|
+
Effect reads and writes same state → infinite loop.
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
// BAD — cycle
|
|
75
|
+
effect(() => {
|
|
76
|
+
state.count = state.count + 1 // reads count, writes count → re-triggers
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// GOOD — defer mutation
|
|
80
|
+
effect(() => {
|
|
81
|
+
const val = state.count
|
|
82
|
+
defer(() => { state.processedCount = val }) // runs after batch
|
|
83
|
+
})
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### TRAP 4: Using queueMicrotask for cycle avoidance
|
|
87
|
+
`defer()` is batch-aware and synchronous. `queueMicrotask` breaks batching.
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
// BAD
|
|
91
|
+
effect(() => {
|
|
92
|
+
queueMicrotask(() => state.x = state.y) // async, breaks atomic batches
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
// GOOD
|
|
96
|
+
effect(() => {
|
|
97
|
+
defer(() => state.x = state.y) // sync after batch completes
|
|
98
|
+
})
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### TRAP 5: Array clearing
|
|
102
|
+
`array.length = 0` works (triggers all indices + length). `splice(0)` is equivalent.
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
array.length = 0 // works — touches all affected indices
|
|
106
|
+
array.splice(0) // also works — explicit clear
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### TRAP 6: Using .map() for reactive transforms
|
|
110
|
+
`.map()` is static — full rebuild on any change. Use `project()` for per-entry reactivity.
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
// BAD — full rebuild
|
|
114
|
+
const doubled = items.map(x => x.value * 2)
|
|
115
|
+
|
|
116
|
+
// GOOD — per-entry effects
|
|
117
|
+
const doubled = project(items, ({ get }) => get().value * 2)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### TRAP 7: Deep watch overhead
|
|
121
|
+
`watch(obj, fn, { deep: true })` tracks ALL nested properties. Prefer explicit reads + recursive touching (enabled by default).
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
// EXPENSIVE
|
|
125
|
+
watch(state, () => { ... }, { deep: true })
|
|
126
|
+
|
|
127
|
+
// BETTER — explicit property reads
|
|
128
|
+
effect(() => {
|
|
129
|
+
state.user.name // only tracks .user.name
|
|
130
|
+
})
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### TRAP 8: _mutts_* properties are debug helpers
|
|
134
|
+
`_mutts_*` properties are for introspection/logging ONLY — not stable API.
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
// OK for debugging
|
|
138
|
+
console.log(obj._mutts_watchers)
|
|
139
|
+
|
|
140
|
+
// BAD — don't build logic on it
|
|
141
|
+
if (obj._mutts_isReactive) { ... }
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## 3. CORE REACTIVITY
|
|
147
|
+
|
|
148
|
+
### 3.1 reactive() — Trackable state
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
const state = reactive({ count: 0, items: [1, 2, 3] })
|
|
152
|
+
// Same object → same proxy. Works: objects, arrays, Map, Set, WeakMap, WeakSet
|
|
153
|
+
// unwrap(proxy) → original. isReactive(obj) → boolean
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### 3.2 effect() — Auto-tracked reactions
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
const stop = effect(({ reaction, tracked, ascend }) => {
|
|
160
|
+
console.log(state.count) // auto-tracked
|
|
161
|
+
return () => { /* cleanup: before re-run or disposal */ }
|
|
162
|
+
})
|
|
163
|
+
state.count++ // triggers
|
|
164
|
+
stop() // disposes
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
- **Access object**: `{ reaction, tracked(fn), ascend(fn) }` — `reaction` = false on first run, true after. `tracked` restores context in async. `ascend` tracks in parent effect.
|
|
168
|
+
- **Parent-child**: effects inside effects are children — parent disposal cascades.
|
|
169
|
+
- **GC**: unreferenced top-level effects may GC — store `stop` to keep alive.
|
|
170
|
+
- **Modifiers**: `effect.opaque(() => {})` (identity-only), `effect.named('x')(() => {})` (debug label). Chainable.
|
|
171
|
+
|
|
172
|
+
### 3.3 memoize() — Cached computed
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
const doubled = memoize((obj: { value: number }) => obj.value * 2)
|
|
176
|
+
// Args MUST be objects/symbols (WeakMap keys). Cache invalidates on tracked deps.
|
|
177
|
+
// Decorator: @memoize on getters (per-instance) or methods (per-instance+args).
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### 3.4 untracked() — Escape tracking
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
untracked(() => { state.count }) // NOT tracked
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### 3.5 unreactive() — Opt out
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
unreactive(obj) // mark non-reactive
|
|
190
|
+
unreactive(MyClass) // entire class
|
|
191
|
+
@unreactive('id', 'meta') // specific props on @reactive class
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### 3.6 atomic() — Batch mutations
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
const updateBoth = atomic((a, b) => { state.a = a; state.b = b }) // effects fire once
|
|
198
|
+
// Decorator: @atomic on methods
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### 3.7 atom() — Immediate atomic execution
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
atom(() => { state.a = 1; state.b = 2 }) // runs now, effects fire once
|
|
205
|
+
// Unlike atomic() which wraps for later, atom() executes immediately
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### 3.8 defer() — Avoid cycles
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
effect(() => {
|
|
212
|
+
const len = state.items.length
|
|
213
|
+
defer(() => { state.processedCount = len }) // sync after batch, FIFO
|
|
214
|
+
})
|
|
215
|
+
// Outside batch: immediate. NO microtask delay.
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### 3.9 biDi() — Bidirectional binding
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
const provide = biDi(
|
|
222
|
+
(v) => input.value = v, // external setter
|
|
223
|
+
{ get: () => model.value, set: (v) => model.value = v }
|
|
224
|
+
)
|
|
225
|
+
input.addEventListener('input', () => provide(input.value))
|
|
226
|
+
// Prevents infinite loops automatically
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### 3.10 watch() — Observe changes
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
watch(() => state.count, (newVal, oldVal) => {}) // specific derivation
|
|
233
|
+
watch(state, () => {}) // any property
|
|
234
|
+
watch(state, () => {}, { deep: true }) // deep (expensive)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### 3.10 cleanedBy() — Attach cleanup to objects
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
const obj = cleanedBy({ data: [] }, () => console.log('cleaned up'))
|
|
241
|
+
obj[cleanup]() // triggers cleanup
|
|
242
|
+
// Chains: if obj already has a cleanup, both run
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### 3.11 Lazy computed values
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
// Lazy + trackable (preferred):
|
|
249
|
+
const total = memoize(() => state.a + state.b)
|
|
250
|
+
effect(() => console.log(total())) // recomputes only on read, dependency tracked
|
|
251
|
+
|
|
252
|
+
// Eager + trackable (stable reactive proxy):
|
|
253
|
+
const d = lift(() => ({ value: state.a + state.b }))
|
|
254
|
+
effect(() => console.log(d.value)) // recomputes immediately, .value is reactive
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### 3.12 when() — Promise-based reactive wait
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
await when(() => state.loaded) // resolves when truthy
|
|
261
|
+
await when(() => state.ready, { timeout: 5000 }) // rejects after 5s
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## 4. COLLECTIONS
|
|
267
|
+
|
|
268
|
+
| Type | Tracking |
|
|
269
|
+
|------|----------|
|
|
270
|
+
| `reactive([])` | Per-index, `.length`, iteration |
|
|
271
|
+
| `reactive(new Map())` | Per-key, `.size`, iteration |
|
|
272
|
+
| `reactive(new Set())` | Per-value, `.size`, iteration |
|
|
273
|
+
| `reactive(new WeakMap/Set())` | Per-key/value only |
|
|
274
|
+
|
|
275
|
+
Array methods (`push`, `splice`, `sort`, etc.) all trigger reactivity.
|
|
276
|
+
|
|
277
|
+
### 4.1 Register — Keyed ordered collection
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
const list = new Register(item => item.id, [{ id: 1, label: 'A' }])
|
|
281
|
+
list.get(1) // O(1) lookup by key
|
|
282
|
+
list.set(1, newItem) // update by key
|
|
283
|
+
list.remove(1) // remove by key
|
|
284
|
+
list.keep(x => x.active) // filter in-place
|
|
285
|
+
list.upsert(v => list.push(v), ...items) // update or insert
|
|
286
|
+
// Full array surface + CRUD events: on('add'|'delete'|'update'|'rekey', ...)
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## 5. COLLECTION TRANSFORMS
|
|
292
|
+
|
|
293
|
+
All transforms return reactive results. Cleanup via `result[cleanup]()`.
|
|
294
|
+
|
|
295
|
+
### 5.1 project() — Per-entry reactive map
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
const names = project(users, ({ get }) => get().name.toUpperCase())
|
|
299
|
+
// names[0] recomputes ONLY when users[0] changes
|
|
300
|
+
// Variants: project.record(), project.map() — auto-dispatches by source type
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
**Access object**: `{ get(), set(v), key, source, old, value }`
|
|
304
|
+
|
|
305
|
+
### 5.2 attend() — Per-key lifecycle
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
attend(reactiveRecord, (key) => {
|
|
309
|
+
console.log(`${key} = ${reactiveRecord[key]}`)
|
|
310
|
+
return () => console.log(`cleanup: ${key}`) // disposed when key disappears
|
|
311
|
+
})
|
|
312
|
+
// Works with: arrays, Maps, Sets, or raw () => Iterable<Key>
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### 5.3 organized() — Per-key record transform
|
|
316
|
+
|
|
317
|
+
```ts
|
|
318
|
+
const doubled = organized(source, (access, target) => {
|
|
319
|
+
target[access.key] = access.get() * 2
|
|
320
|
+
return () => delete target[access.key]
|
|
321
|
+
})
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### 5.4 scan() — Reactive accumulation
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
const result = scan(source, (acc, item) => acc + item.val, 0)
|
|
328
|
+
// [1, 3, 6] — changing source[1] recomputes from index 1 onward
|
|
329
|
+
// Items must be objects (WeakMap keys). Move-optimized.
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### 5.6 lift() — Sync a computed array/object
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
const filtered = lift(() => items.filter(x => x.active))
|
|
336
|
+
// Element-wise diff — only changed elements sync, not full rebuild
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
**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.
|
|
340
|
+
|
|
341
|
+
**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 `project()`/effects; use `memoize` for parameterized caching or lazy evaluation.
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## 6. EVOLUTION TRACKING
|
|
346
|
+
|
|
347
|
+
```ts
|
|
348
|
+
let state = getState(obj)
|
|
349
|
+
effect(() => {
|
|
350
|
+
while ('evolution' in state) {
|
|
351
|
+
console.log(state.evolution) // { type: 'set'|'add'|'del'|'bunch', prop, method }
|
|
352
|
+
state = state.next
|
|
353
|
+
}
|
|
354
|
+
})
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
**Recursive touching**: replacing an object with another of the same prototype triggers fine-grained per-property diffs, not wholesale notification. Disable: `reactiveOptions.recursiveTouching = false`.
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## 7. ERROR HANDLING
|
|
362
|
+
|
|
363
|
+
```ts
|
|
364
|
+
effect(() => {
|
|
365
|
+
caught((error) => {
|
|
366
|
+
// return without throwing = handled
|
|
367
|
+
// throw = try next handler
|
|
368
|
+
// return function = cleanup on disposal
|
|
369
|
+
})
|
|
370
|
+
// ... code that might throw
|
|
371
|
+
})
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
- Multiple handlers, tried in order. Unhandled → propagate to parent effect.
|
|
375
|
+
- **Must register before throwing code** (cleared on re-run).
|
|
376
|
+
- Does **not** catch async errors — use `.catch()`.
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
## 8. DEBUGGING & OPTIONS
|
|
381
|
+
|
|
382
|
+
```ts
|
|
383
|
+
import { reactiveOptions } from 'mutts'
|
|
384
|
+
|
|
385
|
+
// Cycle detection
|
|
386
|
+
reactiveOptions.cycleHandling = 'development' // default: graph-based, throws
|
|
387
|
+
reactiveOptions.cycleHandling = 'debug' // full transitive closure, detailed paths
|
|
388
|
+
reactiveOptions.cycleHandling = 'production' // heuristic via maxTriggerPerBatch
|
|
389
|
+
|
|
390
|
+
// Lifecycle hooks (all wrapped via optionCall for safety)
|
|
391
|
+
reactiveOptions.enter = (effect) => {} // before effect runs
|
|
392
|
+
reactiveOptions.leave = (effect) => {} // after effect runs
|
|
393
|
+
reactiveOptions.touched = (obj, evolution, props, effects) => {}
|
|
394
|
+
reactiveOptions.beginChain = (roots) => {} // before batch
|
|
395
|
+
reactiveOptions.garbageCollected = (fn) => {} // effect GC'd
|
|
396
|
+
reactiveOptions.skipRunningEffect = (fn) => {} // effect skipped (already running)
|
|
397
|
+
reactiveOptions.onMemoizationDiscrepancy = (cached, fresh, fn, args, cause) => {}
|
|
398
|
+
|
|
399
|
+
// Introspection (memory-intensive, dev only)
|
|
400
|
+
import { reactiveOptions } from 'mutts'
|
|
401
|
+
import { buildReactivityGraph, getMutationHistory } from 'mutts/debug'
|
|
402
|
+
|
|
403
|
+
reactiveOptions.introspection = { enableHistory: true, historySize: 100 }
|
|
404
|
+
const history = getMutationHistory()
|
|
405
|
+
const graph = buildReactivityGraph()
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
**ReactiveError.debugInfo**: `causalChain`, `creationStack`, `cycle` (for CYCLE_DETECTED).
|
|
409
|
+
|
|
410
|
+
---
|
|
411
|
+
|
|
412
|
+
## 9. ZONES (Async Context)
|
|
413
|
+
|
|
414
|
+
```ts
|
|
415
|
+
import { Zone, asyncZone } from 'mutts'
|
|
416
|
+
|
|
417
|
+
const requestId = new Zone<string>()
|
|
418
|
+
asyncZone.add(requestId) // register for async propagation
|
|
419
|
+
|
|
420
|
+
requestId.with('req-123', async () => {
|
|
421
|
+
await somePromise()
|
|
422
|
+
requestId.active // still 'req-123'
|
|
423
|
+
})
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
- **`Zone<T>`**: stack-based. `.with(value, fn)`, `.active`, `.root(fn)`.
|
|
427
|
+
- **`ZoneHistory<T>`**: extends Zone, tracks history set for cycle detection.
|
|
428
|
+
- **`ZoneAggregator`**: combines zones. `asyncZone` is the global one.
|
|
429
|
+
- **`.zoned`**: snapshot context for manual bridging into unmanaged callbacks.
|
|
430
|
+
- Reactivity uses zones internally (`effectHistory` tracks active effect across await).
|
|
431
|
+
- **Node**: uses `AsyncLocalStorage`. **Browser**: monkey-patches Promise/setTimeout (less robust).
|
|
432
|
+
|
|
433
|
+
---
|
|
434
|
+
|
|
435
|
+
## 10. OTHER MODULES
|
|
436
|
+
|
|
437
|
+
### 10.1 Decorators
|
|
438
|
+
|
|
439
|
+
```ts
|
|
440
|
+
const myDec = decorator({ method(original, name) { ... }, class(target) { ... } })
|
|
441
|
+
// Works with both legacy (experimentalDecorators) and Stage 3 decorators
|
|
442
|
+
// Built-in: @cached, @debounce(ms), @throttle(ms), @deprecated(msg)
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### 10.2 Mixin
|
|
446
|
+
|
|
447
|
+
```ts
|
|
448
|
+
const Countable = mixin((base) => class extends base { count = 0 })
|
|
449
|
+
class A extends Countable { } // as base
|
|
450
|
+
class B extends Countable(Other) { } // as mixin (cached per base)
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
### 10.3 Eventful
|
|
454
|
+
|
|
455
|
+
```ts
|
|
456
|
+
interface MyEvents { click: (x: number, y: number) => void }
|
|
457
|
+
class Button extends Eventful<MyEvents> { }
|
|
458
|
+
btn.on.click((x, y) => {}) // dot notation subscribe
|
|
459
|
+
btn.emit.click(100, 200) // emit
|
|
460
|
+
btn.off.click() // unsubscribe all
|
|
461
|
+
btn.hook((event, ...args) => {}) // global listener
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
### 10.4 Destroyable
|
|
465
|
+
|
|
466
|
+
```ts
|
|
467
|
+
class FileHandler extends Destroyable() {
|
|
468
|
+
@allocated accessor filePath: string
|
|
469
|
+
[destructor](alloc) { console.log(`Closing: ${alloc.filePath}`) }
|
|
470
|
+
}
|
|
471
|
+
// Destroyed objects throw DestructionError on access
|
|
472
|
+
// Destroyable.destroy(instance) or `using` statement
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
### 10.5 Flavored Functions
|
|
476
|
+
|
|
477
|
+
```ts
|
|
478
|
+
const greet = flavored(
|
|
479
|
+
(name: string, opts?: { loud?: boolean }) => opts?.loud ? name.toUpperCase() : name,
|
|
480
|
+
{ get loud() { return flavorOptions(this, { loud: true }) } }
|
|
481
|
+
)
|
|
482
|
+
greet.loud('hi') // "HI" — chainable property modifiers
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
### 10.6 Indexable
|
|
486
|
+
|
|
487
|
+
```ts
|
|
488
|
+
const MyCol = Indexable(Base, {
|
|
489
|
+
get(i) { return this.data[i] },
|
|
490
|
+
set(i, v) { this.data[i] = v }
|
|
491
|
+
})
|
|
492
|
+
// Enables obj[0] numeric index access on custom classes via Proxy
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### 10.7 PromiseChain
|
|
496
|
+
|
|
497
|
+
```ts
|
|
498
|
+
const theme = await chainPromise(api.getUser('123')).getProfile().getSettings().theme
|
|
499
|
+
// Fluent chaining on Promises without intermediate await
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
---
|
|
503
|
+
|
|
504
|
+
## 11. PHILOSOPHY
|
|
505
|
+
|
|
506
|
+
- **Affirmative state**: Declare `Y = f(X)`. Don't say "when X changes, update Y".
|
|
507
|
+
- **Events are legacy**: Use reactive derivations (`effect`, `memoize`, `project`) for internal logic. Events only for DOM/external APIs.
|
|
508
|
+
- **Cleanup ≠ undo**: Cleanup releases subscriptions, does NOT undo side effects.
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
## 12. QUICK REFERENCE — DO vs DON'T
|
|
513
|
+
|
|
514
|
+
| DO | DON'T |
|
|
515
|
+
|----|-------|
|
|
516
|
+
| `morph(arr, fn)` | `arr.map(fn)` for reactive transforms |
|
|
517
|
+
| `effect(() => { state.x })` | bare `state.x` outside effect |
|
|
518
|
+
| `defer(() => state.y = val)` | `queueMicrotask(() => state.y = val)` |
|
|
519
|
+
| `memoize((obj) => obj.n * 2)` | `memoize((n: number) => n * 2)` |
|
|
520
|
+
| `atom(() => { a=1; b=2 })` | sequential mutations (2 effect runs) |
|
|
521
|
+
| `untracked(() => state.x)` | reading state you don't want tracked |
|
|
522
|
+
| `watch(() => state.x, cb)` | manual dirty-checking |
|
|
523
|
+
| `arr.splice(0)` or `arr.length = 0` | manual loop to clear arrays |
|
|
524
|
+
| `isReactive(obj)` to check | `obj._mutts_isReactive` |
|
|
525
|
+
| Store `stop = effect(...)` | letting effect GC unintentionally |
|
|
526
|
+
| `import { x } from 'mutts'` | `import { x } from 'mutts/reactive'` (no subpaths) |
|
|
527
|
+
| `cleanedBy(obj, fn)` for cleanup | manual `obj[cleanup] = fn` |
|
|
528
|
+
| `memoize(() => a + b)` for computed | `effect` + manual state sync |
|
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
|