mutts 1.0.0

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 (82) hide show
  1. package/README.md +150 -0
  2. package/dist/chunks/decorator-BXsign4Z.js +176 -0
  3. package/dist/chunks/decorator-BXsign4Z.js.map +1 -0
  4. package/dist/chunks/decorator-CPbZNnsX.esm.js +168 -0
  5. package/dist/chunks/decorator-CPbZNnsX.esm.js.map +1 -0
  6. package/dist/decorator.d.ts +50 -0
  7. package/dist/decorator.esm.js +2 -0
  8. package/dist/decorator.esm.js.map +1 -0
  9. package/dist/decorator.js +11 -0
  10. package/dist/decorator.js.map +1 -0
  11. package/dist/destroyable.d.ts +48 -0
  12. package/dist/destroyable.esm.js +91 -0
  13. package/dist/destroyable.esm.js.map +1 -0
  14. package/dist/destroyable.js +98 -0
  15. package/dist/destroyable.js.map +1 -0
  16. package/dist/eventful.d.ts +11 -0
  17. package/dist/eventful.esm.js +88 -0
  18. package/dist/eventful.esm.js.map +1 -0
  19. package/dist/eventful.js +90 -0
  20. package/dist/eventful.js.map +1 -0
  21. package/dist/index.d.ts +15 -0
  22. package/dist/index.esm.js +7 -0
  23. package/dist/index.esm.js.map +1 -0
  24. package/dist/index.js +52 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/indexable.d.ts +31 -0
  27. package/dist/indexable.esm.js +85 -0
  28. package/dist/indexable.esm.js.map +1 -0
  29. package/dist/indexable.js +89 -0
  30. package/dist/indexable.js.map +1 -0
  31. package/dist/mutts.umd.js +2 -0
  32. package/dist/mutts.umd.js.map +1 -0
  33. package/dist/mutts.umd.min.js +2 -0
  34. package/dist/mutts.umd.min.js.map +1 -0
  35. package/dist/promiseChain.d.ts +11 -0
  36. package/dist/promiseChain.esm.js +72 -0
  37. package/dist/promiseChain.esm.js.map +1 -0
  38. package/dist/promiseChain.js +74 -0
  39. package/dist/promiseChain.js.map +1 -0
  40. package/dist/reactive.d.ts +114 -0
  41. package/dist/reactive.esm.js +1455 -0
  42. package/dist/reactive.esm.js.map +1 -0
  43. package/dist/reactive.js +1472 -0
  44. package/dist/reactive.js.map +1 -0
  45. package/dist/std-decorators.d.ts +17 -0
  46. package/dist/std-decorators.esm.js +161 -0
  47. package/dist/std-decorators.esm.js.map +1 -0
  48. package/dist/std-decorators.js +169 -0
  49. package/dist/std-decorators.js.map +1 -0
  50. package/docs/decorator.md +300 -0
  51. package/docs/destroyable.md +294 -0
  52. package/docs/events.md +225 -0
  53. package/docs/indexable.md +561 -0
  54. package/docs/promiseChain.md +218 -0
  55. package/docs/reactive.md +2072 -0
  56. package/docs/std-decorators.md +558 -0
  57. package/package.json +132 -0
  58. package/src/decorator.test.ts +495 -0
  59. package/src/decorator.ts +205 -0
  60. package/src/destroyable.test.ts +155 -0
  61. package/src/destroyable.ts +158 -0
  62. package/src/eventful.test.ts +380 -0
  63. package/src/eventful.ts +69 -0
  64. package/src/index.ts +7 -0
  65. package/src/indexable.test.ts +388 -0
  66. package/src/indexable.ts +124 -0
  67. package/src/promiseChain.test.ts +201 -0
  68. package/src/promiseChain.ts +99 -0
  69. package/src/reactive/array.test.ts +923 -0
  70. package/src/reactive/array.ts +352 -0
  71. package/src/reactive/core.test.ts +1663 -0
  72. package/src/reactive/core.ts +866 -0
  73. package/src/reactive/index.ts +28 -0
  74. package/src/reactive/interface.test.ts +1477 -0
  75. package/src/reactive/interface.ts +231 -0
  76. package/src/reactive/map.test.ts +866 -0
  77. package/src/reactive/map.ts +162 -0
  78. package/src/reactive/set.test.ts +289 -0
  79. package/src/reactive/set.ts +142 -0
  80. package/src/std-decorators.test.ts +679 -0
  81. package/src/std-decorators.ts +182 -0
  82. package/src/utils.ts +52 -0
@@ -0,0 +1,2072 @@
1
+ # Reactive Documentation
2
+
3
+
4
+ ## Introduction
5
+
6
+ ### What is Reactivity?
7
+
8
+ Reactivity is a programming paradigm where the system automatically tracks dependencies between data and automatically updates when data changes. This library provides a powerful, lightweight reactive system for JavaScript/TypeScript applications.
9
+
10
+ ### Core Concepts
11
+
12
+ - **Reactive Objects**: Plain JavaScript objects wrapped with reactive capabilities
13
+ - **Effects**: Functions that automatically re-run when their dependencies change
14
+ - **Dependencies**: Reactive properties that an effect depends on
15
+ - **Evolution Tracking**: Built-in change history for reactive objects
16
+ - **Collections**: Reactive wrappers for Array, Map, Set, WeakMap, and WeakSet
17
+
18
+ ### Basic Example
19
+
20
+ ```typescript
21
+ import { reactive, effect } from 'mutts/reactive'
22
+
23
+ // Create a reactive object
24
+ const user = reactive({ name: "John", age: 30 })
25
+
26
+ // Create an effect that depends on user properties
27
+ effect(() => {
28
+ console.log(`User: ${user.name}, Age: ${user.age}`)
29
+ })
30
+
31
+ // When properties change, the effect automatically re-runs
32
+ user.name = "Jane" // Triggers effect
33
+ user.age = 25 // Triggers effect
34
+ ```
35
+
36
+ ## Getting Started
37
+
38
+ ### Installation
39
+
40
+ ```bash
41
+ npm install mutts
42
+ ```
43
+
44
+ ### Basic Usage
45
+
46
+ ```typescript
47
+ import { reactive, effect } from 'mutts/reactive'
48
+
49
+ // Make an object reactive
50
+ const state = reactive({
51
+ count: 0,
52
+ message: "Hello"
53
+ })
54
+
55
+ // Create reactive effects
56
+ effect(() => {
57
+ console.log(`Count: ${state.count}`)
58
+ })
59
+
60
+ effect(() => {
61
+ console.log(`Message: ${state.message}`)
62
+ })
63
+
64
+ // Changes trigger effects automatically
65
+ state.count++ // Triggers first effect
66
+ state.message = "Hi" // Triggers second effect
67
+ ```
68
+
69
+ ### Hello World Example
70
+
71
+ ```typescript
72
+ import { reactive, effect } from 'mutts/reactive'
73
+
74
+ // Simple counter
75
+ const counter = reactive({ value: 0 })
76
+
77
+ effect(() => {
78
+ document.body.innerHTML = `Count: ${counter.value}`
79
+ })
80
+
81
+ // Button click handler
82
+ document.getElementById('increment').onclick = () => {
83
+ counter.value++
84
+ }
85
+ ```
86
+
87
+ ## Core API
88
+
89
+ ### `reactive()`
90
+
91
+ Makes an object reactive by wrapping it in a proxy.
92
+
93
+ ```typescript
94
+ function reactive<T extends Record<PropertyKey, any>>(target: T): T
95
+ ```
96
+
97
+ **Parameters:**
98
+ - `target`: The object to make reactive
99
+
100
+ **Returns:** A reactive proxy of the original object
101
+
102
+ **Example:**
103
+ ```typescript
104
+ const obj = { count: 0 }
105
+ const reactiveObj = reactive(obj)
106
+
107
+ // reactiveObj is now reactive
108
+ effect(() => {
109
+ console.log(reactiveObj.count) // Tracks dependency
110
+ })
111
+
112
+ reactiveObj.count = 5 // Triggers effect
113
+ ```
114
+
115
+ **Note:** The same object will always return the same proxy instance.
116
+
117
+ ### `effect()`
118
+
119
+ Creates a reactive effect that automatically re-runs when dependencies change.
120
+
121
+ ```typescript
122
+ function effect(
123
+ fn: (dep: DependencyFunction, ...args: any[]) => (ScopedCallback | undefined | void),
124
+ ...args: any[]
125
+ ): ScopedCallback
126
+ ```
127
+
128
+ **Parameters:**
129
+ - `fn`: The effect function that provides dependencies and may return a cleanup function
130
+ - `...args`: Additional arguments that are forwarded to the effect function
131
+
132
+ **Returns:** A cleanup function to stop the effect
133
+
134
+ **Example:**
135
+ ```typescript
136
+ const state = reactive({ count: 0, mood: 'happy' })
137
+
138
+ const cleanup = effect(() => {
139
+ console.log(`Count is: ${state.count}`)
140
+ // Optional cleanup called before next run
141
+ return () => console.log('Cleaning up...')
142
+ })
143
+
144
+ state.count++ // Does trigger: 1- the cleaning, 2- the effect
145
+ state.mood = 'surprised' // Does not trigger the effect
146
+
147
+ // Later...
148
+ cleanup() // Stops the effect
149
+ ```
150
+
151
+ **Using effect with arguments (useful in loops):**
152
+ ```typescript
153
+ const items = reactive([{ id: 1 }, { id: 2 }, { id: 3 }])
154
+
155
+ // Create effects in a loop, passing loop variables
156
+ for (let i = 0; i < items.length; i++) {
157
+ effect((dep, index) => {
158
+ console.log(`Item ${index}:`, items[index])
159
+ }, i) // Pass the loop variable as argument
160
+ }
161
+ ```
162
+
163
+ ### `unwrap()`
164
+
165
+ Gets the original, non-reactive object from a reactive proxy.
166
+
167
+ ```typescript
168
+ function unwrap<T>(proxy: T): T
169
+ ```
170
+
171
+ **Example:**
172
+ ```typescript
173
+ const original = { count: 0 }
174
+ const reactive = reactive(original)
175
+ const unwrapped = unwrap(reactive)
176
+
177
+ console.log(unwrapped === original) // true
178
+ console.log(unwrapped === reactive) // false
179
+ ```
180
+
181
+ ### `isReactive()`
182
+
183
+ Checks if an object is a reactive proxy.
184
+
185
+ ```typescript
186
+ function isReactive(obj: any): boolean
187
+ ```
188
+
189
+ ### `isNonReactive()`
190
+
191
+ Checks if an object is marked as non-reactive.
192
+
193
+ ```typescript
194
+ function isNonReactive(obj: any): boolean
195
+ ```
196
+
197
+ ## Effect System
198
+
199
+ ### Basic Effects
200
+
201
+ Effects are the core of the reactive system. They automatically track dependencies and re-run when those dependencies change.
202
+
203
+ ```typescript
204
+ const state = reactive({ count: 0, name: "John" })
205
+
206
+ effect(() => {
207
+ // This effect depends on state.count
208
+ console.log(`Count: ${state.count}`)
209
+ })
210
+
211
+ // Only changing count triggers the effect
212
+ state.count = 5 // Triggers effect
213
+ state.name = "Jane" // Does NOT trigger effect
214
+ ```
215
+
216
+ ### Effect Cleanup
217
+
218
+ Effects return cleanup functions that you can call to stop tracking dependencies.
219
+
220
+ ```typescript
221
+ const state = reactive({ count: 0 })
222
+
223
+ const stopEffect = effect(() => {
224
+ console.log(`Count: ${state.count}`)
225
+ })
226
+
227
+ // Later...
228
+ stopEffect() // Stops the effect
229
+
230
+ state.count = 10 // No longer triggers the effect
231
+ ```
232
+
233
+ ### Automatic Effect Cleanup
234
+
235
+ The reactive system provides **automatic cleanup** for effects, making memory management much easier. You **do not** have to call the cleanup function in most cases, but you **may** want to, especially if your effects have side effects like timers, DOM manipulation, or event listeners.
236
+
237
+ #### How Automatic Cleanup Works
238
+
239
+ 1. **Parent-Child Independence**: When an effect is created inside another effect, it becomes a "child" of the parent effect. However, when the parent effect is cleaned up, child effects become independent and are NOT automatically cleaned up - they continue to run until they're garbage collected or explicitly cleaned up.
240
+
241
+ 2. **Garbage Collection Cleanup**: For top-level effects (not created inside other effects) and orphaned child effects, the system uses JavaScript's garbage collection to automatically clean them up when they're no longer referenced.
242
+
243
+ #### Examples
244
+
245
+ **Parent-Child Independence:**
246
+ ```typescript
247
+ const state = reactive({ a: 1, b: 2 })
248
+
249
+ const stopParent = effect(() => {
250
+ state.a
251
+
252
+ // Child effect - becomes independent when parent is cleaned up
253
+ effect(() => {
254
+ state.b
255
+ return () => console.log('Child cleanup')
256
+ })
257
+
258
+ return () => console.log('Parent cleanup')
259
+ })
260
+
261
+ // Only cleans up the parent - child becomes independent
262
+ stopParent() // Logs: "Parent cleanup" (child continues running)
263
+ ```
264
+
265
+ **Garbage Collection Cleanup:**
266
+ ```typescript
267
+ const state = reactive({ value: 1 })
268
+
269
+ // Top-level effect - automatically cleaned up via garbage collection
270
+ effect(() => {
271
+ state.value
272
+ return () => console.log('GC cleanup')
273
+ })
274
+
275
+ // No explicit cleanup needed - will be cleaned up when garbage collected
276
+ ```
277
+
278
+ **When You Should Store Cleanup Functions:**
279
+
280
+ While cleanup is automatic, you should **store and remember** cleanup functions when your effects have side effects that need immediate cleanup:
281
+
282
+ ```typescript
283
+ const state = reactive({ value: 1 })
284
+ const activeEffects: (() => void)[] = []
285
+
286
+ // Store cleanup functions for effects with side effects
287
+ for (let i = 0; i < 3; i++) {
288
+ const stopEffect = effect(() => {
289
+ state.value
290
+
291
+ // Side effect that needs immediate cleanup
292
+ const intervalId = setInterval(() => {
293
+ console.log(`Timer ${i} running`)
294
+ }, 1000)
295
+
296
+ return () => {
297
+ clearInterval(intervalId) // Prevent memory leaks
298
+ }
299
+ })
300
+
301
+ activeEffects.push(stopEffect)
302
+ }
303
+
304
+ // Clean up all effects when needed
305
+ activeEffects.forEach(stop => stop())
306
+ ```
307
+
308
+ **Key Points:**
309
+
310
+ - **You do not have to call cleanup** - the system handles it automatically via garbage collection
311
+ - **You may want to call cleanup** - especially for effects with side effects
312
+ - **You have to store and remember cleanup** - when you need immediate control over when effects stop
313
+ - **Child effects become independent** - when their parent effect is cleaned up, they continue running
314
+ - **All effects use garbage collection** - as the primary cleanup mechanism
315
+
316
+ ### Effect Dependencies
317
+
318
+ Effects automatically track which reactive properties they access.
319
+
320
+ ```typescript
321
+ const state = reactive({ a: 1, b: 2, c: 3 })
322
+
323
+ effect(() => {
324
+ // Only tracks state.a and state.b
325
+ console.log(`Sum: ${state.a + state.b}`)
326
+ })
327
+
328
+ // Only these changes trigger the effect
329
+ state.a = 5 // Triggers effect
330
+ state.b = 10 // Triggers effect
331
+ state.c = 15 // Does NOT trigger effect
332
+ ```
333
+
334
+ ### Async Effects and the `dep` Parameter
335
+
336
+ The `effect` function provides a special `dep` parameter that restores the active effect context for dependency tracking in asynchronous operations. This is crucial because async functions lose the global active effect context when they yield control.
337
+
338
+ #### The Problem with Async Effects
339
+
340
+ When an effect function is synchronous, reactive property access automatically tracks dependencies, however, in async functions, the active effect context is lost when the function yields control.
341
+
342
+ The `dep` parameter restores the active effect context for dependency tracking.
343
+
344
+ #### Understanding the active effect context
345
+
346
+ The reactive system uses a global active effect variable to track which effect is currently running:
347
+
348
+ ```typescript
349
+ // Synchronous effect - active effect is maintained throughout
350
+ effect(() => {
351
+ // active effect = this effect
352
+ const value = state.count // ✅ Tracked (active effect is set)
353
+ // active effect = this effect (still set)
354
+ const another = state.name // ✅ Tracked (active effect is still set)
355
+ })
356
+
357
+ // Async effect - active effect is lost after await
358
+ effect(async () => {
359
+ // active effect = this effect
360
+ const value = state.count // ✅ Tracked (active effect is set)
361
+
362
+ await someAsyncOperation() // Function exits, active effect = undefined
363
+
364
+ // active effect = undefined (lost!)
365
+ const another = state.name // ❌ NOT tracked (no active effect)
366
+ })
367
+
368
+ // Async effect with dep() - active effect is restored
369
+ effect(async (dep) => {
370
+ // active effect = this effect
371
+ const value = state.count // ✅ Tracked (active effect is set)
372
+
373
+ await someAsyncOperation() // Function exits, active effect = undefined
374
+
375
+ // dep() temporarily restores active effect for the callback
376
+ const another = dep(() => state.name) // ✅ Tracked (active effect restored)
377
+ })
378
+ ```
379
+
380
+ #### Key Benefits of `dep()` in Async Effects
381
+
382
+ 1. **Restored Context**: `dep()` temporarily restores the active effect context for dependency tracking
383
+ 2. **Consistent Tracking**: Reactive property access works the same way before and after `await`
384
+
385
+ ### Nested Effects
386
+
387
+ Effects can be created inside other effects and will have separate effect scopes:
388
+
389
+ ```typescript
390
+ import { effect, reactive } from 'mutts/reactive'
391
+
392
+ const state = reactive({ a: 0, b: 0 })
393
+
394
+ const stopOuter = effect(() => {
395
+ state.a
396
+
397
+ // Create an inner effect with its own scope
398
+ const stopInner = effect(() => {
399
+ state.b
400
+ })
401
+
402
+ // Return cleanup function for the inner effect
403
+ return stopInner
404
+ })
405
+ ```
406
+
407
+ ### `untracked()`
408
+
409
+ The `untracked()` function allows you to run code without tracking dependencies, which can be useful for creating effects or performing operations that shouldn't be part of the current effect's dependency graph.
410
+
411
+ ```typescript
412
+ import { effect, untracked, reactive } from 'mutts/reactive'
413
+
414
+ const state = reactive({ a: 0, b: 0 })
415
+
416
+ effect(() => {
417
+ state.a
418
+
419
+ // Create an inner effect without tracking the creation under the outer effect
420
+ let stopInner: (() => void) | undefined
421
+ untracked(() => {
422
+ stopInner = effect(() => {
423
+ state.b
424
+ })
425
+ })
426
+
427
+ // Optionally stop it immediately to avoid accumulating watchers
428
+ stopInner && stopInner()
429
+ })
430
+ ```
431
+
432
+ **Use cases for `untracked()`:**
433
+ - Creating effects inside other effects without coupling their dependencies
434
+ - Performing side effects that shouldn't trigger when dependencies change
435
+ - Avoiding circular dependencies in complex reactive systems
436
+
437
+ ### Effect Options and debugging
438
+
439
+ Configure the reactive system behavior:
440
+
441
+ ```typescript
442
+ import { options as reactiveOptions } from 'mutts/reactive'
443
+
444
+ // Set maximum effect chain depth
445
+ reactiveOptions.maxEffectChain = 50
446
+
447
+ // Set maximum deep watch traversal depth
448
+ reactiveOptions.maxDeepWatchDepth = 200
449
+
450
+ // Enable debug logging
451
+ reactiveOptions.enter = (effect) => console.log('Entering effect:', effect)
452
+ reactiveOptions.leave = (effect) => console.log('Leaving effect:', effect)
453
+ reactiveOptions.chain = (caller, target) => console.log('Chaining:', caller, '->', target)
454
+ ```
455
+
456
+ ## Advanced Effects
457
+
458
+ ### Recording Results inside Effects
459
+
460
+ If you want to capture derived values from an effect, push them from inside the effect body. Use the returned cleanup to manage resources between runs.
461
+
462
+ ```typescript
463
+ const state = reactive({ count: 0 })
464
+ const results: number[] = []
465
+
466
+ const stop = effect(() => {
467
+ results.push(state.count * 2)
468
+ return () => {
469
+ // cleanup between runs if needed
470
+ }
471
+ })
472
+
473
+ state.count = 5 // results: [0, 10]
474
+ stop()
475
+ ```
476
+
477
+ ### Effect Cleanup Functions
478
+
479
+ Effects can return cleanup functions that run before the next execution.
480
+
481
+ ```typescript
482
+ const state = reactive({ count: 0 })
483
+
484
+ effect(() => {
485
+ const interval = setInterval(() => {
486
+ console.log('Count:', state.count)
487
+ }, 1000)
488
+
489
+ return () => {
490
+ clearInterval(interval)
491
+ console.log('Cleaned up interval')
492
+ }
493
+ })
494
+
495
+ // When state.count changes, the cleanup runs first
496
+ state.count = 5
497
+ ```
498
+
499
+ ### Effect Lifecycle
500
+
501
+ 1. **Initial Run**: Effect runs immediately when created
502
+ 2. **Dependency Change**: When dependencies change, cleanup runs, then effect re-runs
503
+ 3. **Manual Stop**: Calling the cleanup function stops the effect permanently
504
+
505
+ ```typescript
506
+ const state = reactive({ count: 0 })
507
+
508
+ const stop = effect(() => {
509
+ console.log('Effect running, count:', state.count)
510
+ return () => console.log('Cleanup running')
511
+ })
512
+
513
+ console.log('Effect created')
514
+
515
+ state.count = 5
516
+ // Output:
517
+ // Effect created
518
+ // Effect running, count: 0
519
+ // Cleanup running
520
+ // Effect running, count: 5
521
+
522
+ stop() // Effect stops permanently
523
+ ```
524
+
525
+ ### Effect Arguments
526
+
527
+ Effects can accept additional arguments that are forwarded to the effect function. This is particularly useful in loops or when you need to pass context to the effect:
528
+
529
+ ```typescript
530
+ const items = reactive([
531
+ { id: 1, name: 'Item 1' },
532
+ { id: 2, name: 'Item 2' },
533
+ { id: 3, name: 'Item 3' }
534
+ ])
535
+
536
+ // Create effects for each item with the item index
537
+ const effectCleanups: (() => void)[] = []
538
+
539
+ for (let i = 0; i < items.length; i++) {
540
+ const cleanup = effect((dep, index) => {
541
+ console.log(`Item ${index}:`, items[index].name)
542
+
543
+ // The index is captured in the closure and passed as argument
544
+ return () => console.log(`Cleaning up effect for item ${index}`)
545
+ }, i)
546
+
547
+ effectCleanups.push(cleanup)
548
+ }
549
+
550
+ // Later, clean up all effects
551
+ effectCleanups.forEach(cleanup => cleanup())
552
+ ```
553
+
554
+ ### Watch Function
555
+
556
+ The `watch` function provides a more direct way to observe changes in reactive objects. It comes in two forms:
557
+
558
+ #### Watch with Value Function
559
+
560
+ ```typescript
561
+ const state = reactive({ count: 0, name: 'John' })
562
+
563
+ const stop = watch(
564
+ () => state.count, // Value function
565
+ (newValue, oldValue) => {
566
+ console.log(`Count changed from ${oldValue} to ${newValue}`)
567
+ }
568
+ )
569
+
570
+ state.count = 5 // Triggers: "Count changed from 0 to 5"
571
+ state.name = 'Jane' // No trigger (not watching name)
572
+ ```
573
+
574
+ #### Watch Object Properties
575
+
576
+ The second form of `watch` allows you to watch any property change on a reactive object:
577
+
578
+ ```typescript
579
+ const user = reactive({
580
+ name: 'John',
581
+ age: 30,
582
+ email: 'john@example.com'
583
+ })
584
+
585
+ const stop = watch(
586
+ user, // The reactive object to watch
587
+ () => {
588
+ console.log('Any property of user changed!')
589
+ console.log('Current user:', user)
590
+ }
591
+ )
592
+
593
+ user.name = 'Jane' // Triggers the callback
594
+ user.age = 31 // Triggers the callback
595
+ user.email = 'jane@example.com' // Triggers the callback
596
+ ```
597
+
598
+ #### Use Cases
599
+
600
+ **Object-level watching** is particularly useful for:
601
+
602
+ - **Form validation**: Watch all form fields for changes
603
+ - **Auto-save**: Save whenever any field in a document changes
604
+ - **Logging**: Track all changes to a state object
605
+ - **Dirty checking**: Detect if any property has been modified
606
+
607
+ ```typescript
608
+ const form = reactive({
609
+ firstName: '',
610
+ lastName: '',
611
+ email: '',
612
+ isValid: false
613
+ })
614
+
615
+ const stop = watch(form, () => {
616
+ // Auto-save whenever any field changes
617
+ saveForm(form)
618
+
619
+ // Update validation status
620
+ form.isValid = form.firstName && form.lastName && form.email
621
+ })
622
+
623
+ // Any change to firstName, lastName, or email will trigger auto-save
624
+ form.firstName = 'John'
625
+ form.lastName = 'Doe'
626
+ form.email = 'john.doe@example.com'
627
+ ```
628
+
629
+ #### Deep Watching
630
+
631
+ For both forms of `watch`, you can enable deep watching by passing `{ deep: true }` in the options:
632
+
633
+ ```typescript
634
+ const state = reactive({
635
+ user: {
636
+ name: 'John',
637
+ profile: { age: 30 }
638
+ }
639
+ })
640
+
641
+ // Deep watch - triggers on nested property changes
642
+ const stop = watch(state, () => {
643
+ console.log('Any nested property changed!')
644
+ }, { deep: true })
645
+
646
+ state.user.name = 'Jane' // Triggers
647
+ state.user.profile.age = 31 // Triggers (nested change)
648
+ ```
649
+
650
+ **Deep Watching Behavior:**
651
+
652
+ - **Unreactive objects are skipped**: Deep watching will not traverse into objects marked as unreactive using `@unreactive` or `unreactive()`
653
+ - **Collections are handled specially**:
654
+ - **Arrays**: All elements and length changes are tracked
655
+ - **Sets**: All values are tracked (keys are not separate in Sets)
656
+ - **Maps**: All values are tracked (keys are not tracked separately)
657
+ - **WeakSet/WeakMap**: Cannot be deep watched (not iterable), only replacement triggers
658
+ - **Circular references**: Handled safely - There is also a configurable depth limit
659
+ - **Performance**: Deep watching has higher overhead than shallow watching
660
+
661
+ ```typescript
662
+ // Example with collections
663
+ const state = reactive({
664
+ items: [1, 2, 3],
665
+ tags: new Set(['a', 'b']),
666
+ metadata: new Map([['key1', 'value1']])
667
+ })
668
+
669
+ const stop = watch(state, () => {
670
+ console.log('Collection changed')
671
+ }, { deep: true })
672
+
673
+ state.items.push(4) // Triggers
674
+ state.items[0] = 10 // Triggers
675
+ state.tags.add('c') // Triggers
676
+ state.metadata.set('key2', 'value2') // Triggers
677
+
678
+ // WeakSet/WeakMap only trigger on replacement
679
+ const weakSet = new WeakSet()
680
+ state.weakData = weakSet // Triggers (replacement)
681
+ // Changes to weakSet contents won't trigger (not trackable)
682
+ ```
683
+
684
+ #### Cleanup
685
+
686
+ Both forms of `watch` return a cleanup function:
687
+
688
+ ```typescript
689
+ const stop = watch(user, () => {
690
+ console.log('User changed')
691
+ })
692
+
693
+ // Later, stop watching
694
+ stop()
695
+ ```
696
+
697
+ ## Evolution Tracking
698
+
699
+ ### Understanding Object Evolution
700
+
701
+ The reactive system tracks how objects change over time, creating an "evolution history" that you can inspect.
702
+
703
+ ```typescript
704
+ import { getState } from './reactive'
705
+
706
+ const obj = reactive({ count: 0 })
707
+ let state = getState(obj)
708
+
709
+ effect(() => {
710
+ while ('evolution' in state) {
711
+ console.log('Change:', state.evolution)
712
+ state = state.next
713
+ }
714
+
715
+ console.log('Current count:', obj.count)
716
+ })
717
+
718
+ obj.count = 5
719
+ obj.count = 10
720
+ ```
721
+
722
+ ### `getState()` - Accessing Change History
723
+
724
+ Returns the current state object for tracking evolution.
725
+
726
+ ```typescript
727
+ function getState(obj: any): State
728
+ ```
729
+
730
+ **Returns:** A state object that does contain evolution information ( = `{}`) but will in a next evolution call if the object state has evolved.
731
+
732
+ ### Evolution Types
733
+
734
+ The system tracks different types of changes:
735
+
736
+ - **`add`**: Property was added
737
+ - **`set`**: Property value was changed
738
+ - **`del`**: Property was deleted
739
+ - **`bunch`**: Collection operation (array methods, map/set operations)
740
+
741
+ ```typescript
742
+ const obj = reactive({ count: 0 })
743
+ let state = getState(obj)
744
+
745
+ effect(() => {
746
+ let changes = []
747
+ while ('evolution' in state) {
748
+ changes.push(state.evolution)
749
+ state = state.next
750
+ }
751
+
752
+ if (changes.length > 0) {
753
+ console.log('Changes since last effect:', changes)
754
+ }
755
+ })
756
+
757
+ obj.count = 5 // { type: 'set', prop: 'count' }
758
+ obj.newProp = 'test' // { type: 'add', prop: 'newProp' }
759
+ delete obj.count // { type: 'del', prop: 'count' }
760
+
761
+ // Array operations
762
+ const array = reactive([1, 2, 3])
763
+ array.push(4) // { type: 'bunch', method: 'push' }
764
+ array.reverse() // { type: 'bunch', method: 'reverse' }
765
+ ```
766
+
767
+ ### Change History Patterns
768
+
769
+ Common patterns for using evolution tracking:
770
+
771
+ ```typescript
772
+ // Pattern 1: Count changes
773
+ let state = getState(obj)
774
+ effect(() => {
775
+ let changes = 0
776
+ while ('evolution' in state) {
777
+ changes++
778
+ state = state.next
779
+ }
780
+
781
+ if (changes > 0) {
782
+ console.log(`Detected ${changes} changes`)
783
+ state = getState(obj) // Reset for next run
784
+ }
785
+ })
786
+
787
+ // Pattern 2: Filter specific changes
788
+ let state = getState(obj)
789
+ effect(() => {
790
+ const relevantChanges = []
791
+ while ('evolution' in state) {
792
+ if (state.evolution.type === 'set') {
793
+ relevantChanges.push(state.evolution)
794
+ }
795
+ state = state.next
796
+ }
797
+
798
+ if (relevantChanges.length > 0) {
799
+ console.log('Property updates:', relevantChanges)
800
+ state = getState(obj)
801
+ }
802
+ })
803
+ ```
804
+
805
+ ## Collections
806
+
807
+ ### `ReactiveMap`
808
+
809
+ A reactive wrapper around JavaScript's `Map` class.
810
+
811
+ ```typescript
812
+ const map = reactive(new Map([['key1', 'value1']]))
813
+
814
+ effect(() => {
815
+ console.log('Map size:', map.size)
816
+ console.log('Has key1:', map.has('key1'))
817
+ })
818
+
819
+ map.set('key2', 'value2') // Triggers effect
820
+ map.delete('key1') // Triggers effect
821
+ ```
822
+
823
+ **Features:**
824
+ - Tracks `size` changes
825
+ - Tracks individual key operations
826
+ - Tracks collection-wide operations via `allProps`
827
+
828
+ ### `ReactiveWeakMap`
829
+
830
+ A reactive wrapper around JavaScript's `WeakMap` class.
831
+
832
+ ```typescript
833
+ const weakMap = reactive(new WeakMap())
834
+ const key = { id: 1 }
835
+
836
+ effect(() => {
837
+ console.log('Has key:', weakMap.has(key))
838
+ })
839
+
840
+ weakMap.set(key, 'value') // Triggers effect
841
+ weakMap.delete(key) // Triggers effect
842
+ ```
843
+
844
+ **Features:**
845
+ - Only tracks individual key operations
846
+ - No `size` tracking (WeakMap limitation)
847
+ - No collection-wide operations
848
+
849
+ ### `ReactiveSet`
850
+
851
+ A reactive wrapper around JavaScript's `Set` class.
852
+
853
+ ```typescript
854
+ const set = reactive(new Set([1, 2, 3]))
855
+
856
+ effect(() => {
857
+ console.log('Set size:', set.size)
858
+ console.log('Has 1:', set.has(1))
859
+ })
860
+
861
+ set.add(4) // Triggers effect
862
+ set.delete(1) // Triggers effect
863
+ set.clear() // Triggers effect
864
+ ```
865
+
866
+ **Features:**
867
+ - Tracks `size` changes
868
+ - Tracks individual value operations
869
+ - Tracks collection-wide operations
870
+
871
+ ### `ReactiveWeakSet`
872
+
873
+ A reactive wrapper around JavaScript's `WeakSet` class.
874
+
875
+ ```typescript
876
+ const weakSet = reactive(new WeakSet())
877
+ const obj = { id: 1 }
878
+
879
+ effect(() => {
880
+ console.log('Has obj:', weakSet.has(obj))
881
+ })
882
+
883
+ weakSet.add(obj) // Triggers effect
884
+ weakSet.delete(obj) // Triggers effect
885
+ ```
886
+
887
+ ### Collection-Specific Reactivity
888
+
889
+ Collections provide different levels of reactivity:
890
+
891
+ ```typescript
892
+ const map = reactive(new Map())
893
+
894
+ // Size tracking
895
+ effect(() => {
896
+ console.log('Map size:', map.size)
897
+ })
898
+
899
+ // Individual key tracking
900
+ effect(() => {
901
+ console.log('Value for key1:', map.get('key1'))
902
+ })
903
+
904
+ // Collection-wide tracking
905
+ effect(() => {
906
+ for (const [key, value] of map) {
907
+ // This effect depends on allProps
908
+ }
909
+ })
910
+
911
+ // Operations trigger different effects
912
+ map.set('key1', 'value1') // Triggers size and key1 effects
913
+ map.set('key2', 'value2') // Triggers size and allProps effects
914
+ map.delete('key1') // Triggers size, key1, and allProps effects
915
+ ```
916
+
917
+ ## ReactiveArray
918
+
919
+ ### `ReactiveArray`
920
+
921
+ A reactive wrapper around JavaScript's `Array` class with full array method support.
922
+
923
+ ```typescript
924
+ const array = reactive([1, 2, 3])
925
+
926
+ effect(() => {
927
+ console.log('Array length:', array.length)
928
+ console.log('First element:', array[0])
929
+ })
930
+
931
+ array.push(4) // Triggers effect
932
+ array[0] = 10 // Triggers effect
933
+ ```
934
+
935
+ **Features:**
936
+ - Tracks `length` changes
937
+ - Tracks individual index operations
938
+ - Tracks collection-wide operations via `allProps`
939
+ - Supports all array methods with proper reactivity
940
+
941
+ ### Array Methods
942
+
943
+ All standard array methods are supported with reactivity:
944
+
945
+ ```typescript
946
+ const array = reactive([1, 2, 3])
947
+
948
+ // Mutator methods
949
+ array.push(4) // Triggers length and allProps effects
950
+ array.pop() // Triggers length and allProps effects
951
+ array.shift() // Triggers length and allProps effects
952
+ array.unshift(0) // Triggers length and allProps effects
953
+ array.splice(1, 1, 10) // Triggers length and allProps effects
954
+ array.reverse() // Triggers allProps effects
955
+ array.sort() // Triggers allProps effects
956
+ array.fill(0) // Triggers allProps effects
957
+ array.copyWithin(0, 2) // Triggers allProps effects
958
+
959
+ // Accessor methods (immutable)
960
+ const reversed = array.toReversed()
961
+ const sorted = array.toSorted()
962
+ const spliced = array.toSpliced(1, 1)
963
+ const withNew = array.with(0, 100)
964
+ ```
965
+
966
+ ### Index Access
967
+
968
+ ReactiveArray supports both positive and negative index access:
969
+
970
+ ```typescript
971
+ const array = reactive([1, 2, 3, 4, 5])
972
+
973
+ effect(() => {
974
+ console.log('First element:', array[0])
975
+ console.log('Last element:', array.at(-1))
976
+ })
977
+
978
+ array[0] = 10 // Triggers effect
979
+ array[4] = 50 // Triggers effect
980
+ ```
981
+
982
+ ### Length Reactivity
983
+
984
+ The `length` property is fully reactive:
985
+
986
+ ```typescript
987
+ const array = reactive([1, 2, 3])
988
+
989
+ effect(() => {
990
+ console.log('Array length:', array.length)
991
+ })
992
+
993
+ array.push(4) // Triggers effect
994
+ array.length = 2 // Triggers effect
995
+ array[5] = 10 // Triggers effect (expands array)
996
+ ```
997
+
998
+ ### Array Evolution Tracking
999
+
1000
+ Array operations generate specific evolution events:
1001
+
1002
+ ```typescript
1003
+ const array = reactive([1, 2, 3])
1004
+ let state = getState(array)
1005
+
1006
+ effect(() => {
1007
+ while ('evolution' in state) {
1008
+ console.log('Array change:', state.evolution)
1009
+ state = state.next
1010
+ }
1011
+ })
1012
+
1013
+ array.push(4) // { type: 'bunch', method: 'push' }
1014
+ array[0] = 10 // { type: 'set', prop: 0 }
1015
+ array[5] = 20 // { type: 'add', prop: 5 }
1016
+ ```
1017
+
1018
+ ### Array-Specific Reactivity Patterns
1019
+
1020
+ ```typescript
1021
+ const array = reactive([1, 2, 3])
1022
+
1023
+ // Track specific indices
1024
+ effect(() => {
1025
+ console.log('First two elements:', array[0], array[1])
1026
+ })
1027
+
1028
+ // Track length changes
1029
+ effect(() => {
1030
+ console.log('Array size changed to:', array.length)
1031
+ })
1032
+
1033
+ // Track all elements (via iteration)
1034
+ effect(() => {
1035
+ for (const item of array) {
1036
+ // This effect depends on allProps
1037
+ }
1038
+ })
1039
+
1040
+ // Track specific array methods
1041
+ effect(() => {
1042
+ const lastElement = array.at(-1)
1043
+ console.log('Last element:', lastElement)
1044
+ })
1045
+ ```
1046
+
1047
+ ### Performance Considerations
1048
+
1049
+ ReactiveArray is optimized for common array operations:
1050
+
1051
+ ```typescript
1052
+ // Efficient: Direct index access
1053
+ effect(() => {
1054
+ console.log(array[0]) // Only tracks index 0
1055
+ })
1056
+
1057
+ // Efficient: Length tracking
1058
+ effect(() => {
1059
+ console.log(array.length) // Only tracks length
1060
+ })
1061
+
1062
+ // Less efficient: Iteration tracks all elements
1063
+ effect(() => {
1064
+ array.forEach(item => console.log(item)) // Tracks allProps
1065
+ })
1066
+ ```
1067
+
1068
+ ## Class Reactivity
1069
+
1070
+ ### `@reactive` Decorator
1071
+
1072
+ The `@reactive` decorator makes class instances automatically reactive. This is the recommended approach for adding reactivity to classes.
1073
+
1074
+ ```typescript
1075
+ import { reactive } from 'mutts/reactive'
1076
+
1077
+ @reactive
1078
+ class User {
1079
+ name: string
1080
+ age: number
1081
+
1082
+ constructor(name: string, age: number) {
1083
+ this.name = name
1084
+ this.age = age
1085
+ }
1086
+
1087
+ updateAge(newAge: number) {
1088
+ this.age = newAge
1089
+ }
1090
+ }
1091
+
1092
+ const user = new User("John", 30)
1093
+
1094
+ effect(() => {
1095
+ console.log(`User: ${user.name}, Age: ${user.age}`)
1096
+ })
1097
+
1098
+ user.updateAge(31) // Triggers effect
1099
+ user.name = "Jane" // Triggers effect
1100
+ ```
1101
+
1102
+ ### Functional Syntax
1103
+
1104
+ You can also use the functional syntax for making classes reactive:
1105
+
1106
+ ```typescript
1107
+ import { reactive } from 'mutts/reactive'
1108
+
1109
+ class User {
1110
+ name: string
1111
+ age: number
1112
+
1113
+ constructor(name: string, age: number) {
1114
+ this.name = name
1115
+ this.age = age
1116
+ }
1117
+
1118
+ updateAge(newAge: number) {
1119
+ this.age = newAge
1120
+ }
1121
+ }
1122
+
1123
+ const ReactiveUser = reactive(User)
1124
+ const user = new ReactiveUser("John", 30)
1125
+
1126
+ effect(() => {
1127
+ console.log(`User: ${user.name}, Age: ${user.age}`)
1128
+ })
1129
+
1130
+ user.updateAge(31) // Triggers effect
1131
+ user.name = "Jane" // Triggers effect
1132
+ ```
1133
+
1134
+ ### `ReactiveBase` for Complex Inheritance
1135
+
1136
+ For complex inheritance trees, especially when you need to solve constructor reactivity issues, extend `ReactiveBase`:
1137
+
1138
+ ```typescript
1139
+ import { ReactiveBase, reactive } from 'mutts/reactive'
1140
+
1141
+ class GameObject extends ReactiveBase {
1142
+ id = 'game-object'
1143
+ position = { x: 0, y: 0 }
1144
+ }
1145
+
1146
+ class Entity extends GameObject {
1147
+ health = 100
1148
+ }
1149
+
1150
+ @reactive
1151
+ class Player extends Entity {
1152
+ name = 'Player'
1153
+ level = 1
1154
+ }
1155
+
1156
+ const player = new Player()
1157
+
1158
+ effect(() => {
1159
+ console.log(`Player ${player.name} at (${player.position.x}, ${player.position.y})`)
1160
+ })
1161
+
1162
+ player.position.x = 10 // Triggers effect
1163
+ player.health = 80 // Triggers effect
1164
+ ```
1165
+
1166
+ **Advantages of `ReactiveBase`:**
1167
+
1168
+ 1. **Constructor Reactivity**: Solves the issue where `this` in the constructor is not yet reactive
1169
+ 2. **Inheritance Safety**: Prevents reactivity from being added to prototype chains in complex inheritance trees
1170
+ 3. **No Side Effects**: The base class itself has no effect - it only enables proper reactivity when combined with `@reactive`
1171
+
1172
+ ### Choosing the Right Approach
1173
+
1174
+ **Use `@reactive` decorator when:**
1175
+ - You have simple classes without complex inheritance
1176
+ - You want the cleanest, most modern syntax
1177
+ - You don't need to modify or use `this` in the constructor
1178
+
1179
+ **Use `ReactiveBase` + `@reactive` when:**
1180
+ - You have complex inheritance trees (like game objects, UI components)
1181
+ - You need to modify or use `this` in the constructor
1182
+ - You want to prevent reactivity from being added to prototype chains
1183
+
1184
+ ### Making Existing Class Instances Reactive
1185
+
1186
+ You can also make existing class instances reactive:
1187
+
1188
+ ```typescript
1189
+ class Counter {
1190
+ count = 0
1191
+
1192
+ increment() {
1193
+ this.count++
1194
+ }
1195
+ }
1196
+
1197
+ const counter = new Counter()
1198
+ const reactiveCounter = reactive(counter)
1199
+
1200
+ effect(() => {
1201
+ console.log('Count:', reactiveCounter.count)
1202
+ })
1203
+
1204
+ reactiveCounter.increment() // Triggers effect
1205
+ ```
1206
+
1207
+ ### Method Reactivity
1208
+
1209
+ Methods that modify properties automatically trigger effects:
1210
+
1211
+ ```typescript
1212
+ @reactive
1213
+ class ShoppingCart {
1214
+ items: string[] = []
1215
+
1216
+ addItem(item: string) {
1217
+ this.items.push(item)
1218
+ }
1219
+
1220
+ removeItem(item: string) {
1221
+ const index = this.items.indexOf(item)
1222
+ if (index > -1) {
1223
+ this.items.splice(index, 1)
1224
+ }
1225
+ }
1226
+ }
1227
+
1228
+ const cart = new ShoppingCart()
1229
+
1230
+ effect(() => {
1231
+ console.log('Cart items:', cart.items)
1232
+ })
1233
+
1234
+ cart.addItem('Apple') // Triggers effect
1235
+ cart.removeItem('Apple') // Triggers effect
1236
+ ```
1237
+
1238
+ ### Inheritance Support
1239
+
1240
+ The `@reactive` decorator works with inheritance:
1241
+
1242
+ ```typescript
1243
+ @reactive
1244
+ class Animal {
1245
+ species: string
1246
+
1247
+ constructor(species: string) {
1248
+ this.species = species
1249
+ }
1250
+ }
1251
+
1252
+ class Dog extends Animal {
1253
+ breed: string
1254
+
1255
+ constructor(breed: string) {
1256
+ super('Canis')
1257
+ this.breed = breed
1258
+ }
1259
+ }
1260
+
1261
+ const dog = new Dog('Golden Retriever')
1262
+
1263
+ effect(() => {
1264
+ console.log(`${dog.species}: ${dog.breed}`)
1265
+ })
1266
+
1267
+ dog.breed = 'Labrador' // Triggers effect
1268
+ ```
1269
+
1270
+ **Note**: When using inheritance with the `@reactive` decorator, apply it to the base class. The decorator will automatically handle inheritance properly.
1271
+
1272
+ ## Non-Reactive System
1273
+
1274
+ ### `unreactive()`
1275
+
1276
+ Marks objects or classes as non-reactive, preventing them from being wrapped.
1277
+
1278
+ ```typescript
1279
+ function unreactive<T>(target: T): T
1280
+ function unreactive(target: Constructor<T>): Constructor<T>
1281
+ ```
1282
+
1283
+ **Examples:**
1284
+
1285
+ ```typescript
1286
+ // Mark individual object as non-reactive
1287
+ const obj = { count: 0 }
1288
+ unreactive(obj)
1289
+ const reactiveObj = reactive(obj) // Returns obj unchanged
1290
+
1291
+ // Mark entire class as non-reactive
1292
+ class Utility {
1293
+ static helper() { return 'help' }
1294
+ }
1295
+ unreactive(Utility)
1296
+ const instance = new Utility()
1297
+ const reactiveInstance = reactive(instance) // Returns instance unchanged
1298
+ ```
1299
+
1300
+ ### `@unreactive` Decorator
1301
+
1302
+ Mark class properties as non-reactive.
1303
+
1304
+ ```typescript
1305
+ @reactive
1306
+ class User {
1307
+ @unreactive
1308
+ id: string = 'user-123'
1309
+
1310
+ name: string = 'John'
1311
+ age: number = 30
1312
+ }
1313
+
1314
+ const user = new User()
1315
+
1316
+ effect(() => {
1317
+ console.log(user.name, user.age) // Tracks these
1318
+ console.log(user.id) // Does NOT track this
1319
+ })
1320
+
1321
+ user.name = 'Jane' // Triggers effect
1322
+ user.id = 'new-id' // Does NOT trigger effect
1323
+ ```
1324
+
1325
+ ### Non-Reactive Classes
1326
+
1327
+ Classes marked as non-reactive bypass the reactive system entirely:
1328
+
1329
+ ```typescript
1330
+ class Config {
1331
+ apiUrl: string = 'https://api.example.com'
1332
+ timeout: number = 5000
1333
+ }
1334
+
1335
+ unreactive(Config)
1336
+ ```
1337
+ -or-
1338
+ ```typescript
1339
+ @unreactive
1340
+ class Config {
1341
+ apiUrl: string = 'https://api.example.com'
1342
+ timeout: number = 5000
1343
+ }
1344
+
1345
+ const ReactiveConfig = reactive(Config)
1346
+ const config = new ReactiveConfig()
1347
+
1348
+ effect(() => {
1349
+ console.log('Config:', config.apiUrl, config.timeout)
1350
+ })
1351
+
1352
+ // These changes won't trigger effects
1353
+ config.apiUrl = 'https://new-api.example.com'
1354
+ config.timeout = 10000
1355
+ ```
1356
+
1357
+ ### Making Special Reactive Objects Non-Reactive
1358
+
1359
+ Special reactive objects (Arrays, Maps, Sets, WeakMaps, WeakSets) can also be made non-reactive:
1360
+
1361
+ ```typescript
1362
+ // Make individual reactive collections non-reactive
1363
+ const array = reactive([1, 2, 3])
1364
+ unreactive(array) // array is no longer reactive
1365
+
1366
+ const map = reactive(new Map([['key', 'value']]))
1367
+ unreactive(map) // map is no longer reactive
1368
+
1369
+ const set = reactive(new Set([1, 2, 3]))
1370
+ unreactive(set) // set is no longer reactive
1371
+ ```
1372
+
1373
+ **Making reactive collection classes non-reactive:**
1374
+
1375
+ ```typescript
1376
+ // Make the entire ReactiveArray class non-reactive
1377
+ unreactive(ReactiveArray)
1378
+
1379
+ // Now all ReactiveArray instances will be non-reactive
1380
+ const array = reactive([1, 2, 3]) // Returns non-reactive array
1381
+ const reactiveArray = new ReactiveArray([1, 2, 3]) // Non-reactive instance
1382
+
1383
+ // Make other reactive collection classes non-reactive
1384
+ unreactive(ReactiveMap)
1385
+ unreactive(ReactiveSet)
1386
+ unreactive(ReactiveWeakMap)
1387
+ unreactive(ReactiveWeakSet)
1388
+ ```
1389
+
1390
+ **Use cases for non-reactive collections:**
1391
+ - Large datasets that don't need reactivity
1392
+ - Performance-critical operations
1393
+ - Static configuration data
1394
+ - Temporary data structures
1395
+
1396
+ ### Performance Considerations
1397
+
1398
+ Non-reactive objects can improve performance:
1399
+
1400
+ ```typescript
1401
+ // Good: Mark large, rarely-changing objects as non-reactive
1402
+ const config = unreactive({
1403
+ apiEndpoints: { /* large config object */ },
1404
+ featureFlags: { /* many flags */ }
1405
+ })
1406
+
1407
+ // Good: Mark utility classes as non-reactive
1408
+ class MathUtils {
1409
+ static PI = 3.14159
1410
+ static square(x: number) { return x * x }
1411
+ }
1412
+ unreactive(MathUtils)
1413
+
1414
+ // Good: Mark properties that don't need reactivity
1415
+ class User {
1416
+ @unreactive
1417
+ metadata: any = {} // Large metadata object
1418
+
1419
+ name: string = 'John' // This should be reactive
1420
+ }
1421
+ ```
1422
+
1423
+ ## Computed Properties
1424
+
1425
+ ### `@computed` Decorator
1426
+
1427
+ Creates computed properties that cache their values and only recompute when dependencies change.
1428
+
1429
+ ```typescript
1430
+ @reactive
1431
+ class Calculator {
1432
+ @computed
1433
+ get area() {
1434
+ return this.width * this.height
1435
+ }
1436
+
1437
+ width: number = 10
1438
+ height: number = 5
1439
+ }
1440
+
1441
+ const calc = new Calculator()
1442
+
1443
+ effect(() => {
1444
+ console.log('Area:', calc.area)
1445
+ })
1446
+
1447
+ calc.width = 20 // Triggers effect, recomputes area
1448
+ calc.height = 10 // Triggers effect, recomputes area
1449
+ ```
1450
+
1451
+ ### Computed Functions
1452
+
1453
+ Create computed values outside of classes:
1454
+
1455
+ ```typescript
1456
+ const state = reactive({ a: 1, b: 2 })
1457
+
1458
+ const sum = computed(() => state.a + state.b)
1459
+ const product = computed(() => state.a * state.b)
1460
+
1461
+ effect(() => {
1462
+ console.log('Sum:', sum, 'Product:', product)
1463
+ })
1464
+
1465
+ state.a = 5 // Both computed values update
1466
+ ```
1467
+
1468
+ ### Caching and Invalidation
1469
+
1470
+ Computed values are cached until their dependencies change:
1471
+
1472
+ ```typescript
1473
+ const state = reactive({ x: 1, y: 2 })
1474
+
1475
+ let computeCount = 0
1476
+ const expensive = computed(() => {
1477
+ computeCount++
1478
+ console.log('Computing expensive value...')
1479
+ return Math.pow(state.x, state.y)
1480
+ })
1481
+
1482
+ console.log('First access:', expensive) // Computes once
1483
+ console.log('Second access:', expensive) // Uses cached value
1484
+ console.log('Compute count:', computeCount) // 1
1485
+
1486
+ state.x = 2 // Invalidates cache
1487
+ console.log('After change:', expensive) // Recomputes
1488
+ console.log('Compute count:', computeCount) // 2
1489
+ ```
1490
+
1491
+ ### Computed vs Effects
1492
+
1493
+ Choose between computed and effects based on your needs:
1494
+
1495
+ ```typescript
1496
+ const state = reactive({ count: 0 })
1497
+
1498
+ // Use computed when you need a value
1499
+ const doubled = computed(() => state.count * 2)
1500
+
1501
+ // Use effect when you need side effects
1502
+ effect(() => {
1503
+ console.log('Count doubled:', doubled)
1504
+ document.title = `Count: ${state.count}`
1505
+ })
1506
+
1507
+ state.count = 5
1508
+ // doubled becomes 10
1509
+ // effect logs and updates title
1510
+ ```
1511
+
1512
+ ## Advanced Patterns
1513
+
1514
+ ### Custom Reactive Objects
1515
+
1516
+ Create custom reactive objects with specialized behavior:
1517
+
1518
+ ```typescript
1519
+ class ReactiveArray<T> {
1520
+ private items: T[] = []
1521
+
1522
+ push(item: T) {
1523
+ this.items.push(item)
1524
+ touched(this, 'length')
1525
+ touched(this, 'allProps')
1526
+ }
1527
+
1528
+ get length() {
1529
+ dependant(this, 'length')
1530
+ return this.items.length
1531
+ }
1532
+
1533
+ get(index: number) {
1534
+ dependant(this, index)
1535
+ return this.items[index]
1536
+ }
1537
+ }
1538
+
1539
+ const array = new ReactiveArray<number>()
1540
+ effect(() => {
1541
+ console.log('Array length:', array.length)
1542
+ })
1543
+
1544
+ array.push(1) // Triggers effect
1545
+ ```
1546
+
1547
+ ### Native Reactivity Registration
1548
+
1549
+ Register custom reactive classes for automatic wrapping:
1550
+
1551
+ ```typescript
1552
+ import { registerNativeReactivity } from 'mutts/reactive'
1553
+
1554
+ class CustomMap<K, V> {
1555
+ private data = new Map<K, V>()
1556
+
1557
+ set(key: K, value: V) {
1558
+ this.data.set(key, value)
1559
+ // Custom reactivity logic
1560
+ }
1561
+
1562
+ get(key: K) {
1563
+ return this.data.get(key)
1564
+ }
1565
+ }
1566
+
1567
+ class ReactiveCustomMap<K, V> extends CustomMap<K, V> {
1568
+ // Reactive wrapper implementation
1569
+ }
1570
+
1571
+ registerNativeReactivity(CustomMap, ReactiveCustomMap)
1572
+
1573
+ // Now CustomMap instances are automatically wrapped
1574
+ const customMap = reactive(new CustomMap())
1575
+ ```
1576
+
1577
+ ### Memory Management
1578
+
1579
+ The reactive system uses WeakMaps to avoid memory leaks:
1580
+
1581
+ ```typescript
1582
+ // Objects can be garbage collected when no longer referenced
1583
+ let obj = { data: 'large object' }
1584
+ const reactiveObj = reactive(obj)
1585
+
1586
+ effect(() => {
1587
+ console.log(reactiveObj.data)
1588
+ })
1589
+
1590
+ // Remove reference
1591
+ obj = null
1592
+ reactiveObj = null
1593
+
1594
+ // The original object and its reactive wrapper can be GC'd
1595
+ ```
1596
+
1597
+ ### Performance Optimization
1598
+
1599
+ Optimize reactive performance:
1600
+
1601
+ ```typescript
1602
+ // 1. Use non-reactive for static data
1603
+ const staticConfig = unreactive({
1604
+ apiUrl: 'https://api.example.com',
1605
+ version: '1.0.0'
1606
+ })
1607
+
1608
+ // 2. Batch changes when possible
1609
+ const state = reactive({ a: 1, b: 2, c: 3 })
1610
+
1611
+ // Instead of:
1612
+ state.a = 10
1613
+ state.b = 20
1614
+ state.c = 30
1615
+
1616
+ // Consider batching or using a single update method
1617
+
1618
+ // 3. Avoid unnecessary reactivity
1619
+ @reactive
1620
+ class User {
1621
+ @unreactive
1622
+ private internalId = crypto.randomUUID() // Never changes
1623
+
1624
+ name: string = 'John' // Changes, should be reactive
1625
+ }
1626
+
1627
+ // 4. Use appropriate collection types
1628
+ const smallSet = new ReactiveSet(new Set()) // For small collections
1629
+ const largeMap = new ReactiveMap(new Map()) // For large collections with key access
1630
+ ```
1631
+
1632
+ ## Debugging and Development
1633
+
1634
+ ### Debug Options
1635
+
1636
+ Configure debug behavior:
1637
+
1638
+ ```typescript
1639
+ import { options as reactiveOptions } from 'mutts/reactive'
1640
+
1641
+ // Track effect entry/exit
1642
+ reactiveOptions.enter = (effect) => {
1643
+ console.log('🔵 Entering effect:', effect.name || 'anonymous')
1644
+ }
1645
+
1646
+ reactiveOptions.leave = (effect) => {
1647
+ console.log('🔴 Leaving effect:', effect.name || 'anonymous')
1648
+ }
1649
+
1650
+ // Track effect chaining
1651
+ reactiveOptions.chain = (caller, target) => {
1652
+ console.log('⛓️ Effect chain:', caller.name || 'anonymous', '->', target.name || 'anonymous')
1653
+ }
1654
+
1655
+ // Set maximum chain depth
1656
+ reactiveOptions.maxEffectChain = 50
1657
+
1658
+ // Set maximum deep watch traversal depth
1659
+ reactiveOptions.maxDeepWatchDepth = 200
1660
+ ```
1661
+
1662
+ ### Effect Stack Traces
1663
+
1664
+ Debug effect execution:
1665
+
1666
+ ```typescript
1667
+ const state = reactive({ count: 0 })
1668
+
1669
+ effect(() => {
1670
+ console.trace('Effect running')
1671
+ console.log('Count:', state.count)
1672
+ })
1673
+
1674
+ // This will show the call stack when the effect runs
1675
+ state.count = 5
1676
+ ```
1677
+
1678
+ ### Evolution Inspection
1679
+
1680
+ Inspect object evolution history:
1681
+
1682
+ ```typescript
1683
+ const obj = reactive({ x: 1 })
1684
+ let state = getState(obj)
1685
+
1686
+ effect(() => {
1687
+ console.log('=== Evolution History ===')
1688
+ let depth = 0
1689
+ while ('evolution' in state) {
1690
+ console.log(`${' '.repeat(depth)}${state.evolution.type}: ${state.evolution.prop}`)
1691
+ state = state.next
1692
+ depth++
1693
+ }
1694
+ console.log('=== End History ===')
1695
+
1696
+ // Reset state reference
1697
+ state = getState(obj)
1698
+ })
1699
+
1700
+ obj.x = 2
1701
+ obj.y = 'new'
1702
+ delete obj.x
1703
+ ```
1704
+
1705
+ ## API Reference
1706
+
1707
+ ### Decorators
1708
+
1709
+ #### `@computed`
1710
+
1711
+ Marks a class accessor as computed. The computed value will be cached and invalidated when dependencies change.
1712
+
1713
+ ```typescript
1714
+ class MyClass {
1715
+ private _value = 0;
1716
+
1717
+ @computed
1718
+ get doubled() {
1719
+ return this._value * 2;
1720
+ }
1721
+ }
1722
+
1723
+ // Function usage
1724
+ function myExpensiveCalculus() {
1725
+
1726
+ }
1727
+ ...
1728
+ const result = computed(myExpensiveCalculus);
1729
+ ```
1730
+
1731
+ **Use Cases:**
1732
+ - Caching expensive calculations
1733
+ - Derived state that depends on reactive values
1734
+ - Computed properties in classes
1735
+ - Performance optimization for frequently accessed values
1736
+
1737
+ **Notes:**
1738
+ By how JS works, writing `computed(()=> ...)` will always be wrong, as the notation `()=> ...` internally is a `new Function(...)`.
1739
+ So, even if the return value is cached, it will never be used.
1740
+
1741
+ #### `@unreactive`
1742
+
1743
+ Marks a class property as non-reactive. The property change will not be tracked by the reactive system.
1744
+
1745
+ Marks a class (and its descendants) as non-reactive.
1746
+
1747
+ ```typescript
1748
+ class MyClass {
1749
+ @unreactive
1750
+ private config = { theme: 'dark' };
1751
+ }
1752
+
1753
+ // Class decorator usage
1754
+ @unreactive
1755
+ class NonReactiveClass {
1756
+ // All instances will be non-reactive
1757
+ }
1758
+
1759
+ // Function usage
1760
+ const nonReactiveObj = unreactive({ config: { theme: 'dark' } });
1761
+ ```
1762
+
1763
+ **Use Cases:**
1764
+ - Configuration objects that shouldn't trigger reactivity
1765
+ - Static data that never changes
1766
+ - Performance optimization for objects that don't need tracking
1767
+ - Third-party objects that shouldn't be made reactive
1768
+
1769
+ ### Core Functions
1770
+
1771
+ #### `reactive<T>(target: T): T`
1772
+
1773
+ Creates a reactive proxy of the target object. All property access and mutations will be tracked.
1774
+
1775
+ **Use Cases:**
1776
+ - Converting plain objects to reactive objects
1777
+ - Making class instances reactive
1778
+ - Creating reactive arrays, maps, and sets
1779
+ - Setting up reactive state management
1780
+
1781
+ ```typescript
1782
+ const state = reactive({ count: 0, name: 'John' });
1783
+ const items = reactive([1, 2, 3]);
1784
+ const map = reactive(new Map([['key', 'value']]));
1785
+ ```
1786
+
1787
+ #### `effect(fn, ...args): ScopedCallback`
1788
+
1789
+ Creates a reactive effect that runs when its dependencies change.
1790
+
1791
+ **Use Cases:**
1792
+ - Side effects like DOM updates
1793
+ - Logging and debugging
1794
+ - Data synchronization
1795
+ - Cleanup operations
1796
+
1797
+ ```typescript
1798
+ const cleanup = effect((dep) => {
1799
+ console.log('Count changed:', state.count);
1800
+ return () => {
1801
+ // Cleanup function
1802
+ };
1803
+ });
1804
+ ```
1805
+
1806
+ #### `computed<T>(getter: ComputedFunction<T>): T`
1807
+
1808
+ Creates a computed value that caches its result and recomputes when dependencies change.
1809
+
1810
+ **Use Cases:**
1811
+ - Derived state calculations
1812
+ - Expensive computations
1813
+ - Data transformations
1814
+ - Conditional logic based on reactive state
1815
+
1816
+ ```typescript
1817
+ const result = computed(someExpensiveCalculus);
1818
+ ```
1819
+
1820
+ #### `watch(value, callback, options?): ScopedCallback`
1821
+
1822
+ Watches a reactive value or function and calls a callback when it changes.
1823
+
1824
+ **Use Cases:**
1825
+ - Reacting to specific value changes
1826
+ - Debugging reactive state
1827
+ - Side effects for specific properties
1828
+ - Data validation
1829
+
1830
+ ```typescript
1831
+ // Watch a specific value
1832
+ const stop = watch(() => state.count, (newVal, oldVal) => {
1833
+ console.log(`Count changed from ${oldVal} to ${newVal}`);
1834
+ });
1835
+
1836
+ // Watch an object with deep option
1837
+ const stopDeep = watch(state, (newState) => {
1838
+ console.log('State changed:', newState);
1839
+ }, { deep: true, immediate: true });
1840
+ ```
1841
+
1842
+ #### `unwrap<T>(proxy: T): T`
1843
+
1844
+ Returns the original object from a reactive proxy.
1845
+
1846
+ **Use Cases:**
1847
+ - Accessing original object for serialization
1848
+ - Passing to non-reactive functions
1849
+ - Performance optimization
1850
+ - Debugging
1851
+
1852
+ ```typescript
1853
+ const original = unwrap(reactiveState);
1854
+ JSON.stringify(original); // Safe to serialize
1855
+ ```
1856
+
1857
+ #### `isReactive(obj: any): boolean`
1858
+
1859
+ Checks if an object is a reactive proxy.
1860
+
1861
+ **Use Cases:**
1862
+ - Type checking
1863
+ - Debugging
1864
+ - Conditional logic
1865
+ - Validation
1866
+
1867
+ ```typescript
1868
+ if (isReactive(obj)) {
1869
+ console.log('Object is reactive');
1870
+ }
1871
+ ```
1872
+
1873
+ #### `isNonReactive(obj: any): boolean`
1874
+
1875
+ Checks if an object is marked as non-reactive.
1876
+
1877
+ **Use Cases:**
1878
+ - Validation
1879
+ - Debugging
1880
+ - Conditional logic
1881
+ - Type checking
1882
+
1883
+ ```typescript
1884
+ if (isNonReactive(obj)) {
1885
+ console.log('Object is non-reactive');
1886
+ }
1887
+ ```
1888
+
1889
+ #### `untracked(fn: () => void): void`
1890
+
1891
+ Executes a function without tracking dependencies.
1892
+
1893
+ **Use Cases:**
1894
+ - Performance optimization
1895
+ - Avoiding circular dependencies
1896
+ - Side effects that shouldn't trigger reactivity
1897
+ - Batch operations
1898
+
1899
+ ```typescript
1900
+ untracked(() => {
1901
+ // This won't create dependencies
1902
+ console.log('Untracked operation');
1903
+ });
1904
+ ```
1905
+
1906
+ #### `getState(obj)`
1907
+
1908
+ Gets the current state of a reactive object. Used internally for tracking changes.
1909
+
1910
+ **Use Cases:**
1911
+ - Debugging reactive state
1912
+ - Custom reactive implementations
1913
+ - State inspection
1914
+
1915
+ #### `invalidateComputed(callback, warn?)`
1916
+
1917
+ Registers a callback to be called when a computed property is invalidated.
1918
+
1919
+ **Use Cases:**
1920
+ - Custom computed implementations
1921
+ - Cleanup operations
1922
+ - Performance monitoring
1923
+
1924
+ ```typescript
1925
+ const computed = computed(() => {
1926
+ invalidateComputed(() => {
1927
+ console.log('Computed invalidated');
1928
+ });
1929
+ return expensiveCalculation();
1930
+ });
1931
+ ```
1932
+
1933
+ ### Configuration
1934
+
1935
+ #### `reactiveOptions`
1936
+
1937
+ Global options for the reactive system.
1938
+
1939
+ **Properties:**
1940
+ - `enter(effect: Function)`: Called when an effect is entered
1941
+ - `leave(effect: Function)`: Called when an effect is left
1942
+ - `chain(target: Function, caller?: Function)`: Called when effects are chained
1943
+ - `maxEffectChain: number`: Maximum effect chain depth (default: 100)
1944
+ - `maxDeepWatchDepth: number`: Maximum deep watch traversal depth (default: 100)
1945
+ - `instanceMembers: boolean`: Only react on instance members (default: true)
1946
+ - `warn(...args: any[])`: Warning function (default: console.warn)
1947
+
1948
+ **Use Cases:**
1949
+ - Debugging reactive behavior
1950
+ - Performance tuning
1951
+ - Custom logging
1952
+ - Error handling
1953
+
1954
+ ```typescript
1955
+ reactiveOptions.maxEffectChain = 50;
1956
+ reactiveOptions.enter = (effect) => console.log('Effect entered:', effect.name);
1957
+ ```
1958
+
1959
+ ### Classes
1960
+
1961
+ #### `ReactiveBase`
1962
+
1963
+ Base class for reactive objects. When extended, instances are automatically made reactive.
1964
+
1965
+ **Use Cases:**
1966
+ - Creating reactive classes
1967
+ - Automatic reactivity for class instances
1968
+ - Type safety for reactive objects
1969
+
1970
+ ```typescript
1971
+ class MyState extends ReactiveBase {
1972
+ count = 0;
1973
+ name = '';
1974
+ }
1975
+
1976
+ const state = new MyState(); // Automatically reactive
1977
+ ```
1978
+
1979
+ #### `ReactiveError`
1980
+
1981
+ Error class for reactive system errors.
1982
+
1983
+ **Use Cases:**
1984
+ - Error handling in reactive code
1985
+ - Debugging reactive issues
1986
+ - Custom error types
1987
+
1988
+ ### Collections
1989
+
1990
+ Collections (Array, Map, Set, WeakMap, WeakSet) can be automatically made reactive when passed to `reactive()`:
1991
+
1992
+ ```typescript
1993
+ const items = reactive([1, 2, 3]);
1994
+ items.push(4); // Triggers reactivity
1995
+
1996
+ const map = reactive(new Map([['key', 'value']]));
1997
+ map.set('newKey', 'newValue'); // Triggers reactivity
1998
+
1999
+ const set = reactive(new Set([1, 2, 3]));
2000
+ set.add(4); // Triggers reactivity
2001
+ ```
2002
+
2003
+ #### Automatic Collection Reactivity
2004
+
2005
+ All native collections have their specific management:
2006
+
2007
+ ```typescript
2008
+ // Collections still need to be wrapped with reactive()
2009
+ const arr = reactive([1, 2, 3]) // ReactiveArray
2010
+ const map = reactive(new Map()) // ReactiveMap
2011
+ const set = reactive(new Set()) // ReactiveSet
2012
+ const weakMap = reactive(new WeakMap()) // ReactiveWeakMap
2013
+ const weakSet = reactive(new WeakSet()) // ReactiveWeakSet
2014
+
2015
+ effect(() => {
2016
+ console.log('Array length:', arr.length)
2017
+ console.log('Map size:', map.size)
2018
+ console.log('Set size:', set.size)
2019
+ })
2020
+
2021
+ arr.push(4) // Triggers effect
2022
+ map.set('key', 'value') // Triggers effect
2023
+ set.add('item') // Triggers effect
2024
+ ```
2025
+
2026
+ **Use Cases:**
2027
+ - Applications that primarily work with reactive collections
2028
+ - Global reactive state management
2029
+ - Ensuring collection methods (push, set, add, etc.) trigger reactivity
2030
+ - Performance optimization for collection-heavy applications
2031
+
2032
+ **Note:** This module registers native collection types to use specialized reactive wrappers. Without importing this module, collections wrapped with `reactive()` will only have basic object reactivity - collection methods like `map.set()`, `array.push()`, etc. will not trigger effects. The `reactive()` wrapper is still required, but the collections module ensures proper reactive behavior for collection-specific operations.
2033
+
2034
+ ### Types
2035
+
2036
+ #### `ScopedCallback`
2037
+
2038
+ Type for effect cleanup functions.
2039
+
2040
+ #### `DependencyFunction`
2041
+
2042
+ Type for dependency tracking functions used in effects and computed values.
2043
+
2044
+ #### `WatchOptions`
2045
+
2046
+ Options for the watch function:
2047
+ - `immediate?: boolean`: Call callback immediately
2048
+ - `deep?: boolean`: Watch nested properties
2049
+
2050
+ ### Profile Information
2051
+
2052
+ #### `profileInfo`
2053
+
2054
+ Object containing internal reactive system state for debugging and profiling.
2055
+
2056
+ **Properties:**
2057
+ - `objectToProxy`: WeakMap of original objects to their proxies
2058
+ - `proxyToObject`: WeakMap of proxies to their original objects
2059
+ - `effectToReactiveObjects`: WeakMap of effects to watched objects
2060
+ - `watchers`: WeakMap of objects to their property watchers
2061
+ - `objectParents`: WeakMap of objects to their parent relationships
2062
+ - `objectsWithDeepWatchers`: WeakSet of objects with deep watchers
2063
+ - `deepWatchers`: WeakMap of objects to their deep watchers
2064
+ - `effectToDeepWatchedObjects`: WeakMap of effects to deep watched objects
2065
+ - `nonReactiveObjects`: WeakSet of non-reactive objects
2066
+ - `computedCache`: WeakMap of computed functions to their cached values
2067
+
2068
+ **Use Cases:**
2069
+ - Debugging reactive behavior
2070
+ - Performance profiling
2071
+ - Memory leak detection
2072
+ - System state inspection