mutts 1.0.0 → 1.0.2

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 (85) hide show
  1. package/README.md +24 -2
  2. package/dist/chunks/_tslib-C-cuVLvZ.js +73 -0
  3. package/dist/chunks/_tslib-C-cuVLvZ.js.map +1 -0
  4. package/dist/chunks/_tslib-CMEnd0VE.esm.js +68 -0
  5. package/dist/chunks/_tslib-CMEnd0VE.esm.js.map +1 -0
  6. package/dist/chunks/{decorator-BXsign4Z.js → decorator-D4DU97Zg.js} +70 -4
  7. package/dist/chunks/decorator-D4DU97Zg.js.map +1 -0
  8. package/dist/chunks/{decorator-CPbZNnsX.esm.js → decorator-GnHw1Az7.esm.js} +67 -5
  9. package/dist/chunks/decorator-GnHw1Az7.esm.js.map +1 -0
  10. package/dist/chunks/index-DBScoeCX.esm.js +1960 -0
  11. package/dist/chunks/index-DBScoeCX.esm.js.map +1 -0
  12. package/dist/chunks/index-DOTmXL89.js +1983 -0
  13. package/dist/chunks/index-DOTmXL89.js.map +1 -0
  14. package/dist/decorator.d.ts +58 -1
  15. package/dist/decorator.esm.js +1 -1
  16. package/dist/decorator.js +1 -1
  17. package/dist/destroyable.d.ts +42 -0
  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/eventful.d.ts +10 -1
  23. package/dist/eventful.esm.js +5 -27
  24. package/dist/eventful.esm.js.map +1 -1
  25. package/dist/eventful.js +15 -37
  26. package/dist/eventful.js.map +1 -1
  27. package/dist/index.d.ts +52 -3
  28. package/dist/index.esm.js +3 -2
  29. package/dist/index.esm.js.map +1 -1
  30. package/dist/index.js +18 -3
  31. package/dist/index.js.map +1 -1
  32. package/dist/indexable.d.ts +26 -0
  33. package/dist/indexable.esm.js +6 -0
  34. package/dist/indexable.esm.js.map +1 -1
  35. package/dist/indexable.js +6 -0
  36. package/dist/indexable.js.map +1 -1
  37. package/dist/mutts.umd.js +1 -1
  38. package/dist/mutts.umd.js.map +1 -1
  39. package/dist/mutts.umd.min.js +1 -1
  40. package/dist/mutts.umd.min.js.map +1 -1
  41. package/dist/promiseChain.d.ts +10 -0
  42. package/dist/promiseChain.esm.js +6 -0
  43. package/dist/promiseChain.esm.js.map +1 -1
  44. package/dist/promiseChain.js +6 -0
  45. package/dist/promiseChain.js.map +1 -1
  46. package/dist/reactive.d.ts +258 -20
  47. package/dist/reactive.esm.js +4 -1454
  48. package/dist/reactive.esm.js.map +1 -1
  49. package/dist/reactive.js +29 -1466
  50. package/dist/reactive.js.map +1 -1
  51. package/dist/std-decorators.d.ts +35 -0
  52. package/dist/std-decorators.esm.js +36 -1
  53. package/dist/std-decorators.esm.js.map +1 -1
  54. package/dist/std-decorators.js +36 -1
  55. package/dist/std-decorators.js.map +1 -1
  56. package/docs/mixin.md +229 -0
  57. package/docs/reactive.md +7931 -458
  58. package/package.json +1 -2
  59. package/dist/chunks/decorator-BXsign4Z.js.map +0 -1
  60. package/dist/chunks/decorator-CPbZNnsX.esm.js.map +0 -1
  61. package/src/decorator.test.ts +0 -495
  62. package/src/decorator.ts +0 -205
  63. package/src/destroyable.test.ts +0 -155
  64. package/src/destroyable.ts +0 -158
  65. package/src/eventful.test.ts +0 -380
  66. package/src/eventful.ts +0 -69
  67. package/src/index.ts +0 -7
  68. package/src/indexable.test.ts +0 -388
  69. package/src/indexable.ts +0 -124
  70. package/src/promiseChain.test.ts +0 -201
  71. package/src/promiseChain.ts +0 -99
  72. package/src/reactive/array.test.ts +0 -923
  73. package/src/reactive/array.ts +0 -352
  74. package/src/reactive/core.test.ts +0 -1663
  75. package/src/reactive/core.ts +0 -866
  76. package/src/reactive/index.ts +0 -28
  77. package/src/reactive/interface.test.ts +0 -1477
  78. package/src/reactive/interface.ts +0 -231
  79. package/src/reactive/map.test.ts +0 -866
  80. package/src/reactive/map.ts +0 -162
  81. package/src/reactive/set.test.ts +0 -289
  82. package/src/reactive/set.ts +0 -142
  83. package/src/std-decorators.test.ts +0 -679
  84. package/src/std-decorators.ts +0 -182
  85. package/src/utils.ts +0 -52
@@ -1,1663 +0,0 @@
1
- import {
2
- effect,
3
- isNonReactive,
4
- isReactive,
5
- ReactiveBase,
6
- reactive,
7
- reactiveOptions,
8
- unreactive,
9
- untracked,
10
- unwrap,
11
- } from './index'
12
-
13
- describe('reactive', () => {
14
- describe('basic functionality', () => {
15
- it('should make objects reactive', () => {
16
- const obj = { count: 0, name: 'test' }
17
- const reactiveObj = reactive(obj)
18
-
19
- expect(isReactive(reactiveObj)).toBe(true)
20
- expect(isReactive(obj)).toBe(false)
21
- expect(reactiveObj.count).toBe(0)
22
- expect(reactiveObj.name).toBe('test')
23
- })
24
-
25
- it('should not make primitives reactive', () => {
26
- expect(reactive(42)).toBe(42)
27
- expect(reactive('string')).toBe('string')
28
- expect(reactive(true)).toBe(true)
29
- expect(reactive(null)).toBe(null)
30
- expect(reactive(undefined)).toBe(undefined)
31
- expect(reactive(true)).toBe(true)
32
- expect(reactive(null)).toBe(null)
33
- expect(reactive(undefined)).toBe(undefined)
34
- })
35
-
36
- it('should return same proxy for same object', () => {
37
- const obj = { count: 0 }
38
- const proxy1 = reactive(obj)
39
- const proxy2 = reactive(obj)
40
-
41
- expect(proxy1).toBe(proxy2)
42
- })
43
-
44
- it('should return same proxy when called on proxy', () => {
45
- const obj = { count: 0 }
46
- const proxy1 = reactive(obj)
47
- const proxy2 = reactive(proxy1)
48
-
49
- expect(proxy1).toBe(proxy2)
50
- })
51
- })
52
-
53
- describe('property access and modification', () => {
54
- it('should allow reading properties', () => {
55
- const obj = { count: 0, name: 'test' }
56
- const reactiveObj = reactive(obj)
57
-
58
- expect(reactiveObj.count).toBe(0)
59
- expect(reactiveObj.name).toBe('test')
60
- })
61
-
62
- it('should allow setting properties', () => {
63
- const obj = { count: 0 }
64
- const reactiveObj = reactive(obj)
65
-
66
- reactiveObj.count = 5
67
- expect(reactiveObj.count).toBe(5)
68
- expect(obj.count).toBe(5)
69
- })
70
-
71
- it('should handle numeric properties', () => {
72
- const obj = { 0: 'zero', 1: 'one' }
73
- const reactiveObj = reactive(obj)
74
-
75
- expect(reactiveObj[0]).toBe('zero')
76
- expect(reactiveObj[1]).toBe('one')
77
-
78
- reactiveObj[0] = 'ZERO'
79
- expect(reactiveObj[0]).toBe('ZERO')
80
- })
81
-
82
- it('should handle symbol properties', () => {
83
- const sym = Symbol('test')
84
- const obj = { [sym]: 'value' }
85
- const reactiveObj = reactive(obj)
86
-
87
- expect(reactiveObj[sym]).toBe('value')
88
-
89
- reactiveObj[sym] = 'new value'
90
- expect(reactiveObj[sym]).toBe('new value')
91
- })
92
- })
93
-
94
- describe('unwrap functionality', () => {
95
- it('should unwrap reactive objects', () => {
96
- const obj = { count: 0 }
97
- const reactiveObj = reactive(obj)
98
-
99
- const unwrapped = unwrap(reactiveObj)
100
- expect(unwrapped).toBe(obj)
101
- expect(unwrapped).not.toBe(reactiveObj)
102
- })
103
-
104
- it('should return non-reactive objects as-is', () => {
105
- const obj = { count: 0 }
106
- expect(unwrap(obj)).toBe(obj)
107
- })
108
- })
109
- })
110
-
111
- describe('effect', () => {
112
- describe('basic effect functionality', () => {
113
- it('should run effect immediately', () => {
114
- let count = 0
115
- const reactiveObj = reactive({ value: 0 })
116
-
117
- effect(() => {
118
- count++
119
- reactiveObj.value
120
- })
121
-
122
- expect(count).toBe(1)
123
- })
124
-
125
- it('should track dependencies', () => {
126
- let effectCount = 0
127
- const reactiveObj = reactive({ count: 0 })
128
-
129
- effect(() => {
130
- effectCount++
131
- reactiveObj.count
132
- })
133
-
134
- expect(effectCount).toBe(1)
135
-
136
- reactiveObj.count = 5
137
- expect(effectCount).toBe(2)
138
- })
139
-
140
- it('should only track accessed properties', () => {
141
- let effectCount = 0
142
- const reactiveObj = reactive({ count: 0, name: 'test' })
143
-
144
- effect(() => {
145
- effectCount++
146
- reactiveObj.count // Only access count
147
- })
148
-
149
- expect(effectCount).toBe(1)
150
-
151
- reactiveObj.name = 'new name' // Change name
152
- expect(effectCount).toBe(1) // Should not trigger effect
153
-
154
- reactiveObj.count = 5 // Change count
155
- expect(effectCount).toBe(2) // Should trigger effect
156
- })
157
- })
158
-
159
- describe('cascading effects', () => {
160
- it('should properly handle cascading effects', () => {
161
- const reactiveObj = reactive({ a: 0, b: 0, c: 0 })
162
-
163
- effect(() => {
164
- reactiveObj.b = reactiveObj.a + 1
165
- })
166
- effect(() => {
167
- reactiveObj.c = reactiveObj.b + 1
168
- })
169
-
170
- expect(reactiveObj.a).toBe(0)
171
- expect(reactiveObj.b).toBe(1)
172
- expect(reactiveObj.c).toBe(2)
173
-
174
- reactiveObj.b = 5
175
- expect(reactiveObj.a).toBe(0)
176
- expect(reactiveObj.b).toBe(5)
177
- expect(reactiveObj.c).toBe(6)
178
-
179
- reactiveObj.a = 3
180
- expect(reactiveObj.a).toBe(3)
181
- expect(reactiveObj.b).toBe(4)
182
- expect(reactiveObj.c).toBe(5)
183
- })
184
-
185
- it('should allow re-entrant effects (create inner effect inside outer via untracked)', () => {
186
- const state = reactive({ a: 0, b: 0 })
187
- let outerRuns = 0
188
- let innerRuns = 0
189
-
190
- const stopOuter = effect(() => {
191
- outerRuns++
192
- state.a
193
- // Create/refresh inner effect each time outer runs (re-entrancy)
194
- // Use untracked to avoid nested-effect guard and dependency coupling
195
- let stopInner: (() => void) | undefined
196
- untracked(() => {
197
- stopInner = effect(() => {
198
- innerRuns++
199
- state.b
200
- })
201
- })
202
- // Immediately stop to avoid accumulating watchers
203
- stopInner?.()
204
- })
205
-
206
- expect(outerRuns).toBe(1)
207
- expect(innerRuns).toBe(1)
208
-
209
- state.a = 1
210
- expect(outerRuns).toBe(2)
211
- // inner created again due to re-entrancy
212
- expect(innerRuns).toBe(2)
213
-
214
- state.b = 1
215
- // inner was stopped in the same tick; no rerun expected
216
- expect(innerRuns).toBe(2)
217
-
218
- stopOuter()
219
- })
220
- })
221
-
222
- describe('effect cleanup', () => {
223
- it('should return unwatch function', () => {
224
- const reactiveObj = reactive({ count: 0 })
225
- let effectCount = 0
226
-
227
- const unwatch = effect(() => {
228
- effectCount++
229
- reactiveObj.count
230
- })
231
-
232
- expect(typeof unwatch).toBe('function')
233
- expect(effectCount).toBe(1)
234
- })
235
-
236
- it('should stop tracking when unwatched', () => {
237
- const reactiveObj = reactive({ count: 0 })
238
- let effectCount = 0
239
-
240
- const unwatch = effect(() => {
241
- effectCount++
242
- reactiveObj.count
243
- })
244
-
245
- expect(effectCount).toBe(1)
246
-
247
- unwatch()
248
-
249
- reactiveObj.count = 5
250
- expect(effectCount).toBe(1) // Should not trigger effect
251
- })
252
-
253
- it('should clean up dependencies on re-run', () => {
254
- const reactiveObj = reactive({ count: 0, name: 'test' })
255
- let effectCount = 0
256
-
257
- effect(() => {
258
- effectCount++
259
- reactiveObj.count
260
- })
261
-
262
- expect(effectCount).toBe(1)
263
-
264
- // Change the effect to only watch name
265
- effect(() => {
266
- effectCount++
267
- reactiveObj.name
268
- })
269
-
270
- expect(effectCount).toBe(2)
271
-
272
- reactiveObj.count = 5
273
- expect(effectCount).toBe(3) // Should not trigger effect anymore
274
-
275
- reactiveObj.name = 'new name'
276
- expect(effectCount).toBe(4) // Should trigger effect
277
- })
278
- })
279
-
280
- describe('error handling', () => {
281
- it('should propagate errors from effects', () => {
282
- const reactiveObj = reactive({ count: 0 })
283
- let effectCount = 0
284
-
285
- effect(() => {
286
- effectCount++
287
- reactiveObj.count
288
-
289
- if (reactiveObj.count === 1) {
290
- throw new Error('Test error')
291
- }
292
- })
293
-
294
- expect(effectCount).toBe(1)
295
-
296
- // This should throw an error when the effect runs
297
- expect(() => {
298
- reactiveObj.count = 1
299
- }).toThrow('Test error')
300
-
301
- expect(effectCount).toBe(2)
302
- })
303
- })
304
-
305
- describe('complex scenarios', () => {
306
- it('should handle multiple reactive objects', () => {
307
- const obj1 = reactive({ count: 0 })
308
- const obj2 = reactive({ name: 'test' })
309
- let effectCount = 0
310
-
311
- effect(() => {
312
- effectCount++
313
- obj1.count
314
- obj2.name
315
- })
316
-
317
- expect(effectCount).toBe(1)
318
-
319
- obj1.count = 5
320
- expect(effectCount).toBe(2)
321
-
322
- obj2.name = 'new name'
323
- expect(effectCount).toBe(3)
324
- })
325
-
326
- it('should handle object identity changes', () => {
327
- const reactiveObj = reactive({ inner: { count: 0 } })
328
- let effectCount = 0
329
-
330
- effect(() => {
331
- effectCount++
332
- reactiveObj.inner.count
333
- })
334
-
335
- expect(effectCount).toBe(1)
336
-
337
- reactiveObj.inner = { count: 5 }
338
- expect(effectCount).toBe(2)
339
-
340
- reactiveObj.inner.count = 10
341
- expect(effectCount).toBe(3)
342
- })
343
-
344
- it('should manage modifications during effect execution and should not trigger effect', () => {
345
- const state = reactive({ count: 0, multiplier: 2 })
346
- let effectCalls = 0
347
-
348
- const stopEffect = effect(() => {
349
- effectCalls++
350
- // Change watched value during effect
351
- state.count = state.count + 1
352
- })
353
-
354
- expect(effectCalls).toBe(1)
355
-
356
- stopEffect()
357
- })
358
- })
359
- })
360
-
361
- describe('integration tests', () => {
362
- it('should work with complex nested structures', () => {
363
- const state = reactive({
364
- user: {
365
- profile: {
366
- name: 'John',
367
- age: 30,
368
- },
369
- settings: {
370
- theme: 'dark',
371
- notifications: true,
372
- },
373
- },
374
- app: {
375
- version: '1.0.0',
376
- features: ['auth', 'chat'],
377
- },
378
- })
379
-
380
- let profileEffectCount = 0
381
- let settingsEffectCount = 0
382
- let appEffectCount = 0
383
-
384
- effect(() => {
385
- profileEffectCount++
386
- state.user.profile.name
387
- state.user.profile.age
388
- })
389
-
390
- effect(() => {
391
- settingsEffectCount++
392
- state.user.settings.theme
393
- })
394
-
395
- effect(() => {
396
- appEffectCount++
397
- state.app.version
398
- })
399
-
400
- expect(profileEffectCount).toBe(1)
401
- expect(settingsEffectCount).toBe(1)
402
- expect(appEffectCount).toBe(1)
403
-
404
- // Change profile
405
- state.user.profile.name = 'Jane'
406
- expect(profileEffectCount).toBe(2)
407
- expect(settingsEffectCount).toBe(1)
408
- expect(appEffectCount).toBe(1)
409
-
410
- // Change settings
411
- state.user.settings.theme = 'light'
412
- expect(profileEffectCount).toBe(2)
413
- expect(settingsEffectCount).toBe(2)
414
- expect(appEffectCount).toBe(1)
415
-
416
- // Change app
417
- state.app.version = '1.1.0'
418
- expect(profileEffectCount).toBe(2)
419
- expect(settingsEffectCount).toBe(2)
420
- expect(appEffectCount).toBe(2)
421
- })
422
- })
423
-
424
- describe('@reactive decorator', () => {
425
- it('should make class instances reactive using decorator', () => {
426
- @reactive
427
- class TestClass {
428
- count = 0
429
- name = 'test'
430
-
431
- increment() {
432
- this.count++
433
- }
434
-
435
- setName(newName: string) {
436
- this.name = newName
437
- }
438
- }
439
-
440
- const instance = new TestClass()
441
-
442
- let effectCount = 0
443
- effect(() => {
444
- effectCount++
445
- instance.count
446
- })
447
-
448
- expect(effectCount).toBe(1)
449
- expect(instance.count).toBe(0)
450
-
451
- instance.increment()
452
- expect(effectCount).toBe(2)
453
- expect(instance.count).toBe(1)
454
- })
455
-
456
- it('should track property changes on reactive class instances', () => {
457
- @reactive
458
- class User {
459
- name = 'John'
460
- age = 30
461
-
462
- updateProfile(newName: string, newAge: number) {
463
- this.name = newName
464
- this.age = newAge
465
- }
466
- }
467
-
468
- const user = new User()
469
-
470
- let nameEffectCount = 0
471
- let ageEffectCount = 0
472
-
473
- effect(() => {
474
- nameEffectCount++
475
- user.name
476
- })
477
-
478
- effect(() => {
479
- ageEffectCount++
480
- user.age
481
- })
482
-
483
- expect(nameEffectCount).toBe(1)
484
- expect(ageEffectCount).toBe(1)
485
-
486
- user.updateProfile('Jane', 25)
487
- expect(nameEffectCount).toBe(2)
488
- expect(ageEffectCount).toBe(2)
489
- expect(user.name).toBe('Jane')
490
- expect(user.age).toBe(25)
491
- })
492
-
493
- it('should work with inheritance', () => {
494
- // Suppress the expected warning for this test
495
- const originalWarn = reactiveOptions.warn
496
- reactiveOptions.warn = () => {}
497
- try {
498
- @reactive
499
- class Animal {
500
- species = 'unknown'
501
- energy = 100
502
- }
503
-
504
- class Dog extends Animal {
505
- breed = 'mixed'
506
- bark() {
507
- this.energy -= 10
508
- }
509
- }
510
-
511
- const dog = new Dog()
512
-
513
- let energyEffectCount = 0
514
- effect(() => {
515
- energyEffectCount++
516
- dog.energy
517
- })
518
-
519
- expect(energyEffectCount).toBe(1)
520
- expect(dog.energy).toBe(100)
521
-
522
- dog.bark()
523
- expect(energyEffectCount).toBe(2)
524
- expect(dog.energy).toBe(90)
525
- } finally {
526
- // Restore original warn function
527
- reactiveOptions.warn = originalWarn
528
- }
529
- })
530
-
531
- it('should handle method calls that modify properties', () => {
532
- @reactive
533
- class Counter {
534
- value = 0
535
-
536
- add(amount: number) {
537
- this.value += amount
538
- }
539
-
540
- reset() {
541
- this.value = 0
542
- }
543
- }
544
-
545
- const counter = new Counter()
546
-
547
- let effectCount = 0
548
- effect(() => {
549
- effectCount++
550
- counter.value
551
- })
552
-
553
- expect(effectCount).toBe(1)
554
- expect(counter.value).toBe(0)
555
-
556
- counter.add(5)
557
- expect(effectCount).toBe(2)
558
- expect(counter.value).toBe(5)
559
-
560
- counter.reset()
561
- expect(effectCount).toBe(3)
562
- expect(counter.value).toBe(0)
563
- })
564
-
565
- it('should work with functional syntax', () => {
566
- class TestClass {
567
- count = 0
568
- name = 'test'
569
-
570
- increment() {
571
- this.count++
572
- }
573
- }
574
-
575
- const ReactiveTestClass = reactive(TestClass)
576
- const instance = new ReactiveTestClass()
577
-
578
- let effectCount = 0
579
- effect(() => {
580
- effectCount++
581
- instance.count
582
- })
583
-
584
- expect(effectCount).toBe(1)
585
- expect(instance.count).toBe(0)
586
-
587
- instance.increment()
588
- expect(effectCount).toBe(2)
589
- expect(instance.count).toBe(1)
590
- })
591
- })
592
-
593
- describe('ReactiveBase', () => {
594
- it('should make classes extending ReactiveBase reactive when decorated', () => {
595
- class BaseClass extends ReactiveBase {
596
- baseProp = 'base'
597
- }
598
-
599
- @reactive
600
- class DerivedClass extends BaseClass {
601
- derivedProp = 'derived'
602
- }
603
-
604
- const instance = new DerivedClass()
605
-
606
- let effectCount = 0
607
- effect(() => {
608
- effectCount++
609
- instance.baseProp
610
- instance.derivedProp
611
- })
612
-
613
- expect(effectCount).toBe(1)
614
- expect(instance.baseProp).toBe('base')
615
- expect(instance.derivedProp).toBe('derived')
616
-
617
- instance.baseProp = 'new base'
618
- expect(effectCount).toBe(2)
619
-
620
- instance.derivedProp = 'new derived'
621
- expect(effectCount).toBe(3)
622
- })
623
-
624
- it('should solve constructor reactivity issues', () => {
625
- class BaseClass extends ReactiveBase {
626
- value = 0
627
- }
628
-
629
- @reactive
630
- class TestClass extends BaseClass {
631
- constructor() {
632
- super()
633
- // In constructor, 'this' is not yet reactive
634
- // But ReactiveBase ensures the returned instance is reactive
635
- this.value = 42
636
- }
637
- }
638
-
639
- const instance = new TestClass()
640
-
641
- let effectCount = 0
642
- effect(() => {
643
- effectCount++
644
- instance.value
645
- })
646
-
647
- expect(effectCount).toBe(1)
648
- expect(instance.value).toBe(42)
649
-
650
- instance.value = 100
651
- expect(effectCount).toBe(2)
652
- expect(instance.value).toBe(100)
653
- })
654
-
655
- it('should work with complex inheritance trees', () => {
656
- class GameObject extends ReactiveBase {
657
- id = 'game-object'
658
- position = { x: 0, y: 0 }
659
- }
660
-
661
- class Entity extends GameObject {
662
- health = 100
663
- }
664
-
665
- @reactive
666
- class Player extends Entity {
667
- name = 'Player'
668
- level = 1
669
- }
670
-
671
- const player = new Player()
672
-
673
- let positionEffectCount = 0
674
- let healthEffectCount = 0
675
- let levelEffectCount = 0
676
-
677
- effect(() => {
678
- positionEffectCount++
679
- player.position.x
680
- player.position.y
681
- })
682
-
683
- effect(() => {
684
- healthEffectCount++
685
- player.health
686
- })
687
-
688
- effect(() => {
689
- levelEffectCount++
690
- player.level
691
- })
692
-
693
- expect(positionEffectCount).toBe(1)
694
- expect(healthEffectCount).toBe(1)
695
- expect(levelEffectCount).toBe(1)
696
-
697
- player.position.x = 10
698
- expect(positionEffectCount).toBe(2)
699
- expect(healthEffectCount).toBe(1)
700
- expect(levelEffectCount).toBe(1)
701
-
702
- player.health = 80
703
- expect(positionEffectCount).toBe(2)
704
- expect(healthEffectCount).toBe(2)
705
- expect(levelEffectCount).toBe(1)
706
-
707
- player.level = 2
708
- expect(positionEffectCount).toBe(2)
709
- expect(healthEffectCount).toBe(2)
710
- expect(levelEffectCount).toBe(2)
711
- })
712
-
713
- it('should not affect classes that do not extend ReactiveBase', () => {
714
- class RegularClass {
715
- value = 0
716
- }
717
-
718
- @reactive
719
- class TestClass extends RegularClass {
720
- otherValue = 1
721
- }
722
-
723
- const instance = new TestClass()
724
-
725
- let effectCount = 0
726
- effect(() => {
727
- effectCount++
728
- instance.value
729
- instance.otherValue
730
- })
731
-
732
- expect(effectCount).toBe(1)
733
-
734
- instance.value = 10
735
- expect(effectCount).toBe(2)
736
-
737
- instance.otherValue = 20
738
- expect(effectCount).toBe(3)
739
- })
740
- })
741
-
742
- describe('Legacy Reactive mixin', () => {
743
- it('should still work for backward compatibility', () => {
744
- class TestClass {
745
- count = 0
746
- name = 'test'
747
-
748
- increment() {
749
- this.count++
750
- }
751
-
752
- setName(newName: string) {
753
- this.name = newName
754
- }
755
- }
756
-
757
- const ReactiveTestClass = reactive(TestClass)
758
- const instance = new ReactiveTestClass()
759
-
760
- let effectCount = 0
761
- effect(() => {
762
- effectCount++
763
- instance.count
764
- })
765
-
766
- expect(effectCount).toBe(1)
767
- expect(instance.count).toBe(0)
768
-
769
- instance.increment()
770
- expect(effectCount).toBe(2)
771
- expect(instance.count).toBe(1)
772
- })
773
-
774
- it('should warn when used with inheritance', () => {
775
- const consoleSpy = jest.spyOn(console, 'warn').mockImplementation()
776
-
777
- class BaseClass {
778
- baseProp = 'base'
779
- }
780
-
781
- const ReactiveBaseClass = reactive(BaseClass)
782
-
783
- // Create a class that extends the reactive class
784
- class DerivedClass extends ReactiveBaseClass {
785
- derivedProp = 'derived'
786
- }
787
-
788
- const _instance = new DerivedClass()
789
-
790
- // The warning should be triggered when creating the instance
791
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('has been inherited by'))
792
-
793
- consoleSpy.mockRestore()
794
- })
795
- })
796
-
797
- describe('non-reactive functionality', () => {
798
- describe('markNonReactive', () => {
799
- it('should mark individual objects as non-reactive', () => {
800
- const obj = { count: 0, name: 'test' }
801
- unreactive(obj)
802
-
803
- expect(isNonReactive(obj)).toBe(true)
804
- expect(reactive(obj)).toBe(obj) // Should not create a proxy
805
- expect(isReactive(obj)).toBe(false)
806
- })
807
-
808
- it('should not affect other objects', () => {
809
- const obj1 = { count: 0 }
810
- const obj2 = { count: 0 }
811
-
812
- unreactive(obj1)
813
-
814
- expect(isNonReactive(obj1)).toBe(true)
815
- expect(isNonReactive(obj2)).toBe(false)
816
-
817
- const reactiveObj2 = reactive(obj2)
818
- expect(isReactive(reactiveObj2)).toBe(true)
819
- })
820
- })
821
-
822
- describe('markNonReactiveClass', () => {
823
- it('should mark entire classes as non-reactive', () => {
824
- class TestClass {
825
- count = 0
826
- name = 'test'
827
- }
828
-
829
- unreactive(TestClass)
830
-
831
- const instance1 = new TestClass()
832
- const instance2 = new TestClass()
833
-
834
- expect(isNonReactive(instance1)).toBe(true)
835
- expect(isNonReactive(instance2)).toBe(true)
836
- expect(reactive(instance1)).toBe(instance1)
837
- expect(reactive(instance2)).toBe(instance2)
838
- })
839
-
840
- it('should work with inheritance', () => {
841
- class BaseClass {
842
- baseProp = 'base'
843
- }
844
-
845
- class DerivedClass extends BaseClass {
846
- derivedProp = 'derived'
847
- }
848
-
849
- unreactive(BaseClass)
850
-
851
- const baseInstance = new BaseClass()
852
- const derivedInstance = new DerivedClass()
853
-
854
- expect(isNonReactive(baseInstance)).toBe(true)
855
- expect(isNonReactive(derivedInstance)).toBe(true) // Inherits non-reactive status
856
- })
857
-
858
- it('should not affect other classes', () => {
859
- class NonReactiveClass {
860
- prop = 'non-reactive'
861
- }
862
-
863
- class ReactiveClass {
864
- prop = 'reactive'
865
- }
866
-
867
- unreactive(NonReactiveClass)
868
-
869
- const unreactiveInstance = new NonReactiveClass()
870
- const reactiveInstance = new ReactiveClass()
871
-
872
- expect(isNonReactive(unreactiveInstance)).toBe(true)
873
- expect(isNonReactive(reactiveInstance)).toBe(false)
874
-
875
- const reactiveReactiveInstance = reactive(reactiveInstance)
876
- expect(isReactive(reactiveReactiveInstance)).toBe(true)
877
- })
878
- })
879
-
880
- describe('NonReactive symbol (internal)', () => {
881
- it('should mark objects with symbol as non-reactive', () => {
882
- const obj: any = { count: 0 }
883
- // Since we can't access the internal symbol, test the behavior indirectly
884
- // by using the public markNonReactive function
885
- unreactive(obj)
886
-
887
- expect(isNonReactive(obj)).toBe(true)
888
- expect(reactive(obj)).toBe(obj)
889
- expect(isReactive(obj)).toBe(false)
890
- })
891
-
892
- it('should work with the Reactive mixin', () => {
893
- class TestClass {
894
- count = 0
895
- }
896
-
897
- // Mark the class as non-reactive using the public API
898
- unreactive(TestClass)
899
-
900
- const ReactiveTestClass = reactive(TestClass)
901
- const instance = new ReactiveTestClass()
902
-
903
- expect(isNonReactive(instance)).toBe(true)
904
- expect(isReactive(instance)).toBe(false)
905
- })
906
- })
907
-
908
- describe('native objects', () => {
909
- it('should not make Date objects reactive', () => {
910
- const date = new Date()
911
- expect(reactive(date)).toBe(date)
912
- expect(isReactive(date)).toBe(false)
913
- })
914
-
915
- it('should not make RegExp objects reactive', () => {
916
- const regex = /test/
917
- expect(reactive(regex)).toBe(regex)
918
- expect(isReactive(regex)).toBe(false)
919
- })
920
-
921
- it('should not make Error objects reactive', () => {
922
- const error = new Error('test')
923
- expect(reactive(error)).toBe(error)
924
- expect(isReactive(error)).toBe(false)
925
- })
926
- })
927
-
928
- describe('integration with existing reactive system', () => {
929
- it('should work with effects on non-reactive objects', () => {
930
- const obj = { count: 0 }
931
- unreactive(obj)
932
-
933
- let effectCount = 0
934
- effect(() => {
935
- effectCount++
936
- obj.count // Accessing non-reactive object
937
- })
938
-
939
- expect(effectCount).toBe(1)
940
-
941
- obj.count = 5
942
- expect(effectCount).toBe(1) // Should not trigger effect
943
- })
944
-
945
- it('should allow mixing reactive and non-reactive objects', () => {
946
- const reactiveObj = reactive({ count: 0 })
947
- const unreactiveObj = { name: 'test' }
948
- unreactive(unreactiveObj)
949
-
950
- let effectCount = 0
951
- effect(() => {
952
- effectCount++
953
- reactiveObj.count
954
- unreactiveObj.name
955
- })
956
-
957
- expect(effectCount).toBe(1)
958
-
959
- reactiveObj.count = 5
960
- expect(effectCount).toBe(2) // Should trigger effect
961
-
962
- unreactiveObj.name = 'new name'
963
- expect(effectCount).toBe(2) // Should not trigger effect
964
- })
965
-
966
- it('should work with the Reactive mixin and non-reactive classes', () => {
967
- class NonReactiveClass {
968
- count = 0
969
- }
970
-
971
- unreactive(NonReactiveClass)
972
-
973
- const ReactiveNonReactiveClass = reactive(NonReactiveClass)
974
- const instance = new ReactiveNonReactiveClass()
975
-
976
- expect(isNonReactive(instance)).toBe(true)
977
- expect(isReactive(instance)).toBe(false)
978
-
979
- let effectCount = 0
980
- effect(() => {
981
- effectCount++
982
- instance.count
983
- })
984
-
985
- expect(effectCount).toBe(1)
986
-
987
- instance.count = 5
988
- expect(effectCount).toBe(1) // Should not trigger effect since it's non-reactive
989
- })
990
- })
991
- })
992
-
993
- describe('@unreactive decorator', () => {
994
- describe('class-level decorator syntax', () => {
995
- it('should mark properties as unreactive using class-level syntax', () => {
996
- @unreactive('unreactiveProp')
997
- class TestClass {
998
- unreactiveProp = 'test'
999
-
1000
- reactiveProp = 'reactive'
1001
- }
1002
-
1003
- const instance = new TestClass()
1004
- const reactiveInstance = reactive(instance)
1005
-
1006
- // The unreactive property should not trigger effects
1007
- let effectCount = 0
1008
- effect(() => {
1009
- effectCount++
1010
- reactiveInstance.unreactiveProp
1011
- reactiveInstance.reactiveProp
1012
- })
1013
-
1014
- expect(effectCount).toBe(1)
1015
-
1016
- // Changing unreactive property should not trigger effect
1017
- reactiveInstance.unreactiveProp = 'new value'
1018
- expect(effectCount).toBe(1)
1019
-
1020
- // Changing reactive property should trigger effect
1021
- reactiveInstance.reactiveProp = 'new reactive value'
1022
- expect(effectCount).toBe(2)
1023
- })
1024
-
1025
- it('should work with multiple unreactive properties', () => {
1026
- @unreactive('prop1', 'prop2')
1027
- class TestClass {
1028
- prop1 = 'value1'
1029
-
1030
- prop2 = 'value2'
1031
-
1032
- reactiveProp = 'reactive'
1033
- }
1034
-
1035
- const instance = new TestClass()
1036
- const reactiveInstance = reactive(instance)
1037
-
1038
- let effectCount = 0
1039
- effect(() => {
1040
- effectCount++
1041
- reactiveInstance.prop1
1042
- reactiveInstance.prop2
1043
- reactiveInstance.reactiveProp
1044
- })
1045
-
1046
- expect(effectCount).toBe(1)
1047
-
1048
- // Changing unreactive properties should not trigger effect
1049
- reactiveInstance.prop1 = 'new value1'
1050
- reactiveInstance.prop2 = 'new value2'
1051
- expect(effectCount).toBe(1)
1052
-
1053
- // Changing reactive property should trigger effect
1054
- reactiveInstance.reactiveProp = 'new reactive value'
1055
- expect(effectCount).toBe(2)
1056
- })
1057
-
1058
- it('should work with symbol properties', () => {
1059
- const sym = Symbol('test')
1060
-
1061
- @unreactive(sym)
1062
- class TestClass {
1063
- [sym] = 'symbol value'
1064
-
1065
- reactiveProp = 'reactive'
1066
- }
1067
-
1068
- const instance = new TestClass()
1069
- const reactiveInstance = reactive(instance)
1070
-
1071
- let effectCount = 0
1072
- effect(() => {
1073
- effectCount++
1074
- reactiveInstance[sym]
1075
- reactiveInstance.reactiveProp
1076
- })
1077
-
1078
- expect(effectCount).toBe(1)
1079
-
1080
- // Changing unreactive symbol property should not trigger effect
1081
- reactiveInstance[sym] = 'new symbol value'
1082
- expect(effectCount).toBe(1)
1083
-
1084
- // Changing reactive property should trigger effect
1085
- reactiveInstance.reactiveProp = 'new reactive value'
1086
- expect(effectCount).toBe(2)
1087
- })
1088
- })
1089
-
1090
- describe('integration with reactive system', () => {
1091
- it('should bypass reactivity completely for unreactive properties', () => {
1092
- @unreactive('unreactiveProp')
1093
- class TestClass {
1094
- unreactiveProp = { nested: 'value' }
1095
-
1096
- reactiveProp = { nested: 'reactive' }
1097
- }
1098
-
1099
- const instance = new TestClass()
1100
- const reactiveInstance = reactive(instance)
1101
-
1102
- let effectCount = 0
1103
- effect(() => {
1104
- effectCount++
1105
- reactiveInstance.unreactiveProp.nested
1106
- reactiveInstance.reactiveProp.nested
1107
- })
1108
-
1109
- expect(effectCount).toBe(1)
1110
-
1111
- // Changing nested unreactive property should not trigger effect
1112
- reactiveInstance.unreactiveProp.nested = 'new nested value'
1113
- expect(effectCount).toBe(1)
1114
-
1115
- // Changing nested reactive property should trigger effect
1116
- reactiveInstance.reactiveProp.nested = 'new nested reactive value'
1117
- expect(effectCount).toBe(2)
1118
- })
1119
-
1120
- it('should work with regular properties', () => {
1121
- @unreactive('unreactiveProp')
1122
- class TestClass {
1123
- unreactiveProp = 'test'
1124
-
1125
- reactiveProp = 'reactive'
1126
- }
1127
-
1128
- const instance = new TestClass()
1129
- const reactiveInstance = reactive(instance)
1130
-
1131
- let effectCount = 0
1132
- effect(() => {
1133
- effectCount++
1134
- reactiveInstance.unreactiveProp
1135
- reactiveInstance.reactiveProp
1136
- })
1137
-
1138
- expect(effectCount).toBe(1)
1139
-
1140
- // Changing unreactive property should not trigger effect
1141
- reactiveInstance.unreactiveProp = 'new value'
1142
- expect(effectCount).toBe(1)
1143
-
1144
- // Changing reactive property should trigger effect
1145
- reactiveInstance.reactiveProp = 'new reactive value'
1146
- expect(effectCount).toBe(2)
1147
- })
1148
-
1149
- it('should work with inheritance', () => {
1150
- @unreactive('baseUnreactiveProp')
1151
- class BaseClass {
1152
- baseUnreactiveProp = 'base unreactive'
1153
-
1154
- baseReactiveProp = 'base reactive'
1155
- }
1156
-
1157
- @unreactive('derivedUnreactiveProp')
1158
- class DerivedClass extends BaseClass {
1159
- derivedUnreactiveProp = 'derived unreactive'
1160
-
1161
- derivedReactiveProp = 'derived reactive'
1162
- }
1163
-
1164
- const instance = new DerivedClass()
1165
- const reactiveInstance = reactive(instance)
1166
-
1167
- let effectCount = 0
1168
- effect(() => {
1169
- effectCount++
1170
- reactiveInstance.baseUnreactiveProp
1171
- reactiveInstance.baseReactiveProp
1172
- reactiveInstance.derivedUnreactiveProp
1173
- reactiveInstance.derivedReactiveProp
1174
- })
1175
-
1176
- expect(effectCount).toBe(1)
1177
-
1178
- // Changing unreactive properties should not trigger effect
1179
- reactiveInstance.baseUnreactiveProp = 'new base unreactive'
1180
- reactiveInstance.derivedUnreactiveProp = 'new derived unreactive'
1181
- expect(effectCount).toBe(1)
1182
-
1183
- // Changing reactive properties should trigger effect
1184
- reactiveInstance.baseReactiveProp = 'new base reactive'
1185
- expect(effectCount).toBe(2)
1186
- reactiveInstance.derivedReactiveProp = 'new derived reactive'
1187
- expect(effectCount).toBe(3)
1188
- })
1189
- })
1190
-
1191
- describe('edge cases', () => {
1192
- it('should handle undefined and null values', () => {
1193
- @unreactive('unreactiveProp')
1194
- class TestClass {
1195
- unreactiveProp: any = undefined
1196
-
1197
- reactiveProp: any = null
1198
- }
1199
-
1200
- const instance = new TestClass()
1201
- const reactiveInstance = reactive(instance)
1202
-
1203
- let effectCount = 0
1204
- effect(() => {
1205
- effectCount++
1206
- reactiveInstance.unreactiveProp
1207
- reactiveInstance.reactiveProp
1208
- })
1209
-
1210
- expect(effectCount).toBe(1)
1211
-
1212
- // Setting values should not trigger effects for unreactive properties
1213
- reactiveInstance.unreactiveProp = 'new value'
1214
- expect(effectCount).toBe(1)
1215
-
1216
- // Setting values should trigger effects for reactive properties
1217
- reactiveInstance.reactiveProp = 'new value'
1218
- expect(effectCount).toBe(2)
1219
- })
1220
-
1221
- it('should work with computed property names', () => {
1222
- const propName = 'computed'
1223
-
1224
- @unreactive(propName)
1225
- class TestClass {
1226
- [propName] = 'computed unreactive'
1227
-
1228
- reactiveProp = 'reactive'
1229
- }
1230
-
1231
- const instance = new TestClass()
1232
- const reactiveInstance = reactive(instance)
1233
-
1234
- let effectCount = 0
1235
- effect(() => {
1236
- effectCount++
1237
- reactiveInstance[propName]
1238
- reactiveInstance.reactiveProp
1239
- })
1240
-
1241
- expect(effectCount).toBe(1)
1242
-
1243
- // Changing computed unreactive property should not trigger effect
1244
- reactiveInstance[propName] = 'new computed unreactive'
1245
- expect(effectCount).toBe(1)
1246
-
1247
- // Changing reactive property should trigger effect
1248
- reactiveInstance.reactiveProp = 'new reactive value'
1249
- expect(effectCount).toBe(2)
1250
- })
1251
-
1252
- it('should handle property deletion', () => {
1253
- @unreactive('unreactiveProp')
1254
- class TestClass {
1255
- unreactiveProp?: string = 'test'
1256
-
1257
- reactiveProp?: string = 'reactive'
1258
- }
1259
-
1260
- const instance = new TestClass()
1261
- const reactiveInstance = reactive(instance)
1262
-
1263
- let effectCount = 0
1264
- effect(() => {
1265
- effectCount++
1266
- reactiveInstance.unreactiveProp
1267
- reactiveInstance.reactiveProp
1268
- })
1269
-
1270
- expect(effectCount).toBe(1)
1271
-
1272
- // Deleting unreactive property should not trigger effect
1273
- delete reactiveInstance.unreactiveProp
1274
- expect(effectCount).toBe(1)
1275
-
1276
- // Deleting reactive property should trigger effect
1277
- delete reactiveInstance.reactiveProp
1278
- expect(effectCount).toBe(2)
1279
- })
1280
- })
1281
- })
1282
-
1283
- describe('effect reaction result', () => {
1284
- it('should support recording the computed result each run (via effect return cleanup)', () => {
1285
- const state = reactive({ a: 1, b: 2 })
1286
-
1287
- const received: number[] = []
1288
- const stop = effect(() => {
1289
- const sum = state.a + state.b
1290
- received.push(sum)
1291
- return () => {}
1292
- })
1293
-
1294
- // initial run
1295
- expect(received).toEqual([3])
1296
-
1297
- // update triggers rerun and new result
1298
- state.a = 5
1299
- expect(received).toEqual([3, 7])
1300
-
1301
- // another update
1302
- state.b = 10
1303
- expect(received).toEqual([3, 7, 15])
1304
-
1305
- stop()
1306
- })
1307
- })
1308
-
1309
- describe('effect cleanup timing', () => {
1310
- it('should run previous cleanup before the next execution', () => {
1311
- const state = reactive({ v: 1 })
1312
-
1313
- const calls: string[] = []
1314
- effect(() => {
1315
- calls.push(`run:${state.v}`)
1316
- return () => calls.push(`cleanup:${state.v}`)
1317
- })
1318
-
1319
- // initial
1320
- expect(calls).toEqual(['run:1'])
1321
-
1322
- state.v = 2
1323
- // cleanup for previous run must happen before new run is recorded
1324
- // cleanup logs the current value at cleanup time (already updated)
1325
- expect(calls).toEqual(['run:1', 'cleanup:2', 'run:2'])
1326
-
1327
- state.v = 3
1328
- expect(calls).toEqual(['run:1', 'cleanup:2', 'run:2', 'cleanup:3', 'run:3'])
1329
- })
1330
- })
1331
-
1332
- describe('automatic effect cleanup', () => {
1333
- function tick(ms: number = 100) {
1334
- return new Promise((resolve) => setTimeout(resolve, ms))
1335
- }
1336
-
1337
- const gc = global.gc
1338
-
1339
- async function collectGarbages() {
1340
- await tick()
1341
- gc!()
1342
- await tick()
1343
- }
1344
-
1345
- describe('parent-child effect cleanup', () => {
1346
- it('should automatically clean up child effects when parent is cleaned up', () => {
1347
- const state = reactive({ a: 1, b: 2 })
1348
- const cleanupCalls: string[] = []
1349
-
1350
- const stopParent = effect(() => {
1351
- state.a
1352
-
1353
- // Create child effect
1354
- effect(() => {
1355
- state.b
1356
- return () => cleanupCalls.push('child cleanup')
1357
- })
1358
-
1359
- return () => cleanupCalls.push('parent cleanup')
1360
- })
1361
-
1362
- expect(cleanupCalls).toEqual([])
1363
-
1364
- // Stop parent effect - should clean up both parent and child
1365
- stopParent()
1366
- expect(cleanupCalls).toEqual(['parent cleanup', 'child cleanup'])
1367
- })
1368
-
1369
- it('should clean up all nested child effects when parent is cleaned up', () => {
1370
- const state = reactive({ a: 1, b: 2, c: 3 })
1371
- const cleanupCalls: string[] = []
1372
-
1373
- const stopParent = effect(() => {
1374
- state.a
1375
-
1376
- // Create child effect
1377
- effect(() => {
1378
- state.b
1379
-
1380
- // Create grandchild effect
1381
- effect(() => {
1382
- state.c
1383
- return () => cleanupCalls.push('grandchild cleanup')
1384
- })
1385
-
1386
- return () => cleanupCalls.push('child cleanup')
1387
- })
1388
-
1389
- return () => cleanupCalls.push('parent cleanup')
1390
- })
1391
-
1392
- expect(cleanupCalls).toEqual([])
1393
-
1394
- // Stop parent effect - should clean up all nested effects
1395
- stopParent()
1396
- expect(cleanupCalls).toEqual(['parent cleanup', 'child cleanup', 'grandchild cleanup'])
1397
- })
1398
-
1399
- it('should allow child effects to be cleaned up independently', () => {
1400
- const state = reactive({ a: 1, b: 2 })
1401
- const cleanupCalls: string[] = []
1402
-
1403
- const stopParent = effect(() => {
1404
- state.a
1405
-
1406
- // Create child effect and store its cleanup
1407
- const stopChild = effect(() => {
1408
- state.b
1409
- return () => cleanupCalls.push('child cleanup')
1410
- })
1411
-
1412
- // Clean up child independently
1413
- stopChild()
1414
-
1415
- return () => cleanupCalls.push('parent cleanup')
1416
- })
1417
-
1418
- expect(cleanupCalls).toEqual(['child cleanup'])
1419
-
1420
- // Stop parent effect - should only clean up parent
1421
- stopParent()
1422
- expect(cleanupCalls).toEqual(['child cleanup', 'parent cleanup'])
1423
- })
1424
-
1425
- it('should clean up multiple child effects when parent is cleaned up', () => {
1426
- const state = reactive({ a: 1, b: 2, c: 3 })
1427
- const cleanupCalls: string[] = []
1428
-
1429
- const stopParent = effect(() => {
1430
- state.a
1431
-
1432
- // Create multiple child effects
1433
- effect(() => {
1434
- state.b
1435
- return () => cleanupCalls.push('child1 cleanup')
1436
- })
1437
-
1438
- effect(() => {
1439
- state.c
1440
- return () => cleanupCalls.push('child2 cleanup')
1441
- })
1442
-
1443
- return () => cleanupCalls.push('parent cleanup')
1444
- })
1445
-
1446
- expect(cleanupCalls).toEqual([])
1447
-
1448
- // Stop parent effect - should clean up all children and parent
1449
- stopParent()
1450
- expect(cleanupCalls).toEqual(['parent cleanup', 'child1 cleanup', 'child2 cleanup'])
1451
- })
1452
- })
1453
-
1454
- describe('garbage collection cleanup', () => {
1455
- it('should clean up unreferenced top-level effects via GC', async () => {
1456
- const state = reactive({ value: 1 })
1457
- let cleanupCalled = false
1458
-
1459
- // Create effect in a scope that will be garbage collected
1460
- ;(() => {
1461
- const _x = effect(() => {
1462
- state.value
1463
- return () => {
1464
- cleanupCalled = true
1465
- }
1466
- })
1467
- })()
1468
-
1469
- expect(cleanupCalled).toBe(false)
1470
-
1471
- // Force garbage collection
1472
- await collectGarbages()
1473
- expect(cleanupCalled).toBe(true)
1474
- })
1475
-
1476
- it('should clean up parent and child effects when both are unreferenced', async () => {
1477
- const state = reactive({ a: 1, b: 2 })
1478
- const cleanupCalls: string[] = []
1479
-
1480
- // Create parent effect that creates a child, both unreferenced
1481
- ;(() => {
1482
- effect(() => {
1483
- state.a
1484
-
1485
- // Create child effect
1486
- effect(() => {
1487
- state.b
1488
- return () => cleanupCalls.push('child cleanup')
1489
- })
1490
-
1491
- return () => cleanupCalls.push('parent cleanup')
1492
- })
1493
- })()
1494
-
1495
- expect(cleanupCalls).toEqual([])
1496
-
1497
- // Force garbage collection
1498
- await collectGarbages()
1499
-
1500
- // Both parent and child should be cleaned up
1501
- expect(cleanupCalls).toContain('parent cleanup')
1502
- expect(cleanupCalls).toContain('child cleanup')
1503
- expect(cleanupCalls).toHaveLength(2)
1504
- })
1505
-
1506
- it('should clean up orphaned child effects when parent is unreferenced', async () => {
1507
- const state = reactive({ a: 1, b: 2 })
1508
- const cleanupCalls: string[] = []
1509
-
1510
- // Create parent effect that creates a child, both unreferenced
1511
- ;(() => {
1512
- effect(() => {
1513
- state.a
1514
-
1515
- // Create child effect
1516
- effect(() => {
1517
- state.b
1518
- return () => cleanupCalls.push('child cleanup')
1519
- })
1520
-
1521
- return () => cleanupCalls.push('parent cleanup')
1522
- })
1523
- })()
1524
-
1525
- expect(cleanupCalls).toEqual([])
1526
-
1527
- // Force garbage collection - both should be cleaned up
1528
- await collectGarbages()
1529
-
1530
- expect(cleanupCalls).toContain('parent cleanup')
1531
- expect(cleanupCalls).toContain('child cleanup')
1532
- expect(cleanupCalls).toHaveLength(2)
1533
- })
1534
-
1535
- it('should handle child effect referenced but parent unreferenced', async () => {
1536
- const state = reactive({ a: 1, b: 2 })
1537
- const cleanupCalls: string[] = []
1538
-
1539
- // Create parent effect that creates a child, but only keep reference to child
1540
- let stopChild: (() => void) | undefined
1541
- const createParentWithChild = () => {
1542
- effect(() => {
1543
- state.a
1544
-
1545
- // Create child effect and store its cleanup function
1546
- stopChild = effect(() => {
1547
- state.b
1548
- return () => cleanupCalls.push('child cleanup')
1549
- })
1550
- })
1551
- }
1552
-
1553
- createParentWithChild()
1554
-
1555
- expect(cleanupCalls).toEqual([])
1556
- expect(stopChild).toBeDefined()
1557
-
1558
- // Force garbage collection - parent should be cleaned up, child should remain
1559
- await collectGarbages()
1560
-
1561
- // The child effect should still be alive (parent was GCed but child is referenced)
1562
- // Note: The child might be cleaned up if it's also unreferenced
1563
- // This test demonstrates the mechanism, not the exact behavior
1564
-
1565
- // Explicitly clean up child if it's still alive
1566
- if (stopChild) {
1567
- stopChild()
1568
- expect(cleanupCalls).toContain('child cleanup')
1569
- }
1570
- })
1571
-
1572
- it('should handle mixed explicit and GC cleanup', () => {
1573
- const state = reactive({ a: 1, b: 2, c: 3 })
1574
- const cleanupCalls: string[] = []
1575
-
1576
- // Create parent effect
1577
- const stopParent = effect(() => {
1578
- state.a
1579
-
1580
- // Create child that will be explicitly cleaned up
1581
- const stopChild = effect(() => {
1582
- state.b
1583
- return () => cleanupCalls.push('explicit child cleanup')
1584
- })
1585
-
1586
- // Create child that will be GC cleaned up
1587
- effect(() => {
1588
- state.c
1589
- return () => cleanupCalls.push('gc child cleanup')
1590
- })
1591
-
1592
- // Explicitly clean up first child
1593
- stopChild()
1594
-
1595
- return () => cleanupCalls.push('parent cleanup')
1596
- })
1597
-
1598
- expect(cleanupCalls).toEqual(['explicit child cleanup'])
1599
-
1600
- // Stop parent - should clean up parent and all remaining children
1601
- stopParent()
1602
- expect(cleanupCalls).toEqual(['explicit child cleanup', 'parent cleanup', 'gc child cleanup'])
1603
- })
1604
- })
1605
-
1606
- describe('cleanup behavior documentation', () => {
1607
- it('should demonstrate that cleanup is optional but recommended for side effects', () => {
1608
- const state = reactive({ value: 1 })
1609
- let sideEffectExecuted = false
1610
-
1611
- // Effect with side effect that should be cleaned up
1612
- const stopEffect = effect(() => {
1613
- state.value
1614
-
1615
- // Simulate side effect (e.g., DOM manipulation, timers, etc.)
1616
- const intervalId = setInterval(() => {
1617
- sideEffectExecuted = true
1618
- }, 100)
1619
-
1620
- // Return cleanup function to prevent memory leaks
1621
- return () => {
1622
- clearInterval(intervalId)
1623
- }
1624
- })
1625
-
1626
- // Effect is running, side effect should be active
1627
- expect(sideEffectExecuted).toBe(false)
1628
-
1629
- // Stop effect - cleanup should be called
1630
- stopEffect()
1631
-
1632
- // Wait a bit to ensure interval would have fired
1633
- setTimeout(() => {
1634
- expect(sideEffectExecuted).toBe(false) // Should still be false due to cleanup
1635
- }, 150)
1636
- })
1637
-
1638
- it('should show that effects can be stored and remembered for later cleanup', () => {
1639
- const state = reactive({ value: 1 })
1640
- const activeEffects: (() => void)[] = []
1641
- const cleanupCalls: string[] = []
1642
-
1643
- // Create multiple effects and store their cleanup functions
1644
- for (let i = 0; i < 3; i++) {
1645
- const stopEffect = effect(() => {
1646
- state.value
1647
- return () => cleanupCalls.push(`effect ${i} cleanup`)
1648
- })
1649
- activeEffects.push(stopEffect)
1650
- }
1651
-
1652
- expect(cleanupCalls).toEqual([])
1653
-
1654
- // Clean up all effects at once
1655
- activeEffects.forEach((stop) => stop())
1656
-
1657
- expect(cleanupCalls).toHaveLength(3)
1658
- expect(cleanupCalls).toContain('effect 0 cleanup')
1659
- expect(cleanupCalls).toContain('effect 1 cleanup')
1660
- expect(cleanupCalls).toContain('effect 2 cleanup')
1661
- })
1662
- })
1663
- })