mutts 1.0.1 → 1.0.3

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 (111) hide show
  1. package/README.md +36 -6
  2. package/dist/chunks/_tslib-BgjropY9.js +81 -0
  3. package/dist/chunks/_tslib-BgjropY9.js.map +1 -0
  4. package/dist/chunks/_tslib-Mzh1rNsX.esm.js +75 -0
  5. package/dist/chunks/_tslib-Mzh1rNsX.esm.js.map +1 -0
  6. package/dist/chunks/{decorator-8qjFb7dw.js → decorator-DLvrD0UF.js} +103 -14
  7. package/dist/chunks/decorator-DLvrD0UF.js.map +1 -0
  8. package/dist/chunks/{decorator-AbRkXM5O.esm.js → decorator-DqiszP7i.esm.js} +100 -15
  9. package/dist/chunks/decorator-DqiszP7i.esm.js.map +1 -0
  10. package/dist/chunks/index-DzUDtFc7.esm.js +4841 -0
  11. package/dist/chunks/index-DzUDtFc7.esm.js.map +1 -0
  12. package/dist/chunks/index-HNVqPzjz.js +4891 -0
  13. package/dist/chunks/index-HNVqPzjz.js.map +1 -0
  14. package/dist/decorator.d.ts +57 -0
  15. package/dist/decorator.esm.js +1 -1
  16. package/dist/decorator.js +1 -1
  17. package/dist/destroyable.d.ts +43 -1
  18. package/dist/destroyable.esm.js +19 -1
  19. package/dist/destroyable.esm.js.map +1 -1
  20. package/dist/destroyable.js +19 -1
  21. package/dist/destroyable.js.map +1 -1
  22. package/dist/devtools/devtools.html +9 -0
  23. package/dist/devtools/devtools.js +5 -0
  24. package/dist/devtools/devtools.js.map +1 -0
  25. package/dist/devtools/manifest.json +8 -0
  26. package/dist/devtools/panel.css +72 -0
  27. package/dist/devtools/panel.html +31 -0
  28. package/dist/devtools/panel.js +13048 -0
  29. package/dist/devtools/panel.js.map +1 -0
  30. package/dist/eventful.d.ts +10 -1
  31. package/dist/eventful.esm.js +5 -27
  32. package/dist/eventful.esm.js.map +1 -1
  33. package/dist/eventful.js +15 -37
  34. package/dist/eventful.js.map +1 -1
  35. package/dist/index.d.ts +18 -14
  36. package/dist/index.esm.js +4 -3
  37. package/dist/index.esm.js.map +1 -1
  38. package/dist/index.js +44 -5
  39. package/dist/index.js.map +1 -1
  40. package/dist/indexable.d.ts +213 -1
  41. package/dist/indexable.esm.js +203 -3
  42. package/dist/indexable.esm.js.map +1 -1
  43. package/dist/indexable.js +204 -2
  44. package/dist/indexable.js.map +1 -1
  45. package/dist/mutts.umd.js +1 -1
  46. package/dist/mutts.umd.js.map +1 -1
  47. package/dist/mutts.umd.min.js +1 -1
  48. package/dist/mutts.umd.min.js.map +1 -1
  49. package/dist/promiseChain.d.ts +10 -0
  50. package/dist/promiseChain.esm.js +6 -0
  51. package/dist/promiseChain.esm.js.map +1 -1
  52. package/dist/promiseChain.js +6 -0
  53. package/dist/promiseChain.js.map +1 -1
  54. package/dist/reactive.d.ts +774 -33
  55. package/dist/reactive.esm.js +4 -1458
  56. package/dist/reactive.esm.js.map +1 -1
  57. package/dist/reactive.js +53 -1474
  58. package/dist/reactive.js.map +1 -1
  59. package/dist/std-decorators.d.ts +35 -0
  60. package/dist/std-decorators.esm.js +36 -1
  61. package/dist/std-decorators.esm.js.map +1 -1
  62. package/dist/std-decorators.js +36 -1
  63. package/dist/std-decorators.js.map +1 -1
  64. package/docs/ai/api-reference.md +133 -0
  65. package/docs/ai/manual.md +105 -0
  66. package/docs/iterableWeak.md +646 -0
  67. package/docs/mixin.md +229 -0
  68. package/docs/reactive/advanced.md +1280 -0
  69. package/docs/reactive/collections.md +767 -0
  70. package/docs/reactive/core.md +973 -0
  71. package/docs/reactive.md +21 -2688
  72. package/package.json +18 -5
  73. package/src/decorator.ts +266 -0
  74. package/src/destroyable.ts +199 -0
  75. package/src/eventful.ts +77 -0
  76. package/src/index.d.ts +9 -0
  77. package/src/index.ts +9 -0
  78. package/src/indexable.ts +484 -0
  79. package/src/introspection.ts +59 -0
  80. package/src/iterableWeak.ts +233 -0
  81. package/src/mixins.ts +123 -0
  82. package/src/promiseChain.ts +110 -0
  83. package/src/reactive/array.ts +414 -0
  84. package/src/reactive/change.ts +134 -0
  85. package/src/reactive/debug.ts +517 -0
  86. package/src/reactive/deep-touch.ts +268 -0
  87. package/src/reactive/deep-watch-state.ts +82 -0
  88. package/src/reactive/deep-watch.ts +168 -0
  89. package/src/reactive/effect-context.ts +94 -0
  90. package/src/reactive/effects.ts +1333 -0
  91. package/src/reactive/index.ts +75 -0
  92. package/src/reactive/interface.ts +223 -0
  93. package/src/reactive/map.ts +171 -0
  94. package/src/reactive/mapped.ts +130 -0
  95. package/src/reactive/memoize.ts +107 -0
  96. package/src/reactive/non-reactive-state.ts +49 -0
  97. package/src/reactive/non-reactive.ts +43 -0
  98. package/src/reactive/project.project.md +93 -0
  99. package/src/reactive/project.ts +335 -0
  100. package/src/reactive/proxy-state.ts +27 -0
  101. package/src/reactive/proxy.ts +285 -0
  102. package/src/reactive/record.ts +196 -0
  103. package/src/reactive/register.ts +421 -0
  104. package/src/reactive/set.ts +144 -0
  105. package/src/reactive/tracking.ts +101 -0
  106. package/src/reactive/types.ts +358 -0
  107. package/src/reactive/zone.ts +208 -0
  108. package/src/std-decorators.ts +217 -0
  109. package/src/utils.ts +117 -0
  110. package/dist/chunks/decorator-8qjFb7dw.js.map +0 -1
  111. package/dist/chunks/decorator-AbRkXM5O.esm.js.map +0 -1
@@ -0,0 +1,973 @@
1
+ # Reactive Documentation
2
+
3
+ ## Table of Contents
4
+
5
+ - [Introduction](#introduction)
6
+ - [Getting Started](#getting-started)
7
+ - [5-Minute Quick Start](#5-minute-quick-start)
8
+ - [Core API](#core-api)
9
+ - [Effect System](#effect-system)
10
+ - [Atomic Operations](#atomic-operations)
11
+ - [Advanced Effects](#advanced-effects)
12
+ - [Evolution Tracking](#evolution-tracking)
13
+ - [Prototype Chains and Pure Objects](#prototype-chains-and-pure-objects)
14
+ - [Recursive Touching](#recursive-touching)
15
+ - [Why Not Deep Watching?](#why-not-deep-watching)
16
+ - [Collections](#collections)
17
+ - [Register](#register)
18
+ - [Class Reactivity](#class-reactivity)
19
+ - [Non-Reactive System](#non-reactive-system)
20
+ - [Array Mapping](#array-mapping)
21
+ - [Projection](#projection)
22
+ - [Record Organization](#record-organization)
23
+ - [Memoization](#memoization)
24
+ - [Debugging and Development](#debugging-and-development)
25
+ - [Cycle Detection](#cycle-detection)
26
+
27
+ ## Introduction
28
+
29
+ ### What is Reactivity?
30
+
31
+ 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.
32
+
33
+ ### Core Concepts
34
+
35
+ - **Reactive Objects**: Plain JavaScript objects wrapped with reactive capabilities
36
+ - **Effects**: Functions that automatically re-run when their dependencies change
37
+ - **Dependencies**: Reactive properties that an effect depends on
38
+ - **Atomic Operations**: Batching multiple state changes to execute effects only once
39
+ - **Evolution Tracking**: Built-in change history for reactive objects
40
+ - **Collections**: Reactive wrappers for Array, Map, Set, WeakMap, and WeakSet
41
+
42
+ ### Basic Example
43
+
44
+ ```typescript
45
+ import { reactive, effect } from 'mutts/reactive'
46
+
47
+ // Create a reactive object
48
+ const user = reactive({ name: "John", age: 30 })
49
+
50
+ // Create an effect that depends on user properties
51
+ effect(() => {
52
+ console.log(`User: ${user.name}, Age: ${user.age}`)
53
+ })
54
+
55
+ // When properties change, the effect automatically re-runs
56
+ user.name = "Jane" // Triggers effect
57
+ user.age = 25 // Triggers effect
58
+ ```
59
+
60
+ ## Getting Started
61
+
62
+ ### 5-Minute Quick Start
63
+
64
+ Learn the essentials in 5 minutes:
65
+
66
+ **1. Make state reactive:**
67
+ ```typescript
68
+ import { reactive, effect } from 'mutts/reactive'
69
+
70
+ const state = reactive({ count: 0, name: "John" })
71
+ ```
72
+
73
+ **2. React to changes:**
74
+ ```typescript
75
+ // Effects automatically re-run when dependencies change
76
+ effect(() => {
77
+ console.log(`Hello ${state.name}, count is ${state.count}`)
78
+ })
79
+
80
+ state.count++ // Triggers effect
81
+ state.name = "Jane" // Triggers effect
82
+ ```
83
+
84
+ **3. Work with arrays:**
85
+ ```typescript
86
+ const items = reactive([1, 2, 3])
87
+
88
+ effect(() => {
89
+ console.log(`Array length: ${items.length}`)
90
+ })
91
+
92
+ items.push(4) // Triggers effect
93
+ ```
94
+
95
+ **4. Use memoization:**
96
+ ```typescript
97
+ const memoized = memoize((user: User) => {
98
+ return expensiveComputation(user)
99
+ })
100
+
101
+ // Only recomputes when user changes
102
+ const result = memoized(user)
103
+ ```
104
+
105
+ **5. Map over arrays:**
106
+ ```typescript
107
+ const source = reactive([1, 2, 3])
108
+ const doubled = mapped(source, x => x * 2)
109
+ // [2, 4, 6]
110
+
111
+ source.push(4) // doubled automatically becomes [2, 4, 6, 8]
112
+ ```
113
+
114
+ **Ready to go!** Continue reading for advanced features.
115
+
116
+ ---
117
+
118
+ ### Installation
119
+
120
+ ```bash
121
+ npm install mutts
122
+ ```
123
+
124
+ ### Basic Usage
125
+
126
+ ```typescript
127
+ import { reactive, effect } from 'mutts/reactive'
128
+
129
+ // Make an object reactive
130
+ const state = reactive({
131
+ count: 0,
132
+ message: "Hello"
133
+ })
134
+
135
+ // Create reactive effects
136
+ effect(() => {
137
+ console.log(`Count: ${state.count}`)
138
+ })
139
+
140
+ effect(() => {
141
+ console.log(`Message: ${state.message}`)
142
+ })
143
+
144
+ // Changes trigger effects automatically
145
+ state.count++ // Triggers first effect
146
+ state.message = "Hi" // Triggers second effect
147
+ ```
148
+
149
+ ### Hello World Example
150
+
151
+ ```typescript
152
+ import { reactive, effect } from 'mutts/reactive'
153
+
154
+ // Simple counter
155
+ const counter = reactive({ value: 0 })
156
+
157
+ effect(() => {
158
+ document.body.innerHTML = `Count: ${counter.value}`
159
+ })
160
+
161
+ // Button click handler
162
+ document.getElementById('increment').onclick = () => {
163
+ counter.value++
164
+ }
165
+ ```
166
+
167
+ ## Core API
168
+
169
+ ### `reactive()`
170
+
171
+ Makes an object reactive by wrapping it in a proxy.
172
+
173
+ ```typescript
174
+ function reactive<T extends Record<PropertyKey, any>>(target: T): T
175
+ ```
176
+
177
+ **Parameters:**
178
+ - `target`: The object to make reactive
179
+
180
+ **Returns:** A reactive proxy of the original object
181
+
182
+ **Example:**
183
+ ```typescript
184
+ const obj = { count: 0 }
185
+ const reactiveObj = reactive(obj)
186
+
187
+ // reactiveObj is now reactive
188
+ effect(() => {
189
+ console.log(reactiveObj.count) // Tracks dependency
190
+ })
191
+
192
+ reactiveObj.count = 5 // Triggers effect
193
+ ```
194
+
195
+ **Note:** The same object will always return the same proxy instance.
196
+
197
+ **Pure Objects and Prototypes:**
198
+
199
+ `reactive()` works with any object type, including:
200
+ - Normal objects: `reactive({ x: 1 })`
201
+ - Pure objects: `reactive(Object.create(null))`
202
+ - Objects with prototypes: `reactive(Object.create(parent))`
203
+ - Class instances: `reactive(new MyClass())`
204
+
205
+ See [Prototype Chains and Pure Objects](#prototype-chains-and-pure-objects) for detailed information about prototype chain handling.
206
+
207
+ ### `effect()`
208
+
209
+ Creates a reactive effect that automatically re-runs when dependencies change.
210
+
211
+ ```typescript
212
+ interface DependencyAccess {
213
+ tracked: DependencyFunction // Track dependencies in the current effect
214
+ ascend: DependencyFunction // Track dependencies in the parent effect
215
+ reaction: boolean // true after the first run
216
+ }
217
+
218
+ function effect(
219
+ fn: (access: DependencyAccess, ...args: any[]) => (ScopedCallback | undefined | void),
220
+ ...args: any[]
221
+ ): ScopedCallback
222
+ ```
223
+
224
+ **Parameters:**
225
+ - `fn`: The effect function that receives dependency access and may return a cleanup function
226
+ - `...args`: Additional arguments that are forwarded to the effect function
227
+
228
+ **DependencyAccess:**
229
+ - `tracked`: Function to track dependencies in the current effect (use this by default)
230
+ - `ascend`: Function to track dependencies in the parent effect (useful for effects that should be cleaned up with their parent)
231
+ - `reaction`: Boolean flag that is `false` during the initial execution and `true` for every subsequent re-run triggered by dependency changes
232
+
233
+ **Returns:** A cleanup function to stop the effect
234
+
235
+ **Example:**
236
+ ```typescript
237
+ const state = reactive({ count: 0, mood: 'happy' })
238
+
239
+ const cleanup = effect(() => {
240
+ console.log(`Count is: ${state.count}`)
241
+ // Optional cleanup called before next run
242
+ return () => console.log('Cleaning up...')
243
+ })
244
+
245
+ state.count++ // Does trigger: 1- the cleaning, 2- the effect
246
+ state.mood = 'surprised' // Does not trigger the effect
247
+
248
+ // Later...
249
+ cleanup() // Stops the effect
250
+ ```
251
+
252
+ You can also branch on the `reaction` flag to separate initialisation logic from update logic:
253
+
254
+ ```typescript
255
+ effect(({ reaction }) => {
256
+ if (!reaction) {
257
+ console.log('Initial render setup')
258
+ } else {
259
+ console.log('Reacting to dependency change')
260
+ }
261
+ })
262
+ ```
263
+
264
+ **Using effect with arguments (useful in loops):**
265
+ ```typescript
266
+ const items = reactive([{ id: 1 }, { id: 2 }, { id: 3 }])
267
+
268
+ // Create effects in a loop, passing loop variables
269
+ for (let i = 0; i < items.length; i++) {
270
+ effect((access, index) => {
271
+ console.log(`Item ${index}:`, items[index])
272
+ }, i) // Pass the loop variable as argument
273
+ }
274
+ ```
275
+
276
+ ### `unwrap()`
277
+
278
+ Gets the original, non-reactive object from a reactive proxy.
279
+
280
+ ```typescript
281
+ function unwrap<T>(proxy: T): T
282
+ ```
283
+
284
+ **Example:**
285
+ ```typescript
286
+ const original = { count: 0 }
287
+ const reactive = reactive(original)
288
+ const unwrapped = unwrap(reactive)
289
+
290
+ console.log(unwrapped === original) // true
291
+ console.log(unwrapped === reactive) // false
292
+ ```
293
+
294
+ ### `isReactive()`
295
+
296
+ Checks if an object is a reactive proxy.
297
+
298
+ ```typescript
299
+ function isReactive(obj: any): boolean
300
+ ```
301
+
302
+ ### `isNonReactive()`
303
+
304
+ Checks if an object is marked as non-reactive.
305
+
306
+ ```typescript
307
+ function isNonReactive(obj: any): boolean
308
+ ```
309
+
310
+ ## Effect System
311
+
312
+ ### Basic Effects
313
+
314
+ Effects are the core of the reactive system. They automatically track dependencies and re-run when those dependencies change.
315
+
316
+ ```typescript
317
+ const state = reactive({ count: 0, name: "John" })
318
+
319
+ effect(() => {
320
+ // This effect depends on state.count
321
+ console.log(`Count: ${state.count}`)
322
+ })
323
+
324
+ // Only changing count triggers the effect
325
+ state.count = 5 // Triggers effect
326
+ state.name = "Jane" // Does NOT trigger effect
327
+ ```
328
+
329
+ ### Effect Cleanup
330
+
331
+ Effects return cleanup functions that you can call to stop tracking dependencies.
332
+
333
+ ```typescript
334
+ const state = reactive({ count: 0 })
335
+
336
+ const stopEffect = effect(() => {
337
+ console.log(`Count: ${state.count}`)
338
+ })
339
+
340
+ // Later...
341
+ stopEffect() // Stops the effect
342
+
343
+ state.count = 10 // No longer triggers the effect
344
+ ```
345
+
346
+ ### Automatic Effect Cleanup
347
+
348
+ 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.
349
+
350
+ #### How Automatic Cleanup Works
351
+
352
+ 1. **Parent-Child Cleanup**: When an effect is created inside another effect, it becomes a "child" of the parent effect. When the parent effect is cleaned up, it also cleans up its child effects.
353
+
354
+ 2. **Garbage Collection Cleanup**: For top-level effects (not created inside other effects), the system uses JavaScript's garbage collection to automatically clean them up when their cleanup function no longer referenced.
355
+
356
+ #### Examples
357
+
358
+ **Parent-Child Cleanup:**
359
+ ```typescript
360
+ const state = reactive({ a: 1, b: 2 })
361
+
362
+ const stopParent = effect(() => {
363
+ state.a
364
+
365
+ // Child effect - tied to the parent lifecycle
366
+ effect(() => {
367
+ state.b
368
+ return () => console.log('Child cleanup')
369
+ })
370
+
371
+ return () => console.log('Parent cleanup')
372
+ })
373
+
374
+ // Cleans up both parent and child
375
+ stopParent() // Logs (order may vary): "Child cleanup", then "Parent cleanup"
376
+ ```
377
+
378
+ **Garbage Collection Cleanup:**
379
+ ```typescript
380
+ const state = reactive({ value: 1 })
381
+
382
+ // Top-level effect - automatically cleaned up via garbage collection
383
+ effect(() => {
384
+ state.value
385
+ return () => console.log('GC cleanup')
386
+ })
387
+
388
+ // No explicit cleanup needed - will be cleaned up when garbage collected
389
+ ```
390
+
391
+ **When You Should Store Cleanup Functions:**
392
+
393
+ While cleanup can be automatic via GC, you should **store and remember** cleanup functions both to prevent the effect from being garbage-collected (keeping it alive) and to perform immediate cleanup when needed. If you don't hold a reference to the cleanup (or to the effect), the effect can be collected and its cleanup called automatically by GC; storing a reference keeps it active under your control:
394
+
395
+ ```typescript
396
+ const state = reactive({ value: 1 })
397
+ const activeEffects: (() => void)[] = []
398
+
399
+ // Store cleanup functions for effects with side effects
400
+ for (let i = 0; i < 3; i++) {
401
+ const stopEffect = effect(() => {
402
+ state.value
403
+
404
+ // Side effect that needs immediate cleanup
405
+ const intervalId = setInterval(() => {
406
+ console.log(`Timer ${i} running`)
407
+ }, 1000)
408
+
409
+ return () => {
410
+ clearInterval(intervalId) // Prevent memory leaks
411
+ }
412
+ })
413
+
414
+ activeEffects.push(stopEffect)
415
+ }
416
+
417
+ // Clean up all effects when needed
418
+ activeEffects.forEach(stop => stop())
419
+ ```
420
+
421
+ **Key Points:**
422
+
423
+ - **You do not have to call cleanup** - GC may clean up effects when no references remain
424
+ - **You may want to call cleanup** - especially for effects with side effects
425
+ - **Store cleanup references to keep effects alive** - holding a reference prevents GC cleanup and gives you explicit stop control
426
+ - **Parent cleanup cleans child effects** - stopping a parent also stops its child effects
427
+ - **Child effects are referenced by parent effects** - and therefore are not subject to GC cleanups
428
+
429
+ ### Effect Dependencies
430
+
431
+ Effects automatically track which reactive properties they access.
432
+
433
+ ```typescript
434
+ const state = reactive({ a: 1, b: 2, c: 3 })
435
+
436
+ effect(() => {
437
+ // Only tracks state.a and state.b
438
+ console.log(`Sum: ${state.a + state.b}`)
439
+ })
440
+
441
+ // Only these changes trigger the effect
442
+ state.a = 5 // Triggers effect
443
+ state.b = 10 // Triggers effect
444
+ state.c = 15 // Does NOT trigger effect
445
+ ```
446
+
447
+ ### Async Effects and the `access` Parameter
448
+
449
+ The `effect` function provides a special `access` parameter with `tracked` and `ascend` functions that restore 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.
450
+
451
+ #### The Problem with Async Effects
452
+
453
+ 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.
454
+
455
+ The `access.tracked()` function restores the active effect context for dependency tracking.
456
+
457
+ #### Understanding the active effect context
458
+
459
+ The reactive system uses a global active effect variable to track which effect is currently running:
460
+
461
+ ```typescript
462
+ // Synchronous effect - active effect is maintained throughout
463
+ effect(() => {
464
+ // active effect = this effect
465
+ const value = state.count // ✅ Tracked (active effect is set)
466
+ // active effect = this effect (still set)
467
+ const another = state.name // ✅ Tracked (active effect is still set)
468
+ })
469
+
470
+ // Async effect - active effect is lost after await
471
+ effect(async () => {
472
+ // active effect = this effect
473
+ const value = state.count // ✅ Tracked (active effect is set)
474
+
475
+ await someAsyncOperation() // Function exits, active effect = undefined
476
+
477
+ // active effect = undefined (lost!)
478
+ const another = state.name // ❌ NOT tracked (no active effect)
479
+ })
480
+
481
+ // Async effect with access.tracked() - active effect is restored
482
+ effect(async ({ tracked }) => {
483
+ // active effect = this effect
484
+ const value = state.count // ✅ Tracked (active effect is set)
485
+
486
+ await someAsyncOperation() // Function exits, active effect = undefined
487
+
488
+ // tracked() temporarily restores active effect for the callback
489
+ const another = tracked(() => state.name) // ✅ Tracked (active effect restored)
490
+ })
491
+ ```
492
+
493
+ #### Key Benefits of `access.tracked()` in Async Effects
494
+
495
+ 1. **Restored Context**: `access.tracked()` temporarily restores the active effect context for dependency tracking
496
+ 2. **Consistent Tracking**: Reactive property access works the same way before and after `await`
497
+
498
+ ### Using `ascend` for Parent Effect Tracking
499
+
500
+ The `ascend` function allows you to track dependencies in the parent effect instead of the current effect. This is useful when you want child effects to be cleaned up with their parent, but their dependencies should trigger the parent effect.
501
+
502
+ ```typescript
503
+ const inputs = reactive([1, 2, 3])
504
+
505
+ effect(({ ascend }) => {
506
+ // Track inputs.length in the parent effect
507
+ const length = inputs.length
508
+
509
+ if (length > 0) {
510
+ // Use ascend to create effects that track dependencies in the parent
511
+ // When parent is cleaned up, these child effects are also cleaned up
512
+ ascend(() => {
513
+ // Dependencies here are tracked in the parent effect
514
+ inputs.forEach((item, index) => {
515
+ console.log(`Item ${index}:`, item)
516
+ })
517
+ })
518
+ }
519
+ })
520
+ ```
521
+ <|tool▁calls▁begin|><|tool▁call▁begin|>
522
+ grep
523
+
524
+ **When to use `ascend`:**
525
+ - When creating child effects that should be cleaned up with their parent
526
+ - When child effect dependencies should trigger the parent effect
527
+ - For managing effect hierarchies where child effects depend on parent context
528
+
529
+ ### Nested Effects
530
+
531
+ Effects can be created inside other effects and will have separate effect scopes:
532
+
533
+ ```typescript
534
+ import { effect, reactive } from 'mutts/reactive'
535
+
536
+ const state = reactive({ a: 0, b: 0 })
537
+
538
+ const stopOuter = effect(() => {
539
+ state.a
540
+
541
+ // Create an inner effect with its own scope
542
+ const stopInner = effect(() => {
543
+ state.b
544
+ })
545
+
546
+ // Return cleanup function for the inner effect
547
+ return stopInner
548
+ })
549
+ ```
550
+
551
+ ### `untracked()`
552
+
553
+ 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.
554
+
555
+ ```typescript
556
+ import { effect, untracked, reactive } from 'mutts/reactive'
557
+
558
+ const state = reactive({ a: 0, b: 0 })
559
+
560
+ effect(() => {
561
+ state.a
562
+
563
+ // Create an inner effect without tracking the creation under the outer effect
564
+ let stopInner: (() => void) | undefined
565
+ untracked(() => {
566
+ stopInner = effect(() => {
567
+ state.b
568
+ })
569
+ })
570
+
571
+ // Optionally stop it immediately to avoid accumulating watchers
572
+ stopInner && stopInner()
573
+ })
574
+ ```
575
+
576
+ **Use cases for `untracked()`:**
577
+ - Creating effects inside other effects without coupling their dependencies
578
+ - Performing side effects that shouldn't trigger when dependencies change
579
+ - Avoiding circular dependencies in complex reactive systems
580
+
581
+ ### Effect Options and debugging
582
+
583
+ Configure the reactive system behavior:
584
+
585
+ ```typescript
586
+ import { options as reactiveOptions } from 'mutts/reactive'
587
+
588
+ // Set maximum effect chain depth
589
+ reactiveOptions.maxEffectChain = 50
590
+
591
+ // Set maximum deep watch traversal depth
592
+ reactiveOptions.maxDeepWatchDepth = 200
593
+
594
+ // Enable/disable recursive touching (default: true)
595
+ reactiveOptions.recursiveTouching = false
596
+
597
+ // Enable debug logging
598
+ reactiveOptions.enter = (effect) => console.log('Entering effect:', effect)
599
+ reactiveOptions.leave = (effect) => console.log('Leaving effect:', effect)
600
+ reactiveOptions.chain = (caller, target) => console.log('Chaining:', caller, '->', target)
601
+ ```
602
+
603
+
604
+ ### `@reactive` Decorator
605
+
606
+ The `@reactive` decorator makes class instances automatically reactive. This is the recommended approach for adding reactivity to classes.
607
+
608
+ ```typescript
609
+ import { reactive } from 'mutts/reactive'
610
+
611
+ @reactive
612
+ class User {
613
+ name: string
614
+ age: number
615
+
616
+ constructor(name: string, age: number) {
617
+ this.name = name
618
+ this.age = age
619
+ }
620
+
621
+ updateAge(newAge: number) {
622
+ this.age = newAge
623
+ }
624
+ }
625
+
626
+ const user = new User("John", 30)
627
+
628
+ effect(() => {
629
+ console.log(`User: ${user.name}, Age: ${user.age}`)
630
+ })
631
+
632
+ user.updateAge(31) // Triggers effect
633
+ user.name = "Jane" // Triggers effect
634
+ ```
635
+
636
+ ### Functional Syntax
637
+
638
+ You can also use the functional syntax for making classes reactive:
639
+
640
+ ```typescript
641
+ import { reactive } from 'mutts/reactive'
642
+
643
+ class User {
644
+ name: string
645
+ age: number
646
+
647
+ constructor(name: string, age: number) {
648
+ this.name = name
649
+ this.age = age
650
+ }
651
+
652
+ updateAge(newAge: number) {
653
+ this.age = newAge
654
+ }
655
+ }
656
+
657
+ const ReactiveUser = reactive(User)
658
+ const user = new ReactiveUser("John", 30)
659
+
660
+ effect(() => {
661
+ console.log(`User: ${user.name}, Age: ${user.age}`)
662
+ })
663
+
664
+ user.updateAge(31) // Triggers effect
665
+ user.name = "Jane" // Triggers effect
666
+ ```
667
+
668
+ ### `ReactiveBase` for Complex Inheritance
669
+
670
+ For complex inheritance trees, especially when you need to solve constructor reactivity issues, extend `ReactiveBase`:
671
+
672
+ ```typescript
673
+ import { ReactiveBase, reactive } from 'mutts/reactive'
674
+
675
+ class GameObject extends ReactiveBase {
676
+ id = 'game-object'
677
+ position = { x: 0, y: 0 }
678
+ }
679
+
680
+ class Entity extends GameObject {
681
+ health = 100
682
+ }
683
+
684
+ @reactive
685
+ class Player extends Entity {
686
+ name = 'Player'
687
+ level = 1
688
+ }
689
+
690
+ const player = new Player()
691
+
692
+ effect(() => {
693
+ console.log(`Player ${player.name} at (${player.position.x}, ${player.position.y})`)
694
+ })
695
+
696
+ player.position.x = 10 // Triggers effect
697
+ player.health = 80 // Triggers effect
698
+ ```
699
+
700
+ **Advantages of `ReactiveBase`:**
701
+
702
+ 1. **Constructor Reactivity**: Solves the issue where `this` in the constructor is not yet reactive
703
+ 2. **Inheritance Safety**: Prevents reactivity from being added to prototype chains in complex inheritance trees
704
+ 3. **No Side Effects**: The base class itself has no effect - it only enables proper reactivity when combined with `@reactive`
705
+
706
+ ### Choosing the Right Approach
707
+
708
+ **Use `@reactive` decorator when:**
709
+ - You have simple classes without complex inheritance
710
+ - You want the cleanest, most modern syntax
711
+ - You don't need to modify or use `this` in the constructor
712
+
713
+ **Use `ReactiveBase` + `@reactive` when:**
714
+ - You have complex inheritance trees (like game objects, UI components)
715
+ - You need to modify or use `this` in the constructor
716
+ - You want to prevent reactivity from being added to prototype chains
717
+
718
+ ### Making Existing Class Instances Reactive
719
+
720
+ You can also make existing class instances reactive:
721
+
722
+ ```typescript
723
+ class Counter {
724
+ count = 0
725
+
726
+ increment() {
727
+ this.count++
728
+ }
729
+ }
730
+
731
+ const counter = new Counter()
732
+ const reactiveCounter = reactive(counter)
733
+
734
+ effect(() => {
735
+ console.log('Count:', reactiveCounter.count)
736
+ })
737
+
738
+ reactiveCounter.increment() // Triggers effect
739
+ ```
740
+
741
+ ### Method Reactivity
742
+
743
+ Methods that modify properties automatically trigger effects:
744
+
745
+ ```typescript
746
+ @reactive
747
+ class ShoppingCart {
748
+ items: string[] = []
749
+
750
+ addItem(item: string) {
751
+ this.items.push(item)
752
+ }
753
+
754
+ removeItem(item: string) {
755
+ const index = this.items.indexOf(item)
756
+ if (index > -1) {
757
+ this.items.splice(index, 1)
758
+ }
759
+ }
760
+ }
761
+
762
+ const cart = new ShoppingCart()
763
+
764
+ effect(() => {
765
+ console.log('Cart items:', cart.items)
766
+ })
767
+
768
+ cart.addItem('Apple') // Triggers effect
769
+ cart.removeItem('Apple') // Triggers effect
770
+ ```
771
+
772
+ ### Inheritance Support
773
+
774
+ The `@reactive` decorator works with inheritance:
775
+
776
+ ```typescript
777
+ @reactive
778
+ class Animal {
779
+ species: string
780
+
781
+ constructor(species: string) {
782
+ this.species = species
783
+ }
784
+ }
785
+
786
+ class Dog extends Animal {
787
+ breed: string
788
+
789
+ constructor(breed: string) {
790
+ super('Canis')
791
+ this.breed = breed
792
+ }
793
+ }
794
+
795
+ const dog = new Dog('Golden Retriever')
796
+
797
+ effect(() => {
798
+ console.log(`${dog.species}: ${dog.breed}`)
799
+ })
800
+
801
+ dog.breed = 'Labrador' // Triggers effect
802
+ ```
803
+
804
+ **Note**: When using inheritance with the `@reactive` decorator, apply it to the base class. The decorator will automatically handle inheritance properly.
805
+
806
+ ## Non-Reactive System
807
+
808
+ ### `unreactive()`
809
+
810
+ Marks objects or classes as non-reactive, preventing them from being wrapped.
811
+
812
+ ```typescript
813
+ function unreactive<T>(target: T): T
814
+ function unreactive(target: Constructor<T>): Constructor<T>
815
+ ```
816
+
817
+ **Examples:**
818
+
819
+ ```typescript
820
+ // Mark individual object as non-reactive
821
+ const obj = { count: 0 }
822
+ unreactive(obj)
823
+ const reactiveObj = reactive(obj) // Returns obj unchanged
824
+
825
+ // Mark entire class as non-reactive
826
+ class Utility {
827
+ static helper() { return 'help' }
828
+ }
829
+ unreactive(Utility)
830
+ const instance = new Utility()
831
+ const reactiveInstance = reactive(instance) // Returns instance unchanged
832
+ ```
833
+
834
+ ### `@unreactive` Decorator
835
+
836
+ Mark class properties as non-reactive using class-level syntax.
837
+
838
+ ```typescript
839
+ @reactive
840
+ @unreactive('id')
841
+ class User {
842
+ id: string = 'user-123'
843
+
844
+ name: string = 'John'
845
+ age: number = 30
846
+ }
847
+
848
+ const user = new User()
849
+
850
+ effect(() => {
851
+ console.log(user.name, user.age) // Tracks these
852
+ console.log(user.id) // Does NOT track this
853
+ })
854
+
855
+ user.name = 'Jane' // Triggers effect
856
+ user.id = 'new-id' // Does NOT trigger effect
857
+ ```
858
+
859
+ You can mark multiple properties as unreactive:
860
+
861
+ ```typescript
862
+ @reactive
863
+ @unreactive('id', 'metadata', 'internalState')
864
+ class User {
865
+ id: string = 'user-123'
866
+ metadata: any = {}
867
+ internalState: any = {}
868
+
869
+ name: string = 'John'
870
+ age: number = 30
871
+ }
872
+ ```
873
+
874
+ ### Non-Reactive Classes
875
+
876
+ Classes marked as non-reactive bypass the reactive system entirely:
877
+
878
+ ```typescript
879
+ class Config {
880
+ apiUrl: string = 'https://api.example.com'
881
+ timeout: number = 5000
882
+ }
883
+
884
+ unreactive(Config)
885
+ ```
886
+
887
+ Alternatively, you can use the `@unreactive` decorator without arguments to mark the entire class as non-reactive:
888
+
889
+ ```typescript
890
+ @unreactive
891
+ class Config {
892
+ apiUrl: string = 'https://api.example.com'
893
+ timeout: number = 5000
894
+ }
895
+
896
+ const ReactiveConfig = reactive(Config)
897
+ const config = new ReactiveConfig()
898
+
899
+ effect(() => {
900
+ console.log('Config:', config.apiUrl, config.timeout)
901
+ })
902
+
903
+ // These changes won't trigger effects
904
+ config.apiUrl = 'https://new-api.example.com'
905
+ config.timeout = 10000
906
+ ```
907
+
908
+ ### Making Special Reactive Objects Non-Reactive
909
+
910
+ Special reactive objects (Arrays, Maps, Sets, WeakMaps, WeakSets) can also be made non-reactive:
911
+
912
+ ```typescript
913
+ // Make individual reactive collections non-reactive
914
+ const array = reactive([1, 2, 3])
915
+ unreactive(array) // array is no longer reactive
916
+
917
+ const map = reactive(new Map([['key', 'value']]))
918
+ unreactive(map) // map is no longer reactive
919
+
920
+ const set = reactive(new Set([1, 2, 3]))
921
+ unreactive(set) // set is no longer reactive
922
+ ```
923
+
924
+ **Making reactive collection classes non-reactive:**
925
+
926
+ ```typescript
927
+ // Make the entire ReactiveArray class non-reactive
928
+ unreactive(ReactiveArray)
929
+
930
+ // Now all ReactiveArray instances will be non-reactive
931
+ const array = reactive([1, 2, 3]) // Returns non-reactive array
932
+ const reactiveArray = new ReactiveArray([1, 2, 3]) // Non-reactive instance
933
+
934
+ // Make other reactive collection classes non-reactive
935
+ unreactive(ReactiveMap)
936
+ unreactive(ReactiveSet)
937
+ unreactive(ReactiveWeakMap)
938
+ unreactive(ReactiveWeakSet)
939
+ ```
940
+
941
+ **Use cases for non-reactive collections:**
942
+ - Large datasets that don't need reactivity
943
+ - Performance-critical operations
944
+ - Static configuration data
945
+ - Temporary data structures
946
+
947
+ ### Performance Considerations
948
+
949
+ Non-reactive objects can improve performance:
950
+
951
+ ```typescript
952
+ // Good: Mark large, rarely-changing objects as non-reactive
953
+ const config = unreactive({
954
+ apiEndpoints: { /* large config object */ },
955
+ featureFlags: { /* many flags */ }
956
+ })
957
+
958
+ // Good: Mark utility classes as non-reactive
959
+ class MathUtils {
960
+ static PI = 3.14159
961
+ static square(x: number) { return x * x }
962
+ }
963
+ unreactive(MathUtils)
964
+
965
+ // Good: Mark properties that don't need reactivity
966
+ class User {
967
+ @unreactive
968
+ metadata: any = {} // Large metadata object
969
+
970
+ name: string = 'John' // This should be reactive
971
+ }
972
+ ```
973
+