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/reactive/scan.md
CHANGED
|
@@ -42,7 +42,7 @@ The implementation consists of:
|
|
|
42
42
|
2. **Intermediates**: Class instances that link `val` and `prev`. They expose an `acc` getter decorated with `@memoize`.
|
|
43
43
|
3. **Index Sync Effects**: Granular effects (one per result index) that subscribe to `indexToIntermediate[i].acc`.
|
|
44
44
|
|
|
45
|
-
This "
|
|
45
|
+
This "Morph-like" architecture ensures that the main loop only does structural work, while the actual logic propagation is handled by the dependency chain of the intermediates.
|
|
46
46
|
|
|
47
47
|
## API Reference
|
|
48
48
|
|
|
@@ -76,3 +76,249 @@ result[cleanup]()
|
|
|
76
76
|
| **Modify Item at `i`** | O(N) calls (entire reduction) | O(N-i) calls |
|
|
77
77
|
| **Append Item** | O(N+1) calls | 1 call |
|
|
78
78
|
| **Move Item** | O(N) calls | O(affected chain) |
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
# Lift
|
|
83
|
+
|
|
84
|
+
The `lift` function transforms a callback that returns an array or object into a reactive array/object that automatically synchronizes with the source whenever dependencies change.
|
|
85
|
+
|
|
86
|
+
## Overview
|
|
87
|
+
|
|
88
|
+
`lift` is useful when you have a reactive computation that produces an array or object, and you want that result to be reactive itself. It efficiently syncs only the elements that differ from the previous result, minimizing DOM updates and downstream effects.
|
|
89
|
+
|
|
90
|
+
## Basic Usage
|
|
91
|
+
|
|
92
|
+
### Array Example
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import { reactive, lift } from 'mutts/reactive'
|
|
96
|
+
|
|
97
|
+
const items = reactive([1, 2, 3])
|
|
98
|
+
const doubled = lift(() => items.map(x => x * 2))
|
|
99
|
+
|
|
100
|
+
console.log([...doubled]) // [2, 4, 6]
|
|
101
|
+
|
|
102
|
+
items.push(4)
|
|
103
|
+
console.log([...doubled]) // [2, 4, 6, 8]
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Object Example
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import { reactive, lift } from 'mutts/reactive'
|
|
110
|
+
|
|
111
|
+
const user = reactive({ name: 'John', age: 30 })
|
|
112
|
+
const profile = lift(() => ({
|
|
113
|
+
displayName: user.name.toUpperCase(),
|
|
114
|
+
isAdult: user.age >= 18,
|
|
115
|
+
description: `${user.name} is ${user.age} years old`
|
|
116
|
+
}))
|
|
117
|
+
|
|
118
|
+
console.log(profile.displayName) // JOHN
|
|
119
|
+
console.log(profile.isAdult) // true
|
|
120
|
+
|
|
121
|
+
user.name = 'Jane'
|
|
122
|
+
console.log(profile.displayName) // JANE
|
|
123
|
+
console.log(profile.description) // Jane is 30 years old
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## How it Works
|
|
127
|
+
|
|
128
|
+
`lift` creates a reactive array or object and sets up an effect that:
|
|
129
|
+
1. Calls the provided callback to get the source array or object
|
|
130
|
+
2. Compares the source with the current reactive result
|
|
131
|
+
3. Updates only the elements/properties that have changed
|
|
132
|
+
4. Adjusts the structure if needed (array length or object properties)
|
|
133
|
+
|
|
134
|
+
For arrays, this approach preserves references to unchanged elements and triggers minimal reactive updates. For objects, it uses `Object.assign()` to merge changes and removes properties that no longer exist in the source.
|
|
135
|
+
|
|
136
|
+
## API Reference
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
function lift<Output extends (any[] | object)>(
|
|
140
|
+
cb: (access: EffectAccess) => Output
|
|
141
|
+
): Output & { [cleanup]: ScopedCallback }
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Parameters
|
|
145
|
+
- `cb`: A callback function that returns an array or object. The callback is tracked reactively, so accessing reactive values inside it will cause the result to update when those values change. The callback receives an `EffectAccess` parameter for advanced use cases.
|
|
146
|
+
|
|
147
|
+
### Returns
|
|
148
|
+
A reactive array or object that stays synchronized with the callback's result. The result includes a `[cleanup]` symbol that can be called to stop tracking.
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
import { cleanup } from 'mutts/reactive'
|
|
152
|
+
// ...
|
|
153
|
+
doubled[cleanup]()
|
|
154
|
+
profile[cleanup]()
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Use Cases
|
|
158
|
+
|
|
159
|
+
### Dynamic Filtering (Arrays)
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
const allItems = reactive([
|
|
163
|
+
{ id: 1, active: true, name: 'Item 1' },
|
|
164
|
+
{ id: 2, active: false, name: 'Item 2' },
|
|
165
|
+
{ id: 3, active: true, name: 'Item 3' },
|
|
166
|
+
])
|
|
167
|
+
|
|
168
|
+
const activeItems = lift(() => allItems.filter(item => item.active))
|
|
169
|
+
|
|
170
|
+
// activeItems automatically updates when items change or active status changes
|
|
171
|
+
allItems[1].active = true
|
|
172
|
+
console.log(activeItems.length) // 3
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Computed Transformations (Arrays)
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
const numbers = reactive([1, 2, 3, 4, 5])
|
|
179
|
+
const multiplier = reactive({ value: 2 })
|
|
180
|
+
|
|
181
|
+
const scaled = lift(() => numbers.map(n => n * multiplier.value))
|
|
182
|
+
|
|
183
|
+
multiplier.value = 3
|
|
184
|
+
// scaled is now [3, 6, 9, 12, 15]
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Conditional Array Construction
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
const showExtras = reactive({ value: false })
|
|
191
|
+
const baseItems = reactive(['A', 'B', 'C'])
|
|
192
|
+
|
|
193
|
+
const displayItems = lift(() =>
|
|
194
|
+
showExtras.value
|
|
195
|
+
? [...baseItems, 'Extra 1', 'Extra 2']
|
|
196
|
+
: baseItems
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
showExtras.value = true
|
|
200
|
+
// displayItems is now ['A', 'B', 'C', 'Extra 1', 'Extra 2']
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Computed Object Properties
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
const user = reactive({ firstName: 'John', lastName: 'Doe', age: 30 })
|
|
207
|
+
const settings = reactive({ theme: 'dark', language: 'en' })
|
|
208
|
+
|
|
209
|
+
const userProfile = lift(() => ({
|
|
210
|
+
fullName: `${user.firstName} ${user.lastName}`,
|
|
211
|
+
isMinor: user.age < 18,
|
|
212
|
+
displayTheme: settings.theme === 'dark' ? 'Dark Mode' : 'Light Mode',
|
|
213
|
+
locale: settings.language.toUpperCase()
|
|
214
|
+
}))
|
|
215
|
+
|
|
216
|
+
user.firstName = 'Jane'
|
|
217
|
+
// userProfile.fullName is now 'Jane Doe'
|
|
218
|
+
|
|
219
|
+
settings.theme = 'light'
|
|
220
|
+
// userProfile.displayTheme is now 'Light Mode'
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Dynamic Object Composition
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
const baseConfig = reactive({ api: 'https://api.example.com', timeout: 5000 })
|
|
227
|
+
const userPrefs = reactive({ retries: 3, logging: false })
|
|
228
|
+
const envVars = reactive({ debug: true, version: '1.0.0' })
|
|
229
|
+
|
|
230
|
+
const fullConfig = lift(() => ({
|
|
231
|
+
...baseConfig,
|
|
232
|
+
...userPrefs,
|
|
233
|
+
environment: envVars.debug ? 'development' : 'production',
|
|
234
|
+
version: envVars.version,
|
|
235
|
+
logging: envVars.debug || userPrefs.logging
|
|
236
|
+
}))
|
|
237
|
+
|
|
238
|
+
envVars.debug = false
|
|
239
|
+
// fullConfig.environment becomes 'production'
|
|
240
|
+
|
|
241
|
+
userPrefs.logging = true
|
|
242
|
+
// fullConfig.logging becomes true
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Conditional Object Properties
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
const user = reactive({ role: 'admin', permissions: ['read', 'write'] })
|
|
249
|
+
const showAdvanced = reactive({ value: true })
|
|
250
|
+
|
|
251
|
+
const userInterface = lift(() => {
|
|
252
|
+
const base = {
|
|
253
|
+
canEdit: user.permissions.includes('write'),
|
|
254
|
+
userName: user.role
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return showAdvanced.value ? {
|
|
258
|
+
...base,
|
|
259
|
+
isAdmin: user.role === 'admin',
|
|
260
|
+
permissionCount: user.permissions.length
|
|
261
|
+
} : base
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
showAdvanced.value = false
|
|
265
|
+
// userInterface no longer has isAdmin and permissionCount properties
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## Comparison with `scan`
|
|
269
|
+
|
|
270
|
+
| Feature | `lift` | `scan` |
|
|
271
|
+
| :--- | :--- | :--- |
|
|
272
|
+
| **Purpose** | Synchronize with computed arrays/objects | Accumulate values with intermediates |
|
|
273
|
+
| **Input** | Callback returning array/object | Source array + accumulator function |
|
|
274
|
+
| **Output** | Reactive array/object | Reactive array of accumulated values |
|
|
275
|
+
| **Optimization** | Element-wise/property-wise sync | Intermediate caching + move optimization |
|
|
276
|
+
| **Use Case** | Derived arrays/objects (map, filter, computed properties) | Cumulative operations (sum, reduce) |
|
|
277
|
+
| **Data Types** | Arrays and objects | Arrays only (object items required) |
|
|
278
|
+
|
|
279
|
+
## Comparison with Recursive Touching (Deep Touch)
|
|
280
|
+
|
|
281
|
+
When you assign a new array/object to a reactive property (`state.items = newArray`), the reactive system performs a **recursive touch** — it diffs old vs new element-by-element and fires per-index notifications on the *same proxy*. This raises the question: is `lift` redundant?
|
|
282
|
+
|
|
283
|
+
| | Recursive Touching | `lift` |
|
|
284
|
+
| :--- | :--- | :--- |
|
|
285
|
+
| **Trigger** | Direct assignment to a reactive property | Any reactive dependency change inside the callback |
|
|
286
|
+
| **Scope** | Same-shape replacement of one value | Arbitrary computation → stable reactive output |
|
|
287
|
+
| **Identity** | Same proxy, same object | Returns a **new persistent proxy** that outlives re-evaluations |
|
|
288
|
+
| **Use case** | `state.user = fetchedUser` — fine-grained diff on assignment | `lift(() => items.filter(x => x.active))` — derived collection |
|
|
289
|
+
|
|
290
|
+
Deep touching makes `lift` unnecessary for **replacement** patterns (`state.items = newItems`). `lift` remains essential for **derived collections** where the result is a transformation (filter, map, reshape) rather than a direct assignment — there is no single property to assign to, and the whole output is recomputed from scratch each time.
|
|
291
|
+
|
|
292
|
+
## Comparison with `memoize`
|
|
293
|
+
|
|
294
|
+
Both `lift` and `memoize` compute derived values from reactive dependencies, but they differ in evaluation strategy and output type.
|
|
295
|
+
|
|
296
|
+
| | `memoize` | `lift` |
|
|
297
|
+
| :--- | :--- | :--- |
|
|
298
|
+
| **Evaluation** | Lazy — invalidates on dep change, recomputes on next read | Eager — recomputes immediately on dep change |
|
|
299
|
+
| **Return type** | The raw return value of the function | A **stable reactive proxy** (array or object) |
|
|
300
|
+
| **Downstream reactivity** | Consumers get a new value each time (identity changes) | Consumers see per-property/per-index diffs on the *same* proxy |
|
|
301
|
+
| **Arguments** | Keyed by object args (WeakMap cache tree) | No args — closure over reactive deps |
|
|
302
|
+
| **Decorator** | Yes (`@memoize` on getters/methods) | No |
|
|
303
|
+
| **Cleanup** | Automatic (WeakMap GC) | Explicit `result[cleanup]()` |
|
|
304
|
+
|
|
305
|
+
**When to use which:**
|
|
306
|
+
- **`lift`** for derived collections where downstream consumers (e.g., `morph()`, effects) benefit from per-element diffing on a stable proxy.
|
|
307
|
+
- **`memoize`** for parameterized caching (`memoize((user) => expensiveCompute(user))`) or lazy evaluation where recomputation should only happen on access.
|
|
308
|
+
- For a scalar result read in one place, they are nearly interchangeable — prefer `memoize` for its laziness and automatic cleanup.
|
|
309
|
+
|
|
310
|
+
## Performance Considerations
|
|
311
|
+
|
|
312
|
+
### Arrays
|
|
313
|
+
- **Efficient Updates**: Only changed elements are updated, not the entire array
|
|
314
|
+
- **Length Adjustments**: Array length changes are handled separately from element updates
|
|
315
|
+
- **Reference Stability**: Unchanged elements maintain their references
|
|
316
|
+
|
|
317
|
+
### Objects
|
|
318
|
+
- **Property-wise Updates**: Only changed properties are updated using `Object.assign()`
|
|
319
|
+
- **Property Addition/Removal**: Properties are added or removed as needed when the source object structure changes
|
|
320
|
+
- **Reference Stability**: The reactive object maintains its identity while properties are updated
|
|
321
|
+
|
|
322
|
+
### General
|
|
323
|
+
- **Cleanup**: Remember to call the cleanup function when the lifted array/object is no longer needed to prevent memory leaks
|
|
324
|
+
- **Type Consistency**: The callback must return the same type (array or object) on subsequent calls
|
package/docs/reactive.md
CHANGED
|
@@ -11,10 +11,13 @@ The Mutts Reactive System documentation has been split into focused sections for
|
|
|
11
11
|
* **[Reactive Collections](./reactive/collections.md#collections)**: Map, Set, WeakMap, WeakSet
|
|
12
12
|
* **[Reactive Arrays](./reactive/collections.md#reactivearray)**: Full array method support
|
|
13
13
|
* **[Register](./reactive/collections.md#register)**: ID-keyed ordered collections
|
|
14
|
-
* **[
|
|
14
|
+
* **[Morphing](./reactive/collections.md#morph)**: `morph`, `organized`
|
|
15
|
+
* **[Attend](./reactive/attend.md)**: Reactive enumeration (`attend`)
|
|
15
16
|
* **[Scan](./reactive/scan.md)**: Reactive scan and accumulation
|
|
17
|
+
* **[Resource](./reactive/resource.md)**: Async state tracking (`resource`)
|
|
16
18
|
|
|
17
19
|
## [Advanced Topics](./reactive/advanced.md)
|
|
20
|
+
* **[Choosing the Right Primitive](./reactive/advanced.md#choosing-the-right-reactive-primitive)**: Comparison table of effect-value functions (memoize, lift, project, scan, etc.)
|
|
18
21
|
* **[Atomic Operations](./reactive/advanced.md#atomic-operations)**: Batching and Bidirectional binding
|
|
19
22
|
* **[Evolution Tracking](./reactive/advanced.md#evolution-tracking)**: History introspection
|
|
20
23
|
* **[Prototype Chains](./reactive/advanced.md#prototype-chains-and-pure-objects)**: Advanced inheritance patterns
|
package/docs/std-decorators.md
CHANGED
|
@@ -189,13 +189,13 @@ When the decorator is applied to a non-getter method:
|
|
|
189
189
|
3. **Memory overhead**: Cached values are stored indefinitely
|
|
190
190
|
4. **Getter-only**: Can only be applied to getter methods, not setters or regular methods
|
|
191
191
|
|
|
192
|
-
##
|
|
192
|
+
## Descriptor
|
|
193
193
|
|
|
194
|
-
The `
|
|
194
|
+
The `descriptor` decorator provides a clean, reusable way to configure property descriptors (enumerable, configurable, writable) for class properties. This decorator uses a functional approach that makes it easy to create reusable descriptor configurations.
|
|
195
195
|
|
|
196
196
|
## API Reference
|
|
197
197
|
|
|
198
|
-
### `
|
|
198
|
+
### `descriptor(descriptor: PropertyDescriptor): <T>(...properties: (keyof T)[]) => GenericClassDecorator<T>`
|
|
199
199
|
|
|
200
200
|
A function that creates a decorator to configure property descriptors for specified properties.
|
|
201
201
|
|
|
@@ -207,29 +207,68 @@ A function that creates a decorator to configure property descriptors for specif
|
|
|
207
207
|
|
|
208
208
|
**Returns:** A function that takes property names and returns a class decorator
|
|
209
209
|
|
|
210
|
+
**Flavors:**
|
|
211
|
+
The `descriptor` function is flavored with the following chainable properties:
|
|
212
|
+
- `.enumerable` - Sets enumerable to true
|
|
213
|
+
- `.hidden` - Sets enumerable to false
|
|
214
|
+
- `.configurable` - Sets configurable to true
|
|
215
|
+
- `.frozen` - Sets configurable to false
|
|
216
|
+
- `.writable` - Sets writable to true
|
|
217
|
+
- `.readonly` - Sets writable to false
|
|
218
|
+
|
|
210
219
|
**Usage Pattern:**
|
|
211
220
|
```typescript
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
221
|
+
// Using flavors for single properties
|
|
222
|
+
@descriptor.readonly('id', 'createdAt')
|
|
223
|
+
@descriptor.hidden('_private')
|
|
224
|
+
class MyClass { }
|
|
215
225
|
|
|
216
|
-
|
|
217
|
-
@
|
|
218
|
-
@locked('critical')
|
|
226
|
+
// For multiple descriptor properties on the same property, use object syntax
|
|
227
|
+
@descriptor({ writable: false, enumerable: false, configurable: false })('secret')
|
|
219
228
|
class MyClass { }
|
|
229
|
+
|
|
230
|
+
// Note: Stacking multiple flavored decorators on the same property is not recommended
|
|
231
|
+
// as each decorator creates a new subclass and only the last applied takes effect
|
|
220
232
|
```
|
|
221
233
|
|
|
222
234
|
## Usage Examples
|
|
223
235
|
|
|
236
|
+
### Using Flavors
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
import { descriptor } from 'mutts/std-decorators'
|
|
240
|
+
|
|
241
|
+
// Use flavors directly for cleaner syntax
|
|
242
|
+
@descriptor.readonly('id', 'createdAt')
|
|
243
|
+
@descriptor.hidden('_private', '_cache')
|
|
244
|
+
@descriptor.frozen('critical')
|
|
245
|
+
class User {
|
|
246
|
+
id: string = 'user-123'
|
|
247
|
+
name: string = 'John'
|
|
248
|
+
_private: string = 'secret'
|
|
249
|
+
_cache: Map<string, any> = new Map()
|
|
250
|
+
createdAt: Date = new Date()
|
|
251
|
+
critical: string = 'locked'
|
|
252
|
+
|
|
253
|
+
constructor(name: string) {
|
|
254
|
+
this.name = name
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const user = new User('Alice')
|
|
259
|
+
console.log(Object.keys(user)) // ['name', 'createdAt'] - only enumerable properties
|
|
260
|
+
// user.id = 'new-id' // TypeError: Cannot assign to read only property 'id'
|
|
261
|
+
```
|
|
262
|
+
|
|
224
263
|
### Creating Reusable Decorators
|
|
225
264
|
|
|
226
265
|
```typescript
|
|
227
|
-
import {
|
|
266
|
+
import { descriptor } from 'mutts/std-decorators'
|
|
228
267
|
|
|
229
268
|
// Create reusable descriptor configurations
|
|
230
|
-
const readonly =
|
|
231
|
-
const hidden =
|
|
232
|
-
const locked =
|
|
269
|
+
const readonly = descriptor.readonly
|
|
270
|
+
const hidden = descriptor.hidden
|
|
271
|
+
const locked = descriptor.frozen
|
|
233
272
|
|
|
234
273
|
// Use them on classes
|
|
235
274
|
@readonly('id', 'createdAt')
|
|
@@ -255,11 +294,10 @@ console.log(Object.keys(user)) // ['id', 'name', 'createdAt', 'critical'] - only
|
|
|
255
294
|
### Making Properties Non-Enumerable
|
|
256
295
|
|
|
257
296
|
```typescript
|
|
258
|
-
import {
|
|
297
|
+
import { descriptor } from 'mutts/std-decorators'
|
|
259
298
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
@hidden('_internal', '_cache', 'debug')
|
|
299
|
+
// Use the hidden flavor directly
|
|
300
|
+
@descriptor.hidden('_internal', '_cache', 'debug')
|
|
263
301
|
class CacheManager {
|
|
264
302
|
public data: any[] = []
|
|
265
303
|
_internal: Map<string, any> = new Map()
|
|
@@ -280,13 +318,10 @@ console.log(Object.getOwnPropertyNames(cache)) // ['data', '_internal', '_cache'
|
|
|
280
318
|
### Read-Only Properties
|
|
281
319
|
|
|
282
320
|
```typescript
|
|
283
|
-
import {
|
|
284
|
-
|
|
285
|
-
const readonly = describe({ writable: false })
|
|
286
|
-
const readonlyLocked = describe({ writable: false, configurable: false })
|
|
321
|
+
import { descriptor } from 'mutts/std-decorators'
|
|
287
322
|
|
|
288
|
-
|
|
289
|
-
@
|
|
323
|
+
// Use flavors for read-only properties
|
|
324
|
+
@descriptor.readonly('createdAt', 'version')
|
|
290
325
|
class Document {
|
|
291
326
|
id: string
|
|
292
327
|
title: string
|
|
@@ -306,25 +341,41 @@ class Document {
|
|
|
306
341
|
}
|
|
307
342
|
|
|
308
343
|
const doc = new Document('doc-1', 'My Document')
|
|
309
|
-
// doc.id = 'new-id' // TypeError: Cannot assign to read only property 'id'
|
|
310
344
|
// doc.createdAt = new Date() // TypeError: Cannot assign to read only property 'createdAt'
|
|
345
|
+
// doc.version = 2 // TypeError: Cannot assign to read only property 'version'
|
|
311
346
|
doc.updateTitle('Updated Title') // This works
|
|
312
347
|
```
|
|
313
348
|
|
|
314
|
-
###
|
|
349
|
+
### Combining Multiple Flavors
|
|
315
350
|
|
|
316
351
|
```typescript
|
|
317
|
-
import {
|
|
352
|
+
import { descriptor } from 'mutts/std-decorators'
|
|
318
353
|
|
|
319
|
-
|
|
320
|
-
|
|
354
|
+
// Stack multiple flavored decorators
|
|
355
|
+
@descriptor.readonly('secret')
|
|
356
|
+
@descriptor.hidden('secret')
|
|
357
|
+
@descriptor.frozen('secret')
|
|
358
|
+
class SecureData {
|
|
359
|
+
public info: string = 'public'
|
|
360
|
+
secret: string = 'top secret'
|
|
361
|
+
}
|
|
321
362
|
|
|
322
|
-
|
|
323
|
-
|
|
363
|
+
const data = new SecureData()
|
|
364
|
+
// data.secret = 'leaked' // TypeError: Cannot assign to read only property 'secret'
|
|
365
|
+
console.log(Object.keys(data)) // ['info'] - secret is not enumerable
|
|
366
|
+
// Object.defineProperty(data, 'secret', { value: 'new' }) // TypeError: Cannot redefine property
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Configuration Control
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
import { descriptor } from 'mutts/std-decorators'
|
|
373
|
+
|
|
374
|
+
// Use flavors for configuration control
|
|
375
|
+
@descriptor.frozen('_sealed')
|
|
324
376
|
class SecureObject {
|
|
325
377
|
public data: any
|
|
326
378
|
_sealed: string = 'cannot be reconfigured'
|
|
327
|
-
_frozen: string = 'cannot be changed or reconfigured'
|
|
328
379
|
|
|
329
380
|
constructor(data: any) {
|
|
330
381
|
this.data = data
|
|
@@ -333,41 +384,41 @@ class SecureObject {
|
|
|
333
384
|
|
|
334
385
|
const obj = new SecureObject({ key: 'value' })
|
|
335
386
|
// Object.defineProperty(obj, '_sealed', { value: 'new' }) // TypeError: Cannot redefine property
|
|
336
|
-
// Object.defineProperty(obj, '_frozen', { value: 'new' }) // TypeError: Cannot redefine property
|
|
337
387
|
```
|
|
338
388
|
|
|
339
389
|
## Implementation Details
|
|
340
390
|
|
|
341
391
|
### Functional Approach
|
|
342
392
|
|
|
343
|
-
The `
|
|
393
|
+
The `descriptor` function uses a flavored approach that provides direct access to common configurations:
|
|
344
394
|
|
|
345
395
|
```typescript
|
|
346
|
-
//
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
// Apply to multiple classes with different properties
|
|
351
|
-
@readonly('id', 'createdAt')
|
|
396
|
+
// Use flavors directly for cleaner syntax
|
|
397
|
+
@descriptor.readonly('id', 'createdAt')
|
|
398
|
+
@descriptor.hidden('internal')
|
|
352
399
|
class User { }
|
|
353
400
|
|
|
401
|
+
// Or create reusable aliases
|
|
402
|
+
const readonly = descriptor.readonly
|
|
403
|
+
const hidden = descriptor.hidden
|
|
404
|
+
|
|
354
405
|
@readonly('version', 'buildDate')
|
|
355
406
|
class Package { }
|
|
356
407
|
```
|
|
357
408
|
|
|
358
409
|
### Common Decorator Patterns
|
|
359
410
|
|
|
360
|
-
The
|
|
411
|
+
The flavored approach makes it easy to create common decorator patterns:
|
|
361
412
|
|
|
362
413
|
```typescript
|
|
363
|
-
import {
|
|
414
|
+
import { descriptor } from './std-decorators'
|
|
364
415
|
|
|
365
|
-
// Common reusable decorators
|
|
366
|
-
export const readonly =
|
|
367
|
-
export const hidden =
|
|
368
|
-
export const locked =
|
|
369
|
-
export const frozen =
|
|
370
|
-
export const private =
|
|
416
|
+
// Common reusable decorators using flavors
|
|
417
|
+
export const readonly = descriptor.readonly
|
|
418
|
+
export const hidden = descriptor.hidden
|
|
419
|
+
export const locked = descriptor.frozen
|
|
420
|
+
export const frozen = (() => descriptor.readonly.frozen)()
|
|
421
|
+
export const private = (() => descriptor.hidden.frozen)()
|
|
371
422
|
|
|
372
423
|
// Usage examples
|
|
373
424
|
@readonly('id', 'createdAt')
|
|
@@ -389,7 +440,7 @@ The decorator merges the provided descriptor configuration with the existing pro
|
|
|
389
440
|
|
|
390
441
|
```typescript
|
|
391
442
|
// Original property might have { enumerable: true, writable: true, configurable: true }
|
|
392
|
-
// After @readonly('prop')
|
|
443
|
+
// After @descriptor.readonly('prop')
|
|
393
444
|
// Final descriptor: { enumerable: true, writable: false, configurable: true }
|
|
394
445
|
```
|
|
395
446
|
|
|
@@ -399,9 +450,9 @@ The property descriptor configuration is applied in the constructor after callin
|
|
|
399
450
|
|
|
400
451
|
## Best Practices
|
|
401
452
|
|
|
402
|
-
1. **
|
|
403
|
-
2. **
|
|
404
|
-
3. **Combine multiple decorators**: Stack multiple
|
|
453
|
+
1. **Use flavors directly**: Prefer `descriptor.readonly` over creating aliases for simple cases
|
|
454
|
+
2. **Create reusable aliases**: For complex or frequently used combinations, create aliases
|
|
455
|
+
3. **Combine multiple decorators**: Stack multiple flavored decorators for complex configurations
|
|
405
456
|
4. **Use for encapsulation**: Hide internal properties from enumeration
|
|
406
457
|
5. **Control immutability**: Make critical properties read-only
|
|
407
458
|
6. **Prevent reconfiguration**: Lock important properties from being modified
|
package/docs/utils.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Utilities
|
|
2
|
+
|
|
3
|
+
Mutts provides a collection of lightweight, high-performance utility functions. These are used extensively within the reactive engine but are also exported for general application logic.
|
|
4
|
+
|
|
5
|
+
## Collection Utilities
|
|
6
|
+
|
|
7
|
+
### `zip(...arrays)`
|
|
8
|
+
|
|
9
|
+
A generator that yields tuples containing elements from each input array. It continues until the **longest** array is exhausted (returning `undefined` for shorter arrays).
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { zip } from 'mutts';
|
|
13
|
+
|
|
14
|
+
const names = ['Alice', 'Bob'];
|
|
15
|
+
const scores = [100, 95, 80];
|
|
16
|
+
|
|
17
|
+
for (const [name, score] of zip(names, scores)) {
|
|
18
|
+
console.log(`${name}: ${score}`);
|
|
19
|
+
}
|
|
20
|
+
// Alice: 100
|
|
21
|
+
// Bob: 95
|
|
22
|
+
// undefined: 80
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
> [!NOTE]
|
|
26
|
+
> `zip` is implemented as a generator for memory efficiency. If you need a plain array, spread the result: `[...zip(a, b)]`.
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
### `deepCompare(a, b)`
|
|
30
|
+
|
|
31
|
+
A robust deep comparison utility that handles circular references and various built-in types.
|
|
32
|
+
|
|
33
|
+
- **Supported Types**: Objects, Arrays, `Set`, `Map`, `Date`, `RegExp`.
|
|
34
|
+
- **Circular References**: Safely handled via internal tracking.
|
|
35
|
+
- **Prototypes**: Objects must have matching prototypes to be considered equal.
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { deepCompare } from 'mutts';
|
|
39
|
+
|
|
40
|
+
const obj1 = { date: new Date(0), map: new Map([['a', 1]]) };
|
|
41
|
+
const obj2 = { date: new Date(0), map: new Map([['a', 1]]) };
|
|
42
|
+
|
|
43
|
+
deepCompare(obj1, obj2); // true
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Type Reflection
|
|
47
|
+
|
|
48
|
+
### `isConstructor(fn)` / `isObject(value)`
|
|
49
|
+
|
|
50
|
+
Utilities for robust type checking without the pitfalls of `typeof`.
|
|
51
|
+
|
|
52
|
+
- `isConstructor`: Returns `true` if the function is a `class` or a native constructor (like `Array`).
|
|
53
|
+
- `isObject`: Returns `true` for plain objects. Returns `false` for `null`, `Array`, `Date`, `Map`, etc.
|
|
54
|
+
|
|
55
|
+
## Debugging & Metadata
|
|
56
|
+
|
|
57
|
+
### `tag(name, obj)`
|
|
58
|
+
|
|
59
|
+
Applies a debugging "tag" to an object. It sets `Symbol.toStringTag` and overrides `toString()` so the object appears clearly in logs and DevTools.
|
|
60
|
+
|
|
61
|
+
### `named(name, fn)`
|
|
62
|
+
|
|
63
|
+
Renames a function for better stack traces. If the function already has a name, it appends the new name using `::` as a separator (e.g., `original::new`).
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
const myFn = named('Enhanced', () => {});
|
|
67
|
+
console.log(myFn.name); // "Enhanced"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
package/docs/zone.md
CHANGED
|
@@ -21,20 +21,17 @@ console.log(myZone.active); // undefined
|
|
|
21
21
|
|
|
22
22
|
## Async Propagation
|
|
23
23
|
|
|
24
|
-
By default, zones are lost when an async operation yields control (e.g., after `await`). To fix this, `
|
|
24
|
+
By default, zones are lost when an async operation yields control (e.g., after `await`). To fix this, register zones in `asyncZone` — the global `ZoneAggregator` — which automatically preserves context across async boundaries via `asyncHooks`.
|
|
25
25
|
|
|
26
26
|
```typescript
|
|
27
|
-
import {
|
|
27
|
+
import { asyncZone, Zone } from 'mutts/zone';
|
|
28
28
|
|
|
29
29
|
const requestId = new Zone<string>();
|
|
30
30
|
|
|
31
31
|
// 1. Tell the global aggregator to track this zone
|
|
32
32
|
asyncZone.add(requestId);
|
|
33
33
|
|
|
34
|
-
// 2.
|
|
35
|
-
configureAsyncZone();
|
|
36
|
-
|
|
37
|
-
// 3. Usage
|
|
34
|
+
// 2. Usage
|
|
38
35
|
requestId.with("req-123", async () => {
|
|
39
36
|
await somePromise();
|
|
40
37
|
// Context is automatically preserved across await!
|
|
@@ -43,7 +40,6 @@ requestId.with("req-123", async () => {
|
|
|
43
40
|
```
|
|
44
41
|
|
|
45
42
|
> [!WARNING]
|
|
46
|
-
> TODO
|
|
47
43
|
> **Browser Limitations**: In browser environments (where `AsyncLocalStorage` is unavailable), `mutts` relies on monkey-patching global async primitives (Promise, setTimeout, etc.) to propagate zones. This is generally less robust than Node.js's `async_hooks` and may fail to track context across:
|
|
48
44
|
> * Native `async/await` boundaries in some modern browsers if not transpiled.
|
|
49
45
|
> * Concurrent modifications to global prototypes by other libraries.
|