@esportsplus/reactivity 0.34.0 → 0.36.1

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 (44) hide show
  1. package/README.md +9 -7
  2. package/bench/reactive/array.bench.ts +17 -17
  3. package/build/compiler/array.d.ts +2 -2
  4. package/build/compiler/array.js +10 -52
  5. package/build/compiler/constants.d.ts +4 -4
  6. package/build/compiler/constants.js +20 -4
  7. package/build/compiler/index.js +25 -38
  8. package/build/compiler/object.d.ts +2 -2
  9. package/build/compiler/object.js +5 -16
  10. package/build/compiler/primitives.d.ts +6 -2
  11. package/build/compiler/primitives.js +31 -38
  12. package/build/compiler/types.d.ts +3 -1
  13. package/build/constants.d.ts +2 -2
  14. package/build/constants.js +2 -2
  15. package/build/reactive/array.d.ts +1 -1
  16. package/build/reactive/array.js +26 -15
  17. package/build/reactive/index.js +5 -14
  18. package/build/reactive/object.js +1 -1
  19. package/build/system.d.ts +1 -4
  20. package/build/system.js +105 -83
  21. package/build/types.d.ts +2 -7
  22. package/package.json +5 -5
  23. package/pnpm-workspace.yaml +2 -1
  24. package/src/compiler/array.ts +12 -62
  25. package/src/compiler/constants.ts +21 -5
  26. package/src/compiler/index.ts +38 -69
  27. package/src/compiler/object.ts +7 -24
  28. package/src/compiler/primitives.ts +51 -49
  29. package/src/compiler/types.ts +4 -1
  30. package/src/constants.ts +4 -4
  31. package/src/reactive/array.ts +33 -17
  32. package/src/reactive/index.ts +5 -17
  33. package/src/reactive/object.ts +1 -1
  34. package/src/system.ts +138 -101
  35. package/src/types.ts +2 -9
  36. package/test/async-computed.test.ts +2 -2
  37. package/test/compiler/compiler.test.ts +38 -22
  38. package/test/effects.test.ts +37 -1
  39. package/test/lib/uncaught.ts +26 -0
  40. package/test/reactive/array.test.ts +169 -99
  41. package/test/reactive/nested.test.ts +14 -14
  42. package/test/reactive/objects.test.ts +1 -1
  43. package/test/reactive/reactive.test.ts +49 -0
  44. package/test/system.test.ts +130 -22
@@ -3,6 +3,7 @@ import { effect, read, signal, write } from '~/system';
3
3
  import { ReactiveArray } from '~/reactive/array';
4
4
  import { ReactiveObject } from '~/reactive/object';
5
5
  import reactive from '~/reactive/index';
6
+ import { captureUncaught } from '../lib/uncaught';
6
7
 
7
8
 
8
9
  describe('ReactiveArray', () => {
@@ -15,7 +16,7 @@ describe('ReactiveArray', () => {
15
16
  });
16
17
 
17
18
  it('creates array with initial items', () => {
18
- let arr = new ReactiveArray(1, 2, 3);
19
+ let arr = new ReactiveArray([1, 2, 3]);
19
20
 
20
21
  expect(arr.length).toBe(3);
21
22
  expect(arr[0]).toBe(1);
@@ -24,7 +25,7 @@ describe('ReactiveArray', () => {
24
25
  });
25
26
 
26
27
  it('$length is reactive', async () => {
27
- let arr = new ReactiveArray(1, 2, 3),
28
+ let arr = new ReactiveArray([1, 2, 3]),
28
29
  lengths: number[] = [];
29
30
 
30
31
  effect(() => {
@@ -38,12 +39,32 @@ describe('ReactiveArray', () => {
38
39
 
39
40
  expect(lengths).toEqual([3, 4]);
40
41
  });
42
+
43
+ it('a single numeric item does not create holes', () => {
44
+ let arr = reactive([5]);
45
+
46
+ expect([...arr]).toEqual([5]);
47
+ expect(arr.length).toBe(1);
48
+ expect((arr as unknown as { $length: number }).$length).toBe(1);
49
+ });
50
+
51
+ it('empty and multi-item reactive arrays are unchanged', () => {
52
+ expect([...reactive([])]).toEqual([]);
53
+ expect([...reactive([1, 2, 3])]).toEqual([1, 2, 3]);
54
+ });
55
+
56
+ it('does not overflow the stack for a large array', () => {
57
+ let arr = new ReactiveArray(new Array(500000).fill(0));
58
+
59
+ expect(arr.length).toBe(500000);
60
+ expect(arr.$length).toBe(500000);
61
+ });
41
62
  });
42
63
 
43
64
 
44
65
  describe('$set', () => {
45
66
  it('sets value at index', () => {
46
- let arr = new ReactiveArray(1, 2, 3);
67
+ let arr = new ReactiveArray([1, 2, 3]);
47
68
 
48
69
  arr.$set(1, 20);
49
70
 
@@ -51,7 +72,7 @@ describe('ReactiveArray', () => {
51
72
  });
52
73
 
53
74
  it('skips if same value', () => {
54
- let arr = new ReactiveArray(1, 2, 3),
75
+ let arr = new ReactiveArray([1, 2, 3]),
55
76
  dispatched = false;
56
77
 
57
78
  arr.on('set', () => { dispatched = true; });
@@ -61,7 +82,7 @@ describe('ReactiveArray', () => {
61
82
  });
62
83
 
63
84
  it('dispatches set event', () => {
64
- let arr = new ReactiveArray(1, 2, 3),
85
+ let arr = new ReactiveArray([1, 2, 3]),
65
86
  events: { index: number; item: number }[] = [];
66
87
 
67
88
  arr.on('set', (e) => { events.push(e); });
@@ -71,7 +92,7 @@ describe('ReactiveArray', () => {
71
92
  });
72
93
 
73
94
  it('updates length when setting beyond current length', async () => {
74
- let arr = new ReactiveArray(1, 2, 3);
95
+ let arr = new ReactiveArray([1, 2, 3]);
75
96
 
76
97
  arr.$set(5, 99);
77
98
 
@@ -79,7 +100,7 @@ describe('ReactiveArray', () => {
79
100
  });
80
101
 
81
102
  it('$set beyond length updates $length reactively', async () => {
82
- let arr = new ReactiveArray(1, 2, 3),
103
+ let arr = new ReactiveArray([1, 2, 3]),
83
104
  lengths: number[] = [];
84
105
 
85
106
  effect(() => {
@@ -125,7 +146,7 @@ describe('ReactiveArray', () => {
125
146
  });
126
147
 
127
148
  it('$set with negative index does not affect length', () => {
128
- let arr = new ReactiveArray(1, 2, 3);
149
+ let arr = new ReactiveArray([1, 2, 3]);
129
150
 
130
151
  arr.$set(-1 as any, 42);
131
152
 
@@ -135,7 +156,7 @@ describe('ReactiveArray', () => {
135
156
  });
136
157
 
137
158
  it('$set at large index updates $length', async () => {
138
- let arr = new ReactiveArray(1, 2, 3),
159
+ let arr = new ReactiveArray([1, 2, 3]),
139
160
  lengths: number[] = [];
140
161
 
141
162
  effect(() => {
@@ -155,13 +176,13 @@ describe('ReactiveArray', () => {
155
176
 
156
177
  describe('$length', () => {
157
178
  it('getter returns reactive length', () => {
158
- let arr = new ReactiveArray(1, 2, 3);
179
+ let arr = new ReactiveArray([1, 2, 3]);
159
180
 
160
181
  expect(arr.$length).toBe(3);
161
182
  });
162
183
 
163
184
  it('setter truncates array via splice', () => {
164
- let arr = new ReactiveArray(1, 2, 3, 4, 5);
185
+ let arr = new ReactiveArray([1, 2, 3, 4, 5]);
165
186
 
166
187
  arr.$length = 2;
167
188
 
@@ -171,7 +192,7 @@ describe('ReactiveArray', () => {
171
192
  });
172
193
 
173
194
  it('throws when setting length larger than current', () => {
174
- let arr = new ReactiveArray(1, 2);
195
+ let arr = new ReactiveArray([1, 2]);
175
196
 
176
197
  expect(() => { arr.$length = 5; }).toThrow();
177
198
  });
@@ -189,7 +210,7 @@ describe('ReactiveArray', () => {
189
210
  });
190
211
 
191
212
  it('returns new length', () => {
192
- let arr = new ReactiveArray(1);
213
+ let arr = new ReactiveArray([1]);
193
214
 
194
215
  expect(arr.push(2, 3)).toBe(3);
195
216
  });
@@ -205,7 +226,7 @@ describe('ReactiveArray', () => {
205
226
  });
206
227
 
207
228
  it('no-op for empty push', () => {
208
- let arr = new ReactiveArray(1),
229
+ let arr = new ReactiveArray([1]),
209
230
  dispatched = false;
210
231
 
211
232
  arr.on('push', () => { dispatched = true; });
@@ -229,7 +250,7 @@ describe('ReactiveArray', () => {
229
250
 
230
251
  describe('pop', () => {
231
252
  it('removes last item', () => {
232
- let arr = new ReactiveArray(1, 2, 3);
253
+ let arr = new ReactiveArray([1, 2, 3]);
233
254
 
234
255
  expect(arr.pop()).toBe(3);
235
256
  expect(arr.length).toBe(2);
@@ -242,7 +263,7 @@ describe('ReactiveArray', () => {
242
263
  });
243
264
 
244
265
  it('dispatches pop event', () => {
245
- let arr = new ReactiveArray(1, 2),
266
+ let arr = new ReactiveArray([1, 2]),
246
267
  events: { item: number }[] = [];
247
268
 
248
269
  arr.on('pop', (e) => { events.push(e); });
@@ -262,7 +283,7 @@ describe('ReactiveArray', () => {
262
283
  });
263
284
 
264
285
  it('does not dispatch when popping explicit undefined value', () => {
265
- let arr = new ReactiveArray<number | undefined>(1, undefined),
286
+ let arr = new ReactiveArray<number | undefined>([1, undefined]),
266
287
  dispatched = false;
267
288
 
268
289
  arr.on('pop', () => { dispatched = true; });
@@ -274,7 +295,7 @@ describe('ReactiveArray', () => {
274
295
  });
275
296
 
276
297
  it('does not update reactive length when popping explicit undefined value', async () => {
277
- let arr = new ReactiveArray<number | undefined>(1, undefined),
298
+ let arr = new ReactiveArray<number | undefined>([1, undefined]),
278
299
  lengths: number[] = [];
279
300
 
280
301
  effect(() => {
@@ -293,7 +314,7 @@ describe('ReactiveArray', () => {
293
314
 
294
315
  describe('shift', () => {
295
316
  it('removes first item', () => {
296
- let arr = new ReactiveArray(1, 2, 3);
317
+ let arr = new ReactiveArray([1, 2, 3]);
297
318
 
298
319
  expect(arr.shift()).toBe(1);
299
320
  expect(arr.length).toBe(2);
@@ -307,7 +328,7 @@ describe('ReactiveArray', () => {
307
328
  });
308
329
 
309
330
  it('dispatches shift event', () => {
310
- let arr = new ReactiveArray(10, 20),
331
+ let arr = new ReactiveArray([10, 20]),
311
332
  events: { item: number }[] = [];
312
333
 
313
334
  arr.on('shift', (e) => { events.push(e); });
@@ -317,7 +338,7 @@ describe('ReactiveArray', () => {
317
338
  });
318
339
 
319
340
  it('does not dispatch when shifting explicit undefined value', () => {
320
- let arr = new ReactiveArray<number | undefined>(undefined, 1, 2),
341
+ let arr = new ReactiveArray<number | undefined>([undefined, 1, 2]),
321
342
  dispatched = false;
322
343
 
323
344
  arr.on('shift', () => { dispatched = true; });
@@ -330,7 +351,7 @@ describe('ReactiveArray', () => {
330
351
  });
331
352
 
332
353
  it('does not update reactive length when shifting explicit undefined value', async () => {
333
- let arr = new ReactiveArray<number | undefined>(undefined, 1, 2),
354
+ let arr = new ReactiveArray<number | undefined>([undefined, 1, 2]),
334
355
  lengths: number[] = [];
335
356
 
336
357
  effect(() => {
@@ -360,7 +381,7 @@ describe('ReactiveArray', () => {
360
381
  });
361
382
 
362
383
  it('returns new length', () => {
363
- let arr = new ReactiveArray(1);
384
+ let arr = new ReactiveArray([1]);
364
385
 
365
386
  expect(arr.unshift(0)).toBe(2);
366
387
  });
@@ -376,7 +397,7 @@ describe('ReactiveArray', () => {
376
397
  });
377
398
 
378
399
  it('no-op for empty unshift', async () => {
379
- let arr = new ReactiveArray(1, 2),
400
+ let arr = new ReactiveArray([1, 2]),
380
401
  dispatched = false,
381
402
  lengths: number[] = [];
382
403
 
@@ -399,7 +420,7 @@ describe('ReactiveArray', () => {
399
420
 
400
421
  describe('splice', () => {
401
422
  it('removes items', () => {
402
- let arr = new ReactiveArray(1, 2, 3, 4, 5);
423
+ let arr = new ReactiveArray([1, 2, 3, 4, 5]);
403
424
 
404
425
  let removed = arr.splice(1, 2);
405
426
 
@@ -408,7 +429,7 @@ describe('ReactiveArray', () => {
408
429
  });
409
430
 
410
431
  it('inserts items', () => {
411
- let arr = new ReactiveArray(1, 4, 5);
432
+ let arr = new ReactiveArray([1, 4, 5]);
412
433
 
413
434
  arr.splice(1, 0, 2, 3);
414
435
 
@@ -416,7 +437,7 @@ describe('ReactiveArray', () => {
416
437
  });
417
438
 
418
439
  it('replaces items', () => {
419
- let arr = new ReactiveArray(1, 2, 3);
440
+ let arr = new ReactiveArray([1, 2, 3]);
420
441
 
421
442
  arr.splice(1, 1, 20);
422
443
 
@@ -424,7 +445,7 @@ describe('ReactiveArray', () => {
424
445
  });
425
446
 
426
447
  it('dispatches splice event', () => {
427
- let arr = new ReactiveArray(1, 2, 3),
448
+ let arr = new ReactiveArray([1, 2, 3]),
428
449
  events: { start: number; deleteCount: number; items: number[] }[] = [];
429
450
 
430
451
  arr.on('splice', (e) => { events.push(e); });
@@ -434,7 +455,7 @@ describe('ReactiveArray', () => {
434
455
  });
435
456
 
436
457
  it('no event when nothing changes', () => {
437
- let arr = new ReactiveArray(1, 2, 3),
458
+ let arr = new ReactiveArray([1, 2, 3]),
438
459
  dispatched = false;
439
460
 
440
461
  arr.on('splice', () => { dispatched = true; });
@@ -444,7 +465,7 @@ describe('ReactiveArray', () => {
444
465
  });
445
466
 
446
467
  it('splice with start beyond array length removes nothing', () => {
447
- let arr = new ReactiveArray(1, 2, 3),
468
+ let arr = new ReactiveArray([1, 2, 3]),
448
469
  dispatched = false;
449
470
 
450
471
  arr.on('splice', () => { dispatched = true; });
@@ -457,7 +478,7 @@ describe('ReactiveArray', () => {
457
478
  });
458
479
 
459
480
  it('splice with negative start removes from end', () => {
460
- let arr = new ReactiveArray(1, 2, 3, 4, 5),
481
+ let arr = new ReactiveArray([1, 2, 3, 4, 5]),
461
482
  events: { start: number; deleteCount: number; items: number[] }[] = [];
462
483
 
463
484
  arr.on('splice', (e) => { events.push(e); });
@@ -473,7 +494,7 @@ describe('ReactiveArray', () => {
473
494
 
474
495
  describe('concat', () => {
475
496
  it('appends arrays', () => {
476
- let arr = new ReactiveArray(1, 2);
497
+ let arr = new ReactiveArray([1, 2]);
477
498
 
478
499
  arr.concat([3, 4], [5]);
479
500
 
@@ -490,7 +511,7 @@ describe('ReactiveArray', () => {
490
511
  });
491
512
 
492
513
  it('returns this (mutating)', () => {
493
- let arr = new ReactiveArray(1);
514
+ let arr = new ReactiveArray([1]);
494
515
  let result = arr.concat([2]);
495
516
 
496
517
  expect(result).toBe(arr);
@@ -507,7 +528,7 @@ describe('ReactiveArray', () => {
507
528
  });
508
529
 
509
530
  it('no event when nothing added', () => {
510
- let arr = new ReactiveArray(1),
531
+ let arr = new ReactiveArray([1]),
511
532
  dispatched = false;
512
533
 
513
534
  arr.on('concat', () => { dispatched = true; });
@@ -517,7 +538,7 @@ describe('ReactiveArray', () => {
517
538
  });
518
539
 
519
540
  it('concat with mixed arrays and single primitive values', () => {
520
- let arr = new ReactiveArray(1, 2),
541
+ let arr = new ReactiveArray([1, 2]),
521
542
  events: number[][] = [];
522
543
 
523
544
  arr.on('concat', (e) => { events.push(e.items); });
@@ -531,7 +552,7 @@ describe('ReactiveArray', () => {
531
552
 
532
553
  describe('reverse', () => {
533
554
  it('reverses in place', () => {
534
- let arr = new ReactiveArray(1, 2, 3);
555
+ let arr = new ReactiveArray([1, 2, 3]);
535
556
 
536
557
  arr.reverse();
537
558
 
@@ -539,13 +560,13 @@ describe('ReactiveArray', () => {
539
560
  });
540
561
 
541
562
  it('returns this', () => {
542
- let arr = new ReactiveArray(1, 2);
563
+ let arr = new ReactiveArray([1, 2]);
543
564
 
544
565
  expect(arr.reverse()).toBe(arr);
545
566
  });
546
567
 
547
568
  it('dispatches reverse event', () => {
548
- let arr = new ReactiveArray(1, 2),
569
+ let arr = new ReactiveArray([1, 2]),
549
570
  dispatched = false;
550
571
 
551
572
  arr.on('reverse', () => { dispatched = true; });
@@ -558,7 +579,7 @@ describe('ReactiveArray', () => {
558
579
 
559
580
  describe('sort', () => {
560
581
  it('sorts in place', () => {
561
- let arr = new ReactiveArray(3, 1, 2);
582
+ let arr = new ReactiveArray([3, 1, 2]);
562
583
 
563
584
  arr.sort((a, b) => a - b);
564
585
 
@@ -566,13 +587,13 @@ describe('ReactiveArray', () => {
566
587
  });
567
588
 
568
589
  it('returns this', () => {
569
- let arr = new ReactiveArray(3, 1);
590
+ let arr = new ReactiveArray([3, 1]);
570
591
 
571
592
  expect(arr.sort()).toBe(arr);
572
593
  });
573
594
 
574
595
  it('dispatches sort event with order', () => {
575
- let arr = new ReactiveArray(3, 1, 2),
596
+ let arr = new ReactiveArray([3, 1, 2]),
576
597
  order: number[] = [];
577
598
 
578
599
  arr.on('sort', (e) => { order = e.order; });
@@ -584,7 +605,7 @@ describe('ReactiveArray', () => {
584
605
  });
585
606
 
586
607
  it('handles duplicates in sort', () => {
587
- let arr = new ReactiveArray(2, 1, 2),
608
+ let arr = new ReactiveArray([2, 1, 2]),
588
609
  order: number[] = [];
589
610
 
590
611
  arr.on('sort', (e) => { order = e.order; });
@@ -593,11 +614,32 @@ describe('ReactiveArray', () => {
593
614
  expect([...arr]).toEqual([1, 2, 2]);
594
615
  });
595
616
 
617
+ it('order maps duplicate values stably (first stays first)', () => {
618
+ let arr = new ReactiveArray([3, 1, 3, 2]),
619
+ order: number[] = [];
620
+
621
+ arr.on('sort', (e) => { order = e.order; });
622
+ arr.sort((a, b) => a - b);
623
+
624
+ expect([...arr]).toEqual([1, 2, 3, 3]);
625
+ expect(order).toEqual([1, 3, 0, 2]);
626
+ });
627
+
628
+ it('all-equal array yields identity order', () => {
629
+ let arr = new ReactiveArray([5, 5, 5]),
630
+ order: number[] = [];
631
+
632
+ arr.on('sort', (e) => { order = e.order; });
633
+ arr.sort((a, b) => a - b);
634
+
635
+ expect(order).toEqual([0, 1, 2]);
636
+ });
637
+
596
638
  it('sort preserves object references', () => {
597
639
  let a = { id: 3 },
598
640
  b = { id: 1 },
599
641
  c = { id: 2 },
600
- arr = new ReactiveArray(a, b, c);
642
+ arr = new ReactiveArray([a, b, c]);
601
643
 
602
644
  arr.sort((x, y) => x.id - y.id);
603
645
 
@@ -611,7 +653,7 @@ describe('ReactiveArray', () => {
611
653
 
612
654
  describe('clear', () => {
613
655
  it('empties array', () => {
614
- let arr = new ReactiveArray(1, 2, 3);
656
+ let arr = new ReactiveArray([1, 2, 3]);
615
657
 
616
658
  arr.clear();
617
659
 
@@ -619,7 +661,7 @@ describe('ReactiveArray', () => {
619
661
  });
620
662
 
621
663
  it('dispatches clear event', () => {
622
- let arr = new ReactiveArray(1, 2),
664
+ let arr = new ReactiveArray([1, 2]),
623
665
  dispatched = false;
624
666
 
625
667
  arr.on('clear', () => { dispatched = true; });
@@ -629,7 +671,7 @@ describe('ReactiveArray', () => {
629
671
  });
630
672
 
631
673
  it('updates reactive length to 0', async () => {
632
- let arr = new ReactiveArray(1, 2, 3),
674
+ let arr = new ReactiveArray([1, 2, 3]),
633
675
  lengths: number[] = [];
634
676
 
635
677
  effect(() => { lengths.push(arr.$length); });
@@ -643,7 +685,7 @@ describe('ReactiveArray', () => {
643
685
 
644
686
  describe('dispose', () => {
645
687
  it('empties array', () => {
646
- let arr = new ReactiveArray(1, 2, 3);
688
+ let arr = new ReactiveArray([1, 2, 3]);
647
689
 
648
690
  arr.dispose();
649
691
 
@@ -651,7 +693,7 @@ describe('ReactiveArray', () => {
651
693
  });
652
694
 
653
695
  it('sets reactive length to 0', async () => {
654
- let arr = new ReactiveArray(1, 2, 3);
696
+ let arr = new ReactiveArray([1, 2, 3]);
655
697
 
656
698
  arr.dispose();
657
699
 
@@ -696,27 +738,33 @@ describe('ReactiveArray', () => {
696
738
  expect(calls).toBe(1);
697
739
  });
698
740
 
699
- it('listener errors are caught and listener removed', () => {
741
+ it('listener errors surface asynchronously and the listener is removed', async () => {
700
742
  let arr = new ReactiveArray<number>(),
701
- calls = 0;
743
+ calls = 0,
744
+ captured = await captureUncaught(() => {
745
+ arr.on('push', () => { throw new Error('reactivity: test'); });
746
+ arr.on('push', () => { calls++; });
702
747
 
703
- arr.on('push', () => { throw new Error('test'); });
704
- arr.on('push', () => { calls++; });
705
-
706
- arr.push(1);
707
- arr.push(2);
748
+ arr.push(1);
749
+ arr.push(2);
750
+ });
708
751
 
709
752
  expect(calls).toBe(2);
753
+ expect(captured.length).toBe(1);
754
+ expect((captured[0] as Error).message).toBe('reactivity: test');
710
755
  });
711
756
 
712
- it('null slots reused for new listeners', () => {
713
- let arr = new ReactiveArray<number>();
757
+ it('null slots reused for new listeners', async () => {
758
+ let arr = new ReactiveArray<number>(),
759
+ fn1 = () => { throw new Error('reactivity: remove me'); },
760
+ fn2 = vi.fn();
714
761
 
715
- let fn1 = () => { throw new Error('remove me'); };
716
- let fn2 = vi.fn();
762
+ let captured = await captureUncaught(() => {
763
+ arr.on('push', fn1);
764
+ arr.push(1); // fn1 throws and gets nulled
765
+ });
717
766
 
718
- arr.on('push', fn1);
719
- arr.push(1); // fn1 throws and gets nulled
767
+ expect(captured.length).toBe(1);
720
768
 
721
769
  arr.on('push', fn2);
722
770
  arr.push(2);
@@ -724,23 +772,26 @@ describe('ReactiveArray', () => {
724
772
  expect(fn2).toHaveBeenCalledTimes(1);
725
773
  });
726
774
 
727
- it('multiple listeners removed via errors, new listeners fill holes in order', () => {
775
+ it('multiple listeners removed via errors, new listeners fill holes in order', async () => {
728
776
  let arr = new ReactiveArray<number>(),
729
- order: number[] = [];
730
-
731
- let err1 = () => { throw new Error('err1'); };
732
- let err2 = () => { throw new Error('err2'); };
733
- let fn3 = vi.fn();
734
-
735
- arr.on('push', err1);
736
- arr.on('push', err2);
737
- arr.on('push', fn3);
738
- arr.push(1); // err1 and err2 throw, slots 0 and 1 nulled
777
+ err1 = () => { throw new Error('reactivity: err1'); },
778
+ err2 = () => { throw new Error('reactivity: err2'); },
779
+ fn3 = vi.fn();
780
+
781
+ let captured = await captureUncaught(() => {
782
+ arr.on('push', err1);
783
+ arr.on('push', err2);
784
+ arr.on('push', fn3);
785
+ arr.push(1); // err1 and err2 throw, slots 0 and 1 nulled
786
+ });
739
787
 
740
788
  expect(fn3).toHaveBeenCalledTimes(1);
789
+ expect(captured.length).toBe(1);
790
+ expect(captured[0]).toBeInstanceOf(AggregateError);
791
+ expect((captured[0] as AggregateError).errors.length).toBe(2);
741
792
 
742
- let fn4 = vi.fn();
743
- let fn5 = vi.fn();
793
+ let fn4 = vi.fn(),
794
+ fn5 = vi.fn();
744
795
 
745
796
  arr.on('push', fn4); // fills hole at slot 0
746
797
  arr.on('push', fn5); // fills hole at slot 1
@@ -751,39 +802,44 @@ describe('ReactiveArray', () => {
751
802
  expect(fn5).toHaveBeenCalledTimes(1);
752
803
  });
753
804
 
754
- it('trailing null slots cleaned after dispatch', () => {
755
- let arr = new ReactiveArray<number>();
756
-
757
- let fn1 = vi.fn();
758
- let err2 = () => { throw new Error('remove'); };
805
+ it('trailing null slots cleaned after dispatch', async () => {
806
+ let arr = new ReactiveArray<number>(),
807
+ fn1 = vi.fn(),
808
+ err2 = () => { throw new Error('reactivity: remove'); };
759
809
 
760
- arr.on('push', fn1);
761
- arr.on('push', err2);
810
+ let captured = await captureUncaught(() => {
811
+ arr.on('push', fn1);
812
+ arr.on('push', err2);
762
813
 
763
- // Before dispatch: listeners = [fn1, err2] (length 2)
764
- arr.push(1); // err2 throws → nulled → trailing null cleaned
814
+ // Before dispatch: listeners = [fn1, err2] (length 2)
815
+ arr.push(1); // err2 throws → nulled → trailing null cleaned
816
+ });
765
817
 
766
818
  // Trailing null should be cleaned, so internal array length is 1
767
819
  expect(arr.listeners['push']!.length).toBe(1);
768
820
  expect(fn1).toHaveBeenCalledTimes(1);
821
+ expect(captured.length).toBe(1);
769
822
  });
770
823
 
771
- it('on() inserts after trailing nulls cleaned from dispatch', () => {
824
+ it('on() inserts after trailing nulls cleaned from dispatch', async () => {
772
825
  let arr = new ReactiveArray<number>(),
773
826
  fn1 = vi.fn(),
774
- fn2 = () => { throw new Error('err2'); },
775
- fn3 = () => { throw new Error('err3'); };
827
+ fn2 = () => { throw new Error('reactivity: err2'); },
828
+ fn3 = () => { throw new Error('reactivity: err3'); };
776
829
 
777
- arr.on('push', fn1);
778
- arr.on('push', fn2);
779
- arr.on('push', fn3);
830
+ let captured = await captureUncaught(() => {
831
+ arr.on('push', fn1);
832
+ arr.on('push', fn2);
833
+ arr.on('push', fn3);
780
834
 
781
- // Dispatch: fn2 and fn3 throw → nulled → trailing nulls cleaned
782
- // listeners = [fn1, null, null] → cleanup → [fn1]
783
- arr.push(1);
835
+ // Dispatch: fn2 and fn3 throw → nulled → trailing nulls cleaned
836
+ // listeners = [fn1, null, null] → cleanup → [fn1]
837
+ arr.push(1);
838
+ });
784
839
 
785
840
  expect(fn1).toHaveBeenCalledTimes(1);
786
841
  expect(arr.listeners['push']!.length).toBe(1);
842
+ expect(captured[0]).toBeInstanceOf(AggregateError);
787
843
 
788
844
  // Register fn4 — should append at index 1 (no holes left)
789
845
  let fn4 = vi.fn();
@@ -942,7 +998,7 @@ describe('ReactiveArray', () => {
942
998
  spyB = vi.spyOn(b, 'dispose'),
943
999
  spyC = vi.spyOn(c, 'dispose');
944
1000
 
945
- let arr = new ReactiveArray<ReactiveObject<any>>(a, b, c);
1001
+ let arr = new ReactiveArray<ReactiveObject<any>>([a, b, c]);
946
1002
 
947
1003
  arr.dispose();
948
1004
 
@@ -958,7 +1014,7 @@ describe('ReactiveArray', () => {
958
1014
  spyA = vi.spyOn(a, 'dispose'),
959
1015
  spyB = vi.spyOn(b, 'dispose');
960
1016
 
961
- let arr = new ReactiveArray<ReactiveObject<any>>(a, b);
1017
+ let arr = new ReactiveArray<ReactiveObject<any>>([a, b]);
962
1018
 
963
1019
  arr.clear();
964
1020
 
@@ -973,7 +1029,7 @@ describe('ReactiveArray', () => {
973
1029
  spyA = vi.spyOn(a, 'dispose'),
974
1030
  spyB = vi.spyOn(b, 'dispose');
975
1031
 
976
- let arr = new ReactiveArray<ReactiveObject<any>>(a, b);
1032
+ let arr = new ReactiveArray<ReactiveObject<any>>([a, b]);
977
1033
 
978
1034
  arr.pop();
979
1035
 
@@ -988,7 +1044,7 @@ describe('ReactiveArray', () => {
988
1044
  spyA = vi.spyOn(a, 'dispose'),
989
1045
  spyB = vi.spyOn(b, 'dispose');
990
1046
 
991
- let arr = new ReactiveArray<ReactiveObject<any>>(a, b);
1047
+ let arr = new ReactiveArray<ReactiveObject<any>>([a, b]);
992
1048
 
993
1049
  arr.shift();
994
1050
 
@@ -1007,7 +1063,7 @@ describe('ReactiveArray', () => {
1007
1063
  spyC = vi.spyOn(c, 'dispose'),
1008
1064
  spyD = vi.spyOn(d, 'dispose');
1009
1065
 
1010
- let arr = new ReactiveArray<ReactiveObject<any>>(a, b, c, d);
1066
+ let arr = new ReactiveArray<ReactiveObject<any>>([a, b, c, d]);
1011
1067
 
1012
1068
  arr.splice(1, 2);
1013
1069
 
@@ -1026,7 +1082,7 @@ describe('ReactiveArray', () => {
1026
1082
  spyB = vi.spyOn(b, 'dispose'),
1027
1083
  spyR = vi.spyOn(replacement, 'dispose');
1028
1084
 
1029
- let arr = new ReactiveArray<ReactiveObject<any>>(a, b);
1085
+ let arr = new ReactiveArray<ReactiveObject<any>>([a, b]);
1030
1086
 
1031
1087
  arr.splice(0, 1, replacement);
1032
1088
 
@@ -1036,10 +1092,24 @@ describe('ReactiveArray', () => {
1036
1092
  expect(arr.length).toBe(2);
1037
1093
  });
1038
1094
 
1095
+ it('$set() disposes the replaced ReactiveObject', () => {
1096
+ let a = new ReactiveObject({ x: 1 }),
1097
+ b = new ReactiveObject({ y: 2 }),
1098
+ spyA = vi.spyOn(a, 'dispose'),
1099
+ spyB = vi.spyOn(b, 'dispose'),
1100
+ arr = new ReactiveArray<ReactiveObject<any>>([a]);
1101
+
1102
+ arr.$set(0, b);
1103
+
1104
+ expect(spyA).toHaveBeenCalledTimes(1);
1105
+ expect(spyB).not.toHaveBeenCalled();
1106
+ expect(arr[0]).toBe(b);
1107
+ });
1108
+
1039
1109
  it('does not dispose non-ReactiveObject elements', () => {
1040
1110
  let obj = { dispose: vi.fn() };
1041
1111
 
1042
- let arr = new ReactiveArray<any>(1, 'str', obj);
1112
+ let arr = new ReactiveArray<any>([1, 'str', obj]);
1043
1113
 
1044
1114
  arr.dispose();
1045
1115