mutts 1.0.2 → 1.0.4

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.
Files changed (104) hide show
  1. package/README.md +14 -6
  2. package/dist/chunks/{_tslib-C-cuVLvZ.js → _tslib-BgjropY9.js} +9 -1
  3. package/dist/chunks/_tslib-BgjropY9.js.map +1 -0
  4. package/dist/chunks/{_tslib-CMEnd0VE.esm.js → _tslib-Mzh1rNsX.esm.js} +9 -2
  5. package/dist/chunks/_tslib-Mzh1rNsX.esm.js.map +1 -0
  6. package/dist/chunks/{decorator-D4DU97Zg.js → decorator-DLvrD0UF.js} +42 -19
  7. package/dist/chunks/decorator-DLvrD0UF.js.map +1 -0
  8. package/dist/chunks/{decorator-GnHw1Az7.esm.js → decorator-DqiszP7i.esm.js} +42 -19
  9. package/dist/chunks/decorator-DqiszP7i.esm.js.map +1 -0
  10. package/dist/chunks/index-79Kk8D6e.esm.js +4857 -0
  11. package/dist/chunks/index-79Kk8D6e.esm.js.map +1 -0
  12. package/dist/chunks/index-GRBSx0mB.js +4908 -0
  13. package/dist/chunks/index-GRBSx0mB.js.map +1 -0
  14. package/dist/decorator.esm.js +1 -1
  15. package/dist/decorator.js +1 -1
  16. package/dist/destroyable.d.ts +1 -1
  17. package/dist/destroyable.esm.js +1 -1
  18. package/dist/destroyable.esm.js.map +1 -1
  19. package/dist/destroyable.js +1 -1
  20. package/dist/destroyable.js.map +1 -1
  21. package/dist/devtools/devtools.html +9 -0
  22. package/dist/devtools/devtools.js +5 -0
  23. package/dist/devtools/devtools.js.map +1 -0
  24. package/dist/devtools/manifest.json +8 -0
  25. package/dist/devtools/panel.css +72 -0
  26. package/dist/devtools/panel.html +31 -0
  27. package/dist/devtools/panel.js +13048 -0
  28. package/dist/devtools/panel.js.map +1 -0
  29. package/dist/eventful.esm.js +1 -1
  30. package/dist/eventful.js +1 -1
  31. package/dist/index.d.ts +18 -63
  32. package/dist/index.esm.js +4 -4
  33. package/dist/index.js +37 -11
  34. package/dist/index.js.map +1 -1
  35. package/dist/indexable.d.ts +187 -1
  36. package/dist/indexable.esm.js +197 -3
  37. package/dist/indexable.esm.js.map +1 -1
  38. package/dist/indexable.js +198 -2
  39. package/dist/indexable.js.map +1 -1
  40. package/dist/mutts.umd.js +1 -1
  41. package/dist/mutts.umd.js.map +1 -1
  42. package/dist/mutts.umd.min.js +1 -1
  43. package/dist/mutts.umd.min.js.map +1 -1
  44. package/dist/promiseChain.esm.js.map +1 -1
  45. package/dist/promiseChain.js.map +1 -1
  46. package/dist/reactive.d.ts +602 -97
  47. package/dist/reactive.esm.js +3 -3
  48. package/dist/reactive.js +32 -10
  49. package/dist/reactive.js.map +1 -1
  50. package/dist/std-decorators.esm.js +1 -1
  51. package/dist/std-decorators.js +1 -1
  52. package/docs/ai/api-reference.md +133 -0
  53. package/docs/ai/manual.md +105 -0
  54. package/docs/iterableWeak.md +646 -0
  55. package/docs/reactive/advanced.md +1280 -0
  56. package/docs/reactive/collections.md +767 -0
  57. package/docs/reactive/core.md +973 -0
  58. package/docs/reactive.md +21 -9545
  59. package/package.json +18 -5
  60. package/src/decorator.ts +266 -0
  61. package/src/destroyable.ts +199 -0
  62. package/src/eventful.ts +77 -0
  63. package/src/index.d.ts +9 -0
  64. package/src/index.ts +9 -0
  65. package/src/indexable.ts +484 -0
  66. package/src/introspection.ts +59 -0
  67. package/src/iterableWeak.ts +233 -0
  68. package/src/mixins.ts +123 -0
  69. package/src/promiseChain.ts +110 -0
  70. package/src/reactive/array.ts +414 -0
  71. package/src/reactive/change.ts +134 -0
  72. package/src/reactive/debug.ts +517 -0
  73. package/src/reactive/deep-touch.ts +268 -0
  74. package/src/reactive/deep-watch-state.ts +82 -0
  75. package/src/reactive/deep-watch.ts +168 -0
  76. package/src/reactive/effect-context.ts +94 -0
  77. package/src/reactive/effects.ts +1345 -0
  78. package/src/reactive/index.ts +76 -0
  79. package/src/reactive/interface.ts +223 -0
  80. package/src/reactive/map.ts +171 -0
  81. package/src/reactive/mapped.ts +130 -0
  82. package/src/reactive/memoize.ts +107 -0
  83. package/src/reactive/non-reactive-state.ts +49 -0
  84. package/src/reactive/non-reactive.ts +43 -0
  85. package/src/reactive/project.project.md +93 -0
  86. package/src/reactive/project.ts +335 -0
  87. package/src/reactive/proxy-state.ts +27 -0
  88. package/src/reactive/proxy.ts +289 -0
  89. package/src/reactive/record.ts +196 -0
  90. package/src/reactive/register.ts +421 -0
  91. package/src/reactive/set.ts +144 -0
  92. package/src/reactive/tracking.ts +101 -0
  93. package/src/reactive/types.ts +358 -0
  94. package/src/reactive/zone.ts +208 -0
  95. package/src/std-decorators.ts +217 -0
  96. package/src/utils.ts +117 -0
  97. package/dist/chunks/_tslib-C-cuVLvZ.js.map +0 -1
  98. package/dist/chunks/_tslib-CMEnd0VE.esm.js.map +0 -1
  99. package/dist/chunks/decorator-D4DU97Zg.js.map +0 -1
  100. package/dist/chunks/decorator-GnHw1Az7.esm.js.map +0 -1
  101. package/dist/chunks/index-DBScoeCX.esm.js +0 -1960
  102. package/dist/chunks/index-DBScoeCX.esm.js.map +0 -1
  103. package/dist/chunks/index-DOTmXL89.js +0 -1983
  104. package/dist/chunks/index-DOTmXL89.js.map +0 -1
@@ -0,0 +1,767 @@
1
+ ## Collections
2
+
3
+ ### `ReactiveMap`
4
+
5
+ A reactive wrapper around JavaScript's `Map` class.
6
+
7
+ ```typescript
8
+ const map = reactive(new Map([['key1', 'value1']]))
9
+
10
+ effect(() => {
11
+ console.log('Map size:', map.size)
12
+ console.log('Has key1:', map.has('key1'))
13
+ })
14
+
15
+ map.set('key2', 'value2') // Triggers effect
16
+ map.delete('key1') // Triggers effect
17
+ ```
18
+
19
+ **Features:**
20
+ - Tracks `size` changes
21
+ - Tracks individual key operations
22
+ - Tracks collection-wide operations via `allProps`
23
+
24
+ ### `ReactiveWeakMap`
25
+
26
+ A reactive wrapper around JavaScript's `WeakMap` class.
27
+
28
+ ```typescript
29
+ const weakMap = reactive(new WeakMap())
30
+ const key = { id: 1 }
31
+
32
+ effect(() => {
33
+ console.log('Has key:', weakMap.has(key))
34
+ })
35
+
36
+ weakMap.set(key, 'value') // Triggers effect
37
+ weakMap.delete(key) // Triggers effect
38
+ ```
39
+
40
+ **Features:**
41
+ - Only tracks individual key operations
42
+ - No `size` tracking (WeakMap limitation)
43
+ - No collection-wide operations
44
+
45
+ ### `ReactiveSet`
46
+
47
+ A reactive wrapper around JavaScript's `Set` class.
48
+
49
+ ```typescript
50
+ const set = reactive(new Set([1, 2, 3]))
51
+
52
+ effect(() => {
53
+ console.log('Set size:', set.size)
54
+ console.log('Has 1:', set.has(1))
55
+ })
56
+
57
+ set.add(4) // Triggers effect
58
+ set.delete(1) // Triggers effect
59
+ set.clear() // Triggers effect
60
+ ```
61
+
62
+ **Features:**
63
+ - Tracks `size` changes
64
+ - Tracks individual value operations
65
+ - Tracks collection-wide operations
66
+
67
+ ### `ReactiveWeakSet`
68
+
69
+ A reactive wrapper around JavaScript's `WeakSet` class.
70
+
71
+ ```typescript
72
+ const weakSet = reactive(new WeakSet())
73
+ const obj = { id: 1 }
74
+
75
+ effect(() => {
76
+ console.log('Has obj:', weakSet.has(obj))
77
+ })
78
+
79
+ weakSet.add(obj) // Triggers effect
80
+ weakSet.delete(obj) // Triggers effect
81
+ ```
82
+
83
+ ### Collection-Specific Reactivity
84
+
85
+ Collections provide different levels of reactivity:
86
+
87
+ ```typescript
88
+ const map = reactive(new Map())
89
+
90
+ // Size tracking
91
+ effect(() => {
92
+ console.log('Map size:', map.size)
93
+ })
94
+
95
+ // Individual key tracking
96
+ effect(() => {
97
+ console.log('Value for key1:', map.get('key1'))
98
+ })
99
+
100
+ // Collection-wide tracking
101
+ effect(() => {
102
+ for (const [key, value] of map) {
103
+ // This effect depends on allProps
104
+ }
105
+ })
106
+
107
+ // Operations trigger different effects
108
+ map.set('key1', 'value1') // Triggers size and key1 effects
109
+ map.set('key2', 'value2') // Triggers size and allProps effects
110
+ map.delete('key1') // Triggers size, key1, and allProps effects
111
+ ```
112
+
113
+ ### `ReactiveArray`
114
+
115
+ A reactive wrapper around JavaScript's `Array` class with full array method support.
116
+
117
+ ```typescript
118
+ const array = reactive([1, 2, 3])
119
+
120
+ effect(() => {
121
+ console.log('Array length:', array.length)
122
+ console.log('First element:', array[0])
123
+ })
124
+
125
+ array.push(4) // Triggers effect
126
+ array[0] = 10 // Triggers effect
127
+ ```
128
+
129
+ **Features:**
130
+ - Tracks `length` changes
131
+ - Tracks individual index operations
132
+ - Tracks collection-wide operations via `allProps`
133
+ - Supports all array methods with proper reactivity
134
+
135
+ ### Array Methods
136
+
137
+ All standard array methods are supported with reactivity:
138
+
139
+ ```typescript
140
+ const array = reactive([1, 2, 3])
141
+
142
+ // Mutator methods
143
+ array.push(4) // Triggers length and allProps effects
144
+ array.pop() // Triggers length and allProps effects
145
+ array.shift() // Triggers length and allProps effects
146
+ array.unshift(0) // Triggers length and allProps effects
147
+ array.splice(1, 1, 10) // Triggers length and allProps effects
148
+ array.reverse() // Triggers allProps effects
149
+ array.sort() // Triggers allProps effects
150
+ array.fill(0) // Triggers allProps effects
151
+ array.copyWithin(0, 2) // Triggers allProps effects
152
+
153
+ // Accessor methods (immutable)
154
+ const reversed = array.toReversed()
155
+ const sorted = array.toSorted()
156
+ const spliced = array.toSpliced(1, 1)
157
+ const withNew = array.with(0, 100)
158
+ ```
159
+
160
+ ### Index Access
161
+
162
+ ReactiveArray supports both positive and negative index access:
163
+
164
+ ```typescript
165
+ const array = reactive([1, 2, 3, 4, 5])
166
+
167
+ effect(() => {
168
+ console.log('First element:', array[0])
169
+ console.log('Last element:', array.at(-1))
170
+ })
171
+
172
+ array[0] = 10 // Triggers effect
173
+ array[4] = 50 // Triggers effect
174
+ ```
175
+
176
+ ### Length Reactivity
177
+
178
+ The `length` property is fully reactive:
179
+
180
+ ```typescript
181
+ const array = reactive([1, 2, 3])
182
+
183
+ effect(() => {
184
+ console.log('Array length:', array.length)
185
+ })
186
+
187
+ array.push(4) // Triggers effect
188
+ array.length = 2 // Triggers effect
189
+ array[5] = 10 // Triggers effect (expands array)
190
+ ```
191
+
192
+ ### Array Evolution Tracking
193
+
194
+ Array operations generate specific evolution events:
195
+
196
+ ```typescript
197
+ const array = reactive([1, 2, 3])
198
+ let state = getState(array)
199
+
200
+ effect(() => {
201
+ while ('evolution' in state) {
202
+ console.log('Array change:', state.evolution)
203
+ state = state.next
204
+ }
205
+ })
206
+
207
+ array.push(4) // { type: 'bunch', method: 'push' }
208
+ array[0] = 10 // { type: 'set', prop: 0 }
209
+ array[5] = 20 // { type: 'add', prop: 5 }
210
+ ```
211
+
212
+ ### Array-Specific Reactivity Patterns
213
+
214
+ ```typescript
215
+ const array = reactive([1, 2, 3])
216
+
217
+ // Track specific indices
218
+ effect(() => {
219
+ console.log('First two elements:', array[0], array[1])
220
+ })
221
+
222
+ // Track length changes
223
+ effect(() => {
224
+ console.log('Array size changed to:', array.length)
225
+ })
226
+
227
+ // Track all elements (via iteration)
228
+ effect(() => {
229
+ for (const item of array) {
230
+ // This effect depends on allProps
231
+ }
232
+ })
233
+
234
+ // Track specific array methods
235
+ effect(() => {
236
+ const lastElement = array.at(-1)
237
+ console.log('Last element:', lastElement)
238
+ })
239
+ ```
240
+
241
+ ### Performance Considerations
242
+
243
+ ReactiveArray is optimized for common array operations:
244
+
245
+ ```typescript
246
+ // Efficient: Direct index access
247
+ effect(() => {
248
+ console.log(array[0]) // Only tracks index 0
249
+ })
250
+
251
+ // Efficient: Length tracking
252
+ effect(() => {
253
+ console.log(array.length) // Only tracks length
254
+ })
255
+
256
+ // Less efficient: Iteration tracks all elements
257
+ effect(() => {
258
+ array.forEach(item => console.log(item)) // Tracks allProps
259
+ })
260
+ ```
261
+
262
+ ### `Register`
263
+
264
+ `Register` is an ordered, array-like collection that keeps a stable mapping between keys and values. It is useful when you need array semantics (indexable access, ordering, iteration) but also require identity preservation by key—ideal for UI lists keyed by IDs or when you want to memoise entries across reorders.
265
+
266
+ ```typescript
267
+ import { Register } from 'mutts/reactive'
268
+
269
+ // Create a register where the key comes from the `id` field
270
+ const list = new Register(({id}: { id: number }) => id, [
271
+ { id: 1, label: 'Alpha' },
272
+ { id: 2, label: 'Bravo' },
273
+ ])
274
+
275
+ effect(() => {
276
+ console.log('Length:', list.length)
277
+ console.log('First label:', list[0]?.label)
278
+ })
279
+
280
+ // Push uses the key function to keep identities stable
281
+ list.push({ id: 3, label: 'Charlie' })
282
+
283
+ // Replacing with the same key updates watchers without creating a new identity
284
+ list[0] = { id: 1, label: 'Alpha (updated)' }
285
+
286
+ // Access by key
287
+ const second = list.get(2) // { id: 2, label: 'Bravo' }
288
+
289
+ // Duplicate keys share value identity
290
+ list.push({ id: 2, label: 'Bravo (new data)' })
291
+ console.log(list[1] === list[2]) // true
292
+ ```
293
+
294
+ **Highlights:**
295
+
296
+ - Fully indexable (`list[0]`, `list.at(-1)`, `list.length`, iteration, etc.) thanks to the shared `Indexable` infrastructure.
297
+ - Complete array surface forwarding (`map`, `filter`, `reduce`, `concat`, `reverse`, `sort`, `fill`, `copyWithin`, and more) with reactivity preserved.
298
+ - Stable key/value map under the hood allows quick lookups via `get()`, `hasKey()`, and `indexOfKey()`.
299
+ - When the same key appears multiple times, all slots reference the same underlying value instance, making deduplication and memoisation straightforward.
300
+ - Reordering operations emit index-level touches so list reactivity remains predictable in rendered UIs.
301
+
302
+ ### Register-specific API (beyond Array)
303
+
304
+ The `Register` exposes additional methods and behaviors that standard arrays do not have:
305
+
306
+ - `get(key)` / `set(key, value)`
307
+ - `get(key: K): T | undefined` returns the latest value for a key.
308
+ - `set(key: K, value: T): void` updates the value for an existing key (no-op if key absent).
309
+ - Example:
310
+ ```typescript
311
+ list.set(2, { id: 2, label: 'Bravo (updated)' })
312
+ const v = list.get(2)
313
+ ```
314
+
315
+ - `hasKey(key)` / `indexOfKey(key)`
316
+ - `hasKey(key: K): boolean` whether the key is present in any slot.
317
+ - `indexOfKey(key: K): number` first index at which the key appears, or `-1`.
318
+
319
+ - `remove(key)` / `removeAt(index)`
320
+ - `remove(key: K): void` removes all occurrences of `key` from the register.
321
+ - `removeAt(index: number): T | undefined` removes a single slot by index and returns its value.
322
+
323
+ - `keep(predicate)`
324
+ - `keep(predicate: (value: T) => boolean): void` keeps only items for which the predicate returns `true`; items for which it returns `false` are removed. The predicate is evaluated once per distinct key; duplicate keys follow the same decision.
325
+
326
+ - `update(...values)`
327
+ - `update(...values: T[]): void` updates existing entries by their key; ignores values whose key is not yet present.
328
+
329
+ - `upsert(insert, ...values)`
330
+ - `upsert(insert: (value: T) => void, ...values: T[]): void` updates by key when present, otherwise calls `insert(value)` so you can decide how to insert (e.g. `push`, `unshift`, or `splice`).
331
+ - Example:
332
+ ```typescript
333
+ list.upsert(v => list.push(v), { id: 4, label: 'Delta' }, { id: 2, label: 'Bravo (again)' })
334
+ ```
335
+
336
+ - `entries()`
337
+ - Iterates `[number, value]` pairs in index order: `IterableIterator<[number, T | undefined]>`.
338
+
339
+ - `keys` / `values`
340
+ - `keys: ArrayIterator<number>` provides the index iterator (mirrors `Array#keys()`).
341
+ - `values: IterableIterator<T>` provides an iterator of values (same as default iteration).
342
+
343
+ - `clear()`
344
+ - Removes all entries and disposes internal key-tracking effects.
345
+
346
+ - `toArray()` / `toString()`
347
+ - `toArray(): T[]` materializes the current values into a plain array.
348
+ - `toString(): string` returns a concise description like `[Register length=3]`.
349
+
350
+ Notes:
351
+ - Direct length modification via `list.length = n` is not supported; use `splice` instead.
352
+ - Assigning to an index (`list[i] = value`) uses the key function to bind that slot to `value`’s key.
353
+
354
+ ## Class Reactivity
355
+ ## Array Mapping
356
+
357
+ ### `mapped()`
358
+
359
+ Creates a reactive array by mapping over an input array. The mapper receives the current item value, its index, and the previous mapped value for that index.
360
+
361
+ ```typescript
362
+ import { mapped, reactive } from 'mutts/reactive'
363
+
364
+ const input = reactive([1, 2, 3])
365
+ const doubles = mapped(input, (value, index, oldValue) => value * 2)
366
+
367
+ console.log(doubles) // [2, 4, 6]
368
+
369
+ // When input changes, the mapped output updates in place
370
+ input.push(4)
371
+ console.log(doubles) // [2, 4, 6, 8]
372
+ ```
373
+
374
+ **Mapper signature:**
375
+
376
+ ```typescript
377
+ (value: T, index: number, oldValue?: U) => U
378
+ ```
379
+
380
+ - **value**: current element from the input array
381
+ - **index**: current element index
382
+ - **oldValue**: previously computed value at the same index (useful for incremental updates)
383
+
384
+ **Key features:**
385
+
386
+ - **Live reactivity**: Output array updates when the input array changes (push/pop/splice/assignments).
387
+ - **Granular recompute**: Only indices that change are recomputed; `oldValue` enables incremental updates.
388
+ - **Simple contract**: Mapper works directly with `(value, index, oldValue)` and can freely return reactive objects.
389
+
390
+ **Performance characteristics:**
391
+
392
+ ```typescript
393
+ const users = reactive([
394
+ { name: 'John', age: 30 },
395
+ { name: 'Jane', age: 25 }
396
+ ])
397
+
398
+ let computeCount = 0
399
+ const processedUsers = mapped(users, (user) => {
400
+ computeCount++
401
+ return `${user.name} (${user.age})`
402
+ })
403
+
404
+ console.log(computeCount) // 2 (initial computation)
405
+
406
+ // Modify one user - only that index recomputes
407
+ users[0].age = 31
408
+ console.log(processedUsers[0]) // "John (31)"
409
+ console.log(computeCount) // 3
410
+ ```
411
+
412
+ **Advanced usage:**
413
+
414
+ ```typescript
415
+ const orders = reactive([
416
+ { items: [{ price: 10 }, { price: 20 }] },
417
+ { items: [{ price: 15 }] }
418
+ ])
419
+
420
+ const orderTotals = mapped(orders, (order) => (
421
+ order.items.reduce((sum, item) => sum + item.price, 0)
422
+ ))
423
+ ```
424
+
425
+ ### Identity-preserving mapped arrays
426
+
427
+ Combine `mapped()` with [`memoize()`](#memoize) when you need to reuse mapped results for the same input identity. The memoized mapper runs at most once per input object, even when the source array is reordered, and it can host additional reactive state that should survive reordering.
428
+
429
+ ```typescript
430
+ import { effect, mapped, memoize, reactive } from 'mutts/reactive'
431
+
432
+ const inputs = reactive([{ name: 'John' }, { name: 'Jane' }])
433
+
434
+ const memoizedCard = memoize((user: { name: string }) => {
435
+ const view: { name?: string; setName(next: string): void } = {
436
+ setName(next) {
437
+ user.name = next
438
+ },
439
+ }
440
+
441
+ effect(() => {
442
+ view.name = user.name.toUpperCase()
443
+ })
444
+
445
+ return view
446
+ })
447
+
448
+ const cards = mapped(inputs, (user) => memoizedCard(user))
449
+
450
+ cards[0].setName('Johnny')
451
+ console.log(cards[0].name) // 'JOHNNY'
452
+
453
+ // Reorder: cached output follows the original object
454
+ const first = inputs.shift()!
455
+ inputs.push(first)
456
+ console.log(cards[1].name) // still 'JOHNNY'
457
+ ```
458
+
459
+ Use this pattern when:
460
+
461
+ - You are mapping an array of reactive objects and want to keep derived objects stable across reorders.
462
+ - The mapper returns objects with internal state or nested effects that should survive reordering.
463
+ - You prefer to share memoized helpers across multiple mapped arrays.
464
+
465
+ ## Projection
466
+
467
+ ### `project()`
468
+
469
+ `project()` provides a unified API for transforming reactive collections (arrays, records, and maps) into new reactive collections. Each source entry gets its own reactive effect that recomputes only when that specific entry changes, enabling granular updates perfect for rendering pipelines.
470
+
471
+ **Note:** `project()` is the modern replacement for `mapped()`. It offers the same per-entry reactivity benefits but works across all collection types with a consistent API.
472
+
473
+ #### Basic Usage
474
+
475
+ ```typescript
476
+ import { cleanup, project, reactive } from 'mutts/reactive'
477
+
478
+ // Arrays
479
+ const users = reactive([{ name: 'John', age: 30 }, { name: 'Jane', age: 25 }])
480
+ const names = project.array(users, ({ get }) => get()?.name.toUpperCase() ?? '')
481
+
482
+ console.log(names) // ['JOHN', 'JANE']
483
+
484
+ users[0].name = 'Johnny'
485
+ console.log(names[0]) // 'JOHNNY' - only index 0 recomputed
486
+
487
+ // Records
488
+ const scores = reactive({ math: 90, science: 85 })
489
+ const grades = project.record(scores, ({ get }) => {
490
+ const score = get()
491
+ return score >= 90 ? 'A' : score >= 80 ? 'B' : 'C'
492
+ })
493
+
494
+ console.log(grades.math) // 'A'
495
+ scores.math = 88
496
+ console.log(grades.math) // 'B' - only math key recomputed
497
+
498
+ // Maps
499
+ const inventory = reactive(new Map([
500
+ ['apples', { count: 10 }],
501
+ ['oranges', { count: 5 }]
502
+ ]))
503
+ const totals = project.map(inventory, ({ get }) => get()?.count ?? 0)
504
+
505
+ console.log(totals.get('apples')) // 10
506
+ inventory.get('apples')!.count = 15
507
+ console.log(totals.get('apples')) // 15 - only 'apples' key recomputed
508
+ ```
509
+
510
+ #### Automatic Type Selection
511
+
512
+ You can use `project()` directly and it will automatically select the appropriate helper based on the source type:
513
+
514
+ ```typescript
515
+ // Automatically uses project.array
516
+ const doubled = project([1, 2, 3], ({ get }) => get() * 2)
517
+
518
+ // Automatically uses project.record
519
+ const upper = project({ a: 'hello', b: 'world' }, ({ get }) => get()?.toUpperCase() ?? '')
520
+
521
+ // Automatically uses project.map
522
+ const counts = project(new Map([['x', 1], ['y', 2]]), ({ get }) => get() * 2)
523
+ ```
524
+
525
+ #### Access Object
526
+
527
+ The callback receives a `ProjectAccess` object with:
528
+
529
+ - **`get()`**: Function that returns the current source value for this key/index
530
+ - **`set(value)`**: Function to update the source value (if the source is mutable)
531
+ - **`key`**: The current key or index
532
+ - **`source`**: Reference to the original source collection
533
+ - **`old`**: Previously computed result for this entry (undefined on first run)
534
+ - **`value`**: Computed property that mirrors `get()` (for convenience)
535
+
536
+ ```typescript
537
+ const transformed = project.array(items, (access) => {
538
+ // Access the source value
539
+ const item = access.get()
540
+
541
+ // Access the key/index
542
+ console.log(`Processing index ${access.key}`)
543
+
544
+ // Leverage previous result
545
+ console.log(`Previous result: ${access.old}`)
546
+
547
+ // Transform and return
548
+ return item.value * 2
549
+ })
550
+ ```
551
+
552
+ #### Per-Entry Reactivity
553
+
554
+ Each entry in the source collection gets its own reactive effect. When only one entry changes, only that entry's projection recomputes:
555
+
556
+ ```typescript
557
+ const users = reactive([
558
+ { id: 1, name: 'John', score: 100 },
559
+ { id: 2, name: 'Jane', score: 200 },
560
+ { id: 3, name: 'Bob', score: 150 }
561
+ ])
562
+
563
+ let computeCount = 0
564
+ const summaries = project.array(users, ({ get }) => {
565
+ computeCount++
566
+ const user = get()
567
+ return `${user.name}: ${user.score}`
568
+ })
569
+
570
+ console.log(computeCount) // 3 (initial computation)
571
+
572
+ // Modify only the first user
573
+ users[0].score = 150
574
+ console.log(summaries[0]) // 'John: 150'
575
+ console.log(computeCount) // 4 (only index 0 recomputed)
576
+
577
+ // Add a new user
578
+ users.push({ id: 4, name: 'Alice', score: 175 })
579
+ console.log(computeCount) // 5 (only new index 3 computed)
580
+ ```
581
+
582
+ #### Key Addition and Removal
583
+
584
+ `project()` automatically handles keys being added or removed from the source:
585
+
586
+ ```typescript
587
+ const source = reactive({ a: 1, b: 2 })
588
+ const doubled = project.record(source, ({ get }) => get() * 2)
589
+
590
+ console.log(doubled.a) // 2
591
+ console.log(doubled.b) // 4
592
+
593
+ // Add new key
594
+ source.c = 3
595
+ console.log(doubled.c) // 6 (automatically computed)
596
+
597
+ // Remove key
598
+ delete source.a
599
+ console.log('a' in doubled) // false (automatically removed)
600
+ ```
601
+
602
+ #### Cleanup
603
+
604
+ The returned object includes a `cleanup` symbol that stops all reactive effects:
605
+
606
+ ```typescript
607
+ const result = project.array(items, ({ get }) => get() * 2)
608
+
609
+ // Later, when done
610
+ result[cleanup]() // Stops all effects and cleans up
611
+ ```
612
+
613
+ #### Use Cases
614
+
615
+ - **Rendering Lists**: Transform data models into view models for JSX/HTML rendering, with only changed items recomputing
616
+ - **Derived Collections**: Create computed views of source data that stay in sync
617
+ - **Data Transformation**: Convert between collection types while maintaining reactivity
618
+ - **Performance Optimization**: Avoid full recomputation when only a few entries change
619
+
620
+ #### Comparison with `mapped()`
621
+
622
+ `project()` is designed to eventually replace `mapped()`. Key differences:
623
+
624
+ - **Unified API**: Works with arrays, records, and maps (vs. `mapped()` only for arrays)
625
+ - **Access Pattern**: Uses an access object with `get()`/`set()` instead of direct value/index parameters
626
+ - **Automatic Target Creation**: Creates its own reactive target container (no need to provide a base target)
627
+ - **Consistent Behavior**: Same per-entry reactivity model across all collection types
628
+
629
+ For new code, prefer `project()` over `mapped()`. Existing `mapped()` code will continue to work, but consider migrating for better consistency and future features.
630
+
631
+ ## Record Organization
632
+
633
+ ### `organized()`
634
+
635
+ `organized()` is the record companion to [`mapped()`](#mapped). Instead of iterating over numeric indices, it reacts to property additions, updates, and deletions on any `Record<PropertyKey, T>` (plain objects, dictionaries, even reactive proxies) and lets you build **whatever target structure you need**—a new record, nested buckets, a `Map`, or a more elaborate object with metadata.
636
+
637
+ ```typescript
638
+ import { cleanup, organized, reactive } from 'mutts/reactive'
639
+
640
+ const source = reactive<Record<string, number>>({ apples: 1, oranges: 2 })
641
+
642
+ const doubled = organized(source, (access, target) => {
643
+ target[access.key] = access.get() * 2
644
+ return () => delete target[access.key] // optional cleanup per key
645
+ })
646
+
647
+ console.log(doubled.apples) // 2
648
+
649
+ source.oranges = 5
650
+ console.log(doubled.oranges) // 10
651
+
652
+ delete source.apples
653
+ doubled[cleanup]() // run all remaining key cleanups (here deletes oranges)
654
+ ```
655
+
656
+ #### Signature
657
+
658
+ ```typescript
659
+ function organized<
660
+ Source extends Record<PropertyKey, any>,
661
+ Target extends object = Record<PropertyKey, any>
662
+ >(
663
+ source: Source,
664
+ apply: (access: OrganizedAccess<Source, keyof Source>, target: Target) => ScopedCallback | void,
665
+ baseTarget?: Target
666
+ ): Target & { [cleanup]: ScopedCallback }
667
+ ```
668
+
669
+ - **source**: Any object with consistent value type. If it is not already reactive, `organized()` wraps it transparently.
670
+ - **apply**: Called once per own property. It receives the *stable* `target` object plus an accessor describing the property:
671
+ - `access.key` → the original property key
672
+ - `access.get()` / `access.set(value)` → always respect source getters/setters
673
+ - `access.value` → convenience getter/setter backed by the same logic
674
+ Return a cleanup to dispose per-key resources (event handlers, nested effects, bucket entries, …).
675
+ - **baseTarget** *(optional)*: Provide an initial object (e.g. `{ buckets: {}, cleanups: new Map() }`). It becomes reactive and is returned.
676
+ - **return value**: The same `target`, augmented with the `[cleanup]` symbol. Call `target[cleanup]()` to stop all per-key effects and run the stored cleanups.
677
+
678
+ Under the hood there is:
679
+
680
+ - One effect watching `Reflect.ownKeys(source)` (similar to how `mapped()` tracks `length`)
681
+ - A child effect per key that re-runs whenever that key’s value changes, automatically reusing and replacing the cleanup you returned.
682
+ - Automatic disposal when keys disappear or when `target[cleanup]()` is invoked.
683
+
684
+ #### Re-creating `mapped`-style records
685
+
686
+ ```typescript
687
+ const metrics = reactive({ success: 3, errors: 1 })
688
+
689
+ const readable = organized(metrics, (access, target) => {
690
+ target[access.key] = `${String(access.key)}: ${access.get()}`
691
+ return () => delete target[access.key]
692
+ })
693
+
694
+ console.log(readable.success) // "success: 3"
695
+ metrics.errors = 4
696
+ console.log(readable.errors) // "errors: 4"
697
+ ```
698
+
699
+ #### Partitioning into buckets
700
+
701
+ `organized()` also covers the “partition” helper use case: classify properties into groups while keeping leftovers around.
702
+
703
+ ```typescript
704
+ const props = reactive({
705
+ 'if:visible': true,
706
+ 'onClick': () => console.log('click'),
707
+ 'class:warning': true,
708
+ })
709
+
710
+ type Buckets = {
711
+ events: Record<string, Function>
712
+ classes: Record<string, boolean>
713
+ plain: Record<string, unknown>
714
+ }
715
+
716
+ const buckets = organized(
717
+ props,
718
+ (access, target) => {
719
+ const match = String(access.key).match(/^([^:]+):(.+)$/)
720
+ if (!match) {
721
+ target.plain[String(access.key)] = access.get()
722
+ return () => delete target.plain[String(access.key)]
723
+ }
724
+
725
+ const [, group, name] = match
726
+ if (group === 'if') {
727
+ target.plain[name] = access.get()
728
+ return () => delete target.plain[name]
729
+ }
730
+ if (group === 'class') {
731
+ target.classes[name] = Boolean(access.get())
732
+ return () => delete target.classes[name]
733
+ }
734
+ if (group === 'on') {
735
+ target.events[name] = access.get() as Function
736
+ return () => delete target.events[name]
737
+ }
738
+ },
739
+ { events: {}, classes: {}, plain: {} } satisfies Buckets
740
+ )
741
+ ```
742
+
743
+ Every cleanup removes the entry it created, keeping each bucket in sync with the current source props. This is the same pattern you can use to build a `partitioned()` helper or to manage “mounted” cleanup callbacks keyed by property.
744
+
745
+ #### Feeding other data structures
746
+
747
+ Because the target can be anything, you can build `Map`s, arrays of keys, or richer objects:
748
+
749
+ ```typescript
750
+ const registry = organized(
751
+ reactive({ foo: 1 }),
752
+ (access, target) => {
753
+ target.entries.set(access.key, access.get())
754
+ target.allKeys.add(access.key)
755
+ return () => {
756
+ target.entries.delete(access.key)
757
+ target.allKeys.delete(access.key)
758
+ }
759
+ },
760
+ { entries: new Map<PropertyKey, number>(), allKeys: new Set<PropertyKey>() }
761
+ )
762
+ ```
763
+
764
+ This flexibility makes `organized()` a good base for higher-level utilities such as `mappedKeys`, `partitioned`, or “group by” helpers: implement the logic once inside `apply`, export the tailored function, and reuse the same underlying reactive infrastructure.
765
+
766
+ > **Tip:** If you only need a simple per-key transform with the same shape, return a new record and skip custom `baseTarget`. When you need buckets, metadata, or parallel cleanup tracking, seed `baseTarget` with the structures you plan to mutate.
767
+