@esportsplus/reactivity 0.30.3 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/tests/system.ts CHANGED
@@ -478,6 +478,33 @@ describe('root', () => {
478
478
  expect(outer).toBe(1);
479
479
  expect(inner).toBe(1);
480
480
  });
481
+
482
+ it('tracks disposables counter for unowned computeds', () => {
483
+ let before = root.disposables;
484
+
485
+ root(() => {
486
+ computed(() => 1);
487
+ computed(() => 2);
488
+ computed(() => 3);
489
+ });
490
+
491
+ // root restores disposables to outer value after execution
492
+ expect(root.disposables).toBe(before);
493
+
494
+ // Nested: inner root creates computeds, outer root creates computeds
495
+ root(() => {
496
+ computed(() => 10);
497
+
498
+ root(() => {
499
+ computed(() => 20);
500
+ computed(() => 30);
501
+ });
502
+
503
+ computed(() => 40);
504
+ });
505
+
506
+ expect(root.disposables).toBe(before);
507
+ });
481
508
  });
482
509
 
483
510
 
@@ -548,10 +575,93 @@ describe('isComputed', () => {
548
575
  expect(isComputed(1)).toBe(false);
549
576
  expect(isComputed(null)).toBe(false);
550
577
  });
578
+
579
+ it('returns false for objects with state field but no STATE_COMPUTED bit', () => {
580
+ expect(isComputed({ state: 0 })).toBe(false);
581
+ expect(isComputed({ state: 1 })).toBe(false);
582
+ });
583
+ });
584
+
585
+ describe('computed object size', () => {
586
+ it('does not have a type field', () => {
587
+ let c = computed(() => 42);
588
+
589
+ expect('type' in c).toBe(false);
590
+ });
591
+
592
+ it('has fewer own properties than 14 (old size)', () => {
593
+ let c = computed(() => 42);
594
+
595
+ expect(Object.keys(c).length).toBeLessThan(14);
596
+ });
551
597
  });
552
598
 
553
599
 
554
600
  describe('edge cases', () => {
601
+ it('diamond graph dedup — notify state mask prevents redundant recomputation', async () => {
602
+ let s = signal(1),
603
+ calls = 0,
604
+ left = computed(() => read(s) + 1),
605
+ right = computed(() => read(s) * 2),
606
+ join = computed(() => {
607
+ calls++;
608
+ return read(left) + read(right);
609
+ });
610
+
611
+ effect(() => {
612
+ read(join);
613
+ });
614
+
615
+ expect(read(join)).toBe(4);
616
+ calls = 0;
617
+
618
+ write(s, 2);
619
+ await Promise.resolve();
620
+
621
+ expect(read(join)).toBe(7);
622
+ expect(calls).toBe(1);
623
+ });
624
+
625
+ it('dynamic height adjustment — correct ordering after switching deps', async () => {
626
+ let s = signal(1),
627
+ toggle = signal(true),
628
+ a = computed(() => read(s) + 1),
629
+ b = computed(() => read(a) + 1),
630
+ c = computed(() => read(b) + 1),
631
+ order: string[] = [],
632
+ d = computed(() => {
633
+ order.push('d');
634
+
635
+ if (read(toggle)) {
636
+ return read(a);
637
+ }
638
+
639
+ return read(c);
640
+ });
641
+
642
+ effect(() => {
643
+ read(d);
644
+ });
645
+
646
+ expect(read(d)).toBe(2);
647
+ order.length = 0;
648
+
649
+ // Switch to reading `c` (height 3) instead of `a` (height 1)
650
+ write(toggle, false);
651
+ await Promise.resolve();
652
+
653
+ expect(read(d)).toBe(4);
654
+
655
+ order.length = 0;
656
+
657
+ // Write to source — d should recompute after c due to height adjustment
658
+ write(s, 10);
659
+ await Promise.resolve();
660
+
661
+ expect(read(d)).toBe(13);
662
+ expect(order).toEqual(['d']);
663
+ });
664
+
555
665
  it('handles circular computed reads without infinite loop', () => {
556
666
  let s = signal(0),
557
667
  c1 = computed(() => read(s)),
@@ -652,4 +762,253 @@ describe('edge cases', () => {
652
762
 
653
763
  expect(innerDisposed).toBe(true);
654
764
  });
765
+
766
+ it('stabilizer re-schedules when effect writes to signal during stabilization', async () => {
767
+ let a = signal(0),
768
+ b = signal(0),
769
+ bValues: number[] = [];
770
+
771
+ effect(() => {
772
+ let val = read(a);
773
+
774
+ if (val > 0) {
775
+ write(b, val * 100);
776
+ }
777
+ });
778
+
779
+ effect(() => {
780
+ bValues.push(read(b));
781
+ });
782
+
783
+ write(a, 3);
784
+ await Promise.resolve();
785
+ await Promise.resolve();
786
+
787
+ expect(bValues).toEqual([0, 300]);
788
+ });
789
+
790
+ it('stabilizer re-schedules with nested write chain A → B → C', async () => {
791
+ let a = signal(0),
792
+ b = signal(0),
793
+ c = signal(0),
794
+ cValues: number[] = [];
795
+
796
+ effect(() => {
797
+ let val = read(a);
798
+
799
+ if (val > 0) {
800
+ write(b, val * 2);
801
+ }
802
+ });
803
+
804
+ effect(() => {
805
+ let val = read(b);
806
+
807
+ if (val > 0) {
808
+ write(c, val * 3);
809
+ }
810
+ });
811
+
812
+ effect(() => {
813
+ cValues.push(read(c));
814
+ });
815
+
816
+ write(a, 5);
817
+ await Promise.resolve();
818
+ await Promise.resolve();
819
+ await Promise.resolve();
820
+
821
+ expect(cValues).toEqual([0, 30]);
822
+ });
823
+
824
+ it('computed that throws on update retains previous value', async () => {
825
+ let s = signal(0),
826
+ effectValues: number[] = [],
827
+ c = computed(() => {
828
+ let val = read(s);
829
+
830
+ if (val === 2) {
831
+ throw new Error('boom');
832
+ }
833
+
834
+ return val * 10;
835
+ });
836
+
837
+ effect(() => {
838
+ effectValues.push(read(c));
839
+ });
840
+
841
+ expect(effectValues).toEqual([0]);
842
+
843
+ write(s, 1);
844
+ await Promise.resolve();
845
+
846
+ expect(effectValues).toEqual([0, 10]);
847
+ expect(read(c)).toBe(10);
848
+
849
+ write(s, 2);
850
+ await Promise.resolve();
851
+
852
+ // Value should remain 10 since throw prevented update
853
+ expect(read(c)).toBe(10);
854
+ expect(effectValues).toEqual([0, 10]);
855
+ });
856
+
857
+ it('computed alternates between throwing and succeeding', async () => {
858
+ let s = signal(0),
859
+ effectValues: number[] = [],
860
+ c = computed(() => {
861
+ let val = read(s);
862
+
863
+ if (val % 2 !== 0) {
864
+ throw new Error('odd');
865
+ }
866
+
867
+ return val;
868
+ });
869
+
870
+ effect(() => {
871
+ effectValues.push(read(c));
872
+ });
873
+
874
+ expect(effectValues).toEqual([0]);
875
+
876
+ write(s, 1);
877
+ await Promise.resolve();
878
+
879
+ // Threw on odd, value stays 0
880
+ expect(read(c)).toBe(0);
881
+ expect(effectValues).toEqual([0]);
882
+
883
+ write(s, 2);
884
+ await Promise.resolve();
885
+
886
+ // Succeeds on even, value updates
887
+ expect(read(c)).toBe(2);
888
+ expect(effectValues).toEqual([0, 2]);
889
+
890
+ write(s, 3);
891
+ await Promise.resolve();
892
+
893
+ // Threw on odd again, value stays 2
894
+ expect(read(c)).toBe(2);
895
+ expect(effectValues).toEqual([0, 2]);
896
+
897
+ write(s, 4);
898
+ await Promise.resolve();
899
+
900
+ // Succeeds again
901
+ expect(read(c)).toBe(4);
902
+ expect(effectValues).toEqual([0, 2, 4]);
903
+ });
904
+
905
+ it('heap auto-resizes for computed chain deeper than 64', async () => {
906
+ let s = signal(0),
907
+ chain: ReturnType<typeof computed>[] = [computed(() => read(s) + 1)];
908
+
909
+ for (let i = 1; i < 80; i++) {
910
+ let prev = chain[i - 1];
911
+
912
+ chain.push(computed(() => read(prev) + 1));
913
+ }
914
+
915
+ let tail = chain[chain.length - 1],
916
+ result = -1;
917
+
918
+ effect(() => {
919
+ result = read(tail);
920
+ });
921
+
922
+ expect(result).toBe(80);
923
+
924
+ write(s, 10);
925
+ await Promise.resolve();
926
+
927
+ expect(result).toBe(90);
928
+ });
929
+
930
+ it('system remains functional under high effect churn', async () => {
931
+ let s = signal(0),
932
+ stops: (() => void)[] = [];
933
+
934
+ for (let i = 0; i < 200; i++) {
935
+ stops.push(effect(() => { read(s); }));
936
+ }
937
+
938
+ for (let i = 0, n = stops.length; i < n; i++) {
939
+ stops[i]();
940
+ }
941
+
942
+ stops.length = 0;
943
+
944
+ let result = -1;
945
+
946
+ effect(() => {
947
+ result = read(s);
948
+ });
949
+
950
+ write(s, 42);
951
+ await Promise.resolve();
952
+
953
+ expect(result).toBe(42);
954
+
955
+ for (let i = 0; i < 200; i++) {
956
+ stops.push(effect(() => { read(s); }));
957
+ }
958
+
959
+ for (let i = 0, n = stops.length; i < n; i++) {
960
+ stops[i]();
961
+ }
962
+
963
+ write(s, 99);
964
+ await Promise.resolve();
965
+
966
+ expect(result).toBe(99);
967
+ });
968
+
969
+ it('link pool handles >1000 dependencies with disposal and reuse', async () => {
970
+ let signals: ReturnType<typeof signal>[] = [],
971
+ stops: (() => void)[] = [];
972
+
973
+ for (let i = 0; i < 1100; i++) {
974
+ signals.push(signal(i));
975
+ }
976
+
977
+ // Create effect reading all 1100 signals
978
+ stops.push(effect(() => {
979
+ for (let i = 0, n = signals.length; i < n; i++) {
980
+ read(signals[i]);
981
+ }
982
+ }));
983
+
984
+ // Dispose to return links to pool
985
+ stops[0]();
986
+ stops.length = 0;
987
+
988
+ // Create new effects reusing pooled links
989
+ let sum = -1;
990
+
991
+ stops.push(effect(() => {
992
+ let total = 0;
993
+
994
+ for (let i = 0; i < 50; i++) {
995
+ total += read(signals[i]);
996
+ }
997
+
998
+ sum = total;
999
+ }));
1000
+
1001
+ // sum of 0..49 = 1225
1002
+ expect(sum).toBe(1225);
1003
+
1004
+ write(signals[0], 100);
1005
+ await Promise.resolve();
1006
+
1007
+ // 1225 - 0 + 100 = 1325
1008
+ expect(sum).toBe(1325);
1009
+
1010
+ for (let i = 0, n = stops.length; i < n; i++) {
1011
+ stops[i]();
1012
+ }
1013
+ });
655
1014
  });
@@ -0,0 +1,17 @@
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "compilerOptions": {
4
+ "baseUrl": "..",
5
+ "noEmit": true,
6
+ "paths": {
7
+ "~/*": [
8
+ "src/*"
9
+ ]
10
+ },
11
+ "rootDir": ".."
12
+ },
13
+ "include": [
14
+ "./**/*",
15
+ "../src/**/*"
16
+ ]
17
+ }