@zeix/cause-effect 0.15.1 → 0.16.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/.ai-context.md +254 -0
- package/.cursorrules +54 -0
- package/.github/copilot-instructions.md +132 -0
- package/CLAUDE.md +319 -0
- package/README.md +167 -159
- package/eslint.config.js +1 -1
- package/index.dev.js +528 -407
- package/index.js +1 -1
- package/index.ts +36 -25
- package/package.json +1 -1
- package/src/computed.ts +41 -30
- package/src/diff.ts +57 -44
- package/src/effect.ts +15 -16
- package/src/errors.ts +64 -0
- package/src/match.ts +2 -2
- package/src/resolve.ts +2 -2
- package/src/signal.ts +27 -49
- package/src/state.ts +27 -19
- package/src/store.ts +410 -209
- package/src/system.ts +122 -0
- package/src/util.ts +45 -6
- package/test/batch.test.ts +18 -11
- package/test/benchmark.test.ts +4 -4
- package/test/computed.test.ts +508 -72
- package/test/diff.test.ts +321 -4
- package/test/effect.test.ts +61 -61
- package/test/match.test.ts +38 -28
- package/test/resolve.test.ts +16 -16
- package/test/signal.test.ts +19 -147
- package/test/state.test.ts +212 -25
- package/test/store.test.ts +1370 -134
- package/test/util/dependency-graph.ts +1 -1
- package/types/index.d.ts +10 -9
- package/types/src/collection.d.ts +26 -0
- package/types/src/computed.d.ts +9 -9
- package/types/src/diff.d.ts +5 -3
- package/types/src/effect.d.ts +3 -3
- package/types/src/errors.d.ts +22 -0
- package/types/src/match.d.ts +1 -1
- package/types/src/resolve.d.ts +1 -1
- package/types/src/signal.d.ts +12 -19
- package/types/src/state.d.ts +5 -5
- package/types/src/store.d.ts +40 -36
- package/types/src/system.d.ts +44 -0
- package/types/src/util.d.ts +7 -5
- package/index.d.ts +0 -36
- package/src/scheduler.ts +0 -172
- package/types/test-new-effect.d.ts +0 -1
package/test/diff.test.ts
CHANGED
|
@@ -23,7 +23,7 @@ describe('diff', () => {
|
|
|
23
23
|
test('should detect additions', () => {
|
|
24
24
|
const obj1 = { a: 1 }
|
|
25
25
|
const obj2 = { a: 1, b: 'new' }
|
|
26
|
-
const result = diff(obj1
|
|
26
|
+
const result = diff<{ a: number; b?: string }>(obj1, obj2)
|
|
27
27
|
|
|
28
28
|
expect(result.changed).toBe(true)
|
|
29
29
|
expect(result.add).toEqual({ b: 'new' })
|
|
@@ -34,7 +34,7 @@ describe('diff', () => {
|
|
|
34
34
|
test('should detect removals', () => {
|
|
35
35
|
const obj1 = { a: 1, b: 'hello' }
|
|
36
36
|
const obj2 = { a: 1 }
|
|
37
|
-
const result = diff(obj1
|
|
37
|
+
const result = diff<{ a: number; b?: string }>(obj1, obj2)
|
|
38
38
|
|
|
39
39
|
expect(result.changed).toBe(true)
|
|
40
40
|
expect(Object.keys(result.add)).toHaveLength(0)
|
|
@@ -56,7 +56,12 @@ describe('diff', () => {
|
|
|
56
56
|
test('should detect multiple changes', () => {
|
|
57
57
|
const obj1 = { a: 1, b: 'hello', c: true }
|
|
58
58
|
const obj2 = { a: 2, d: 'new', c: true }
|
|
59
|
-
const result = diff
|
|
59
|
+
const result = diff<{
|
|
60
|
+
a: number
|
|
61
|
+
b?: string
|
|
62
|
+
c: boolean
|
|
63
|
+
d?: string
|
|
64
|
+
}>(obj1, obj2)
|
|
60
65
|
|
|
61
66
|
expect(result.changed).toBe(true)
|
|
62
67
|
expect(result.add).toEqual({ d: 'new' })
|
|
@@ -212,7 +217,10 @@ describe('diff', () => {
|
|
|
212
217
|
test('should handle changes from primitive to object', () => {
|
|
213
218
|
const obj1 = { value: 'string' }
|
|
214
219
|
const obj2 = { value: { type: 'object' } }
|
|
215
|
-
const result = diff
|
|
220
|
+
const result = diff<{ value: string | { type: string } }>(
|
|
221
|
+
obj1,
|
|
222
|
+
obj2,
|
|
223
|
+
)
|
|
216
224
|
|
|
217
225
|
expect(result.changed).toBe(true)
|
|
218
226
|
expect(result.change).toEqual({ value: { type: 'object' } })
|
|
@@ -635,4 +643,313 @@ describe('diff', () => {
|
|
|
635
643
|
})
|
|
636
644
|
})
|
|
637
645
|
})
|
|
646
|
+
|
|
647
|
+
describe('non-plain object type safety', () => {
|
|
648
|
+
test('should handle Symbol objects without throwing TypeError', () => {
|
|
649
|
+
const symbol = Symbol('test')
|
|
650
|
+
const obj = { a: 1 }
|
|
651
|
+
|
|
652
|
+
// These should not throw after we fix the bug
|
|
653
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
654
|
+
expect(() => diff(symbol, obj)).not.toThrow()
|
|
655
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
656
|
+
expect(() => diff(obj, symbol)).not.toThrow()
|
|
657
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
658
|
+
expect(() => isEqual(symbol, obj)).not.toThrow()
|
|
659
|
+
})
|
|
660
|
+
|
|
661
|
+
test('should report additions when diffing from Symbol to valid object', () => {
|
|
662
|
+
const symbol = Symbol('test')
|
|
663
|
+
const obj = { a: 1, b: 'hello' }
|
|
664
|
+
|
|
665
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
666
|
+
const result = diff(symbol, obj)
|
|
667
|
+
|
|
668
|
+
expect(result.changed).toBe(true)
|
|
669
|
+
expect(result.add).toEqual({ a: 1, b: 'hello' })
|
|
670
|
+
expect(result.change).toEqual({})
|
|
671
|
+
expect(result.remove).toEqual({})
|
|
672
|
+
})
|
|
673
|
+
|
|
674
|
+
test('should report removals when diffing from valid object to Symbol', () => {
|
|
675
|
+
const obj = { a: 1, b: 'hello' }
|
|
676
|
+
const symbol = Symbol('test')
|
|
677
|
+
|
|
678
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
679
|
+
const result = diff(obj, symbol)
|
|
680
|
+
|
|
681
|
+
expect(result.changed).toBe(true)
|
|
682
|
+
expect(result.add).toEqual({})
|
|
683
|
+
expect(result.change).toEqual({})
|
|
684
|
+
expect(result.remove).toEqual({ a: 1, b: 'hello' })
|
|
685
|
+
})
|
|
686
|
+
|
|
687
|
+
test('should handle Symbol to Symbol diff with no changes', () => {
|
|
688
|
+
const symbol = Symbol('test')
|
|
689
|
+
|
|
690
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
691
|
+
const result = diff(symbol, symbol)
|
|
692
|
+
|
|
693
|
+
expect(result.changed).toBe(false)
|
|
694
|
+
expect(result.add).toEqual({})
|
|
695
|
+
expect(result.change).toEqual({})
|
|
696
|
+
expect(result.remove).toEqual({})
|
|
697
|
+
})
|
|
698
|
+
|
|
699
|
+
test('should handle different Symbols as changed', () => {
|
|
700
|
+
const symbol1 = Symbol('test1')
|
|
701
|
+
const symbol2 = Symbol('test2')
|
|
702
|
+
|
|
703
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
704
|
+
const result = diff(symbol1, symbol2)
|
|
705
|
+
|
|
706
|
+
expect(result.changed).toBe(true)
|
|
707
|
+
expect(result.add).toEqual({})
|
|
708
|
+
expect(result.change).toEqual({})
|
|
709
|
+
expect(result.remove).toEqual({})
|
|
710
|
+
})
|
|
711
|
+
|
|
712
|
+
test('should handle Date objects without throwing TypeError', () => {
|
|
713
|
+
const date = new Date('2023-01-01')
|
|
714
|
+
const obj = { a: 1 }
|
|
715
|
+
|
|
716
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
717
|
+
expect(() => diff(date, obj)).not.toThrow()
|
|
718
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
719
|
+
expect(() => diff(obj, date)).not.toThrow()
|
|
720
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
721
|
+
expect(() => isEqual(date, obj)).not.toThrow()
|
|
722
|
+
})
|
|
723
|
+
|
|
724
|
+
test('should report additions when diffing from Date to valid object', () => {
|
|
725
|
+
const date = new Date('2023-01-01')
|
|
726
|
+
const obj = { a: 1, b: 'hello' }
|
|
727
|
+
|
|
728
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
729
|
+
const result = diff(date, obj)
|
|
730
|
+
|
|
731
|
+
expect(result.changed).toBe(true)
|
|
732
|
+
expect(result.add).toEqual({ a: 1, b: 'hello' })
|
|
733
|
+
expect(result.change).toEqual({})
|
|
734
|
+
expect(result.remove).toEqual({})
|
|
735
|
+
})
|
|
736
|
+
|
|
737
|
+
test('should report removals when diffing from valid object to Date', () => {
|
|
738
|
+
const obj = { a: 1, b: 'hello' }
|
|
739
|
+
const date = new Date('2023-01-01')
|
|
740
|
+
|
|
741
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
742
|
+
const result = diff(obj, date)
|
|
743
|
+
|
|
744
|
+
expect(result.changed).toBe(true)
|
|
745
|
+
expect(result.add).toEqual({})
|
|
746
|
+
expect(result.change).toEqual({})
|
|
747
|
+
expect(result.remove).toEqual({ a: 1, b: 'hello' })
|
|
748
|
+
})
|
|
749
|
+
|
|
750
|
+
test('should handle Map objects without throwing TypeError', () => {
|
|
751
|
+
const map = new Map([['key', 'value']])
|
|
752
|
+
const obj = { a: 1 }
|
|
753
|
+
|
|
754
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
755
|
+
expect(() => diff(map, obj)).not.toThrow()
|
|
756
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
757
|
+
expect(() => diff(obj, map)).not.toThrow()
|
|
758
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
759
|
+
expect(() => isEqual(map, obj)).not.toThrow()
|
|
760
|
+
})
|
|
761
|
+
|
|
762
|
+
test('should report additions when diffing from Map to valid object', () => {
|
|
763
|
+
const map = new Map([['key', 'value']])
|
|
764
|
+
const obj = { x: 10, y: 20 }
|
|
765
|
+
|
|
766
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
767
|
+
const result = diff(map, obj)
|
|
768
|
+
|
|
769
|
+
expect(result.changed).toBe(true)
|
|
770
|
+
expect(result.add).toEqual({ x: 10, y: 20 })
|
|
771
|
+
expect(result.change).toEqual({})
|
|
772
|
+
expect(result.remove).toEqual({})
|
|
773
|
+
})
|
|
774
|
+
|
|
775
|
+
test('should handle Set objects without throwing TypeError', () => {
|
|
776
|
+
const set = new Set([1, 2, 3])
|
|
777
|
+
const obj = { a: 1 }
|
|
778
|
+
|
|
779
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
780
|
+
expect(() => diff(set, obj)).not.toThrow()
|
|
781
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
782
|
+
expect(() => diff(obj, set)).not.toThrow()
|
|
783
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
784
|
+
expect(() => isEqual(set, obj)).not.toThrow()
|
|
785
|
+
})
|
|
786
|
+
|
|
787
|
+
test('should handle Promise objects without throwing TypeError', () => {
|
|
788
|
+
const promise = Promise.resolve('test')
|
|
789
|
+
const obj = { a: 1 }
|
|
790
|
+
|
|
791
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
792
|
+
expect(() => diff(promise, obj)).not.toThrow()
|
|
793
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
794
|
+
expect(() => diff(obj, promise)).not.toThrow()
|
|
795
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
796
|
+
expect(() => isEqual(promise, obj)).not.toThrow()
|
|
797
|
+
})
|
|
798
|
+
|
|
799
|
+
test('should handle RegExp objects without throwing TypeError', () => {
|
|
800
|
+
const regex = /test/g
|
|
801
|
+
const obj = { a: 1 }
|
|
802
|
+
|
|
803
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
804
|
+
expect(() => diff(regex, obj)).not.toThrow()
|
|
805
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
806
|
+
expect(() => diff(obj, regex)).not.toThrow()
|
|
807
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
808
|
+
expect(() => isEqual(regex, obj)).not.toThrow()
|
|
809
|
+
})
|
|
810
|
+
|
|
811
|
+
test('should handle Function objects without throwing TypeError', () => {
|
|
812
|
+
const func = () => 'test'
|
|
813
|
+
const obj = { a: 1 }
|
|
814
|
+
|
|
815
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
816
|
+
expect(() => diff(func, obj)).not.toThrow()
|
|
817
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
818
|
+
expect(() => diff(obj, func)).not.toThrow()
|
|
819
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
820
|
+
expect(() => isEqual(func, obj)).not.toThrow()
|
|
821
|
+
})
|
|
822
|
+
|
|
823
|
+
test('should handle Error objects without throwing TypeError', () => {
|
|
824
|
+
const error = new Error('test error')
|
|
825
|
+
const obj = { a: 1 }
|
|
826
|
+
|
|
827
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
828
|
+
expect(() => diff(error, obj)).not.toThrow()
|
|
829
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
830
|
+
expect(() => diff(obj, error)).not.toThrow()
|
|
831
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
832
|
+
expect(() => isEqual(error, obj)).not.toThrow()
|
|
833
|
+
})
|
|
834
|
+
|
|
835
|
+
test('should handle WeakMap objects without throwing TypeError', () => {
|
|
836
|
+
const weakMap = new WeakMap()
|
|
837
|
+
const obj = { a: 1 }
|
|
838
|
+
|
|
839
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
840
|
+
expect(() => diff(weakMap, obj)).not.toThrow()
|
|
841
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
842
|
+
expect(() => diff(obj, weakMap)).not.toThrow()
|
|
843
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
844
|
+
expect(() => isEqual(weakMap, obj)).not.toThrow()
|
|
845
|
+
})
|
|
846
|
+
|
|
847
|
+
test('should handle WeakSet objects without throwing TypeError', () => {
|
|
848
|
+
const weakSet = new WeakSet()
|
|
849
|
+
const obj = { a: 1 }
|
|
850
|
+
|
|
851
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
852
|
+
expect(() => diff(weakSet, obj)).not.toThrow()
|
|
853
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
854
|
+
expect(() => diff(obj, weakSet)).not.toThrow()
|
|
855
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
856
|
+
expect(() => isEqual(weakSet, obj)).not.toThrow()
|
|
857
|
+
})
|
|
858
|
+
|
|
859
|
+
test('should handle ArrayBuffer objects without throwing TypeError', () => {
|
|
860
|
+
const buffer = new ArrayBuffer(8)
|
|
861
|
+
const obj = { a: 1 }
|
|
862
|
+
|
|
863
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
864
|
+
expect(() => diff(buffer, obj)).not.toThrow()
|
|
865
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
866
|
+
expect(() => diff(obj, buffer)).not.toThrow()
|
|
867
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
868
|
+
expect(() => isEqual(buffer, obj)).not.toThrow()
|
|
869
|
+
})
|
|
870
|
+
|
|
871
|
+
test('should handle class instances without throwing TypeError', () => {
|
|
872
|
+
class TestClass {
|
|
873
|
+
constructor(public value: string) {}
|
|
874
|
+
}
|
|
875
|
+
const instance = new TestClass('test')
|
|
876
|
+
const obj = { a: 1 }
|
|
877
|
+
|
|
878
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
879
|
+
expect(() => diff(instance, obj)).not.toThrow()
|
|
880
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
881
|
+
expect(() => diff(obj, instance)).not.toThrow()
|
|
882
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
883
|
+
expect(() => isEqual(instance, obj)).not.toThrow()
|
|
884
|
+
})
|
|
885
|
+
|
|
886
|
+
test('should report additions/removals with mixed valid and invalid objects', () => {
|
|
887
|
+
const func = () => 'test'
|
|
888
|
+
const obj1 = { a: 1 }
|
|
889
|
+
const obj2 = { b: 2 }
|
|
890
|
+
|
|
891
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
892
|
+
const result1 = diff(func, obj1)
|
|
893
|
+
expect(result1.changed).toBe(true)
|
|
894
|
+
expect(result1.add).toEqual({ a: 1 })
|
|
895
|
+
expect(result1.remove).toEqual({})
|
|
896
|
+
|
|
897
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
898
|
+
const result2 = diff(obj2, func)
|
|
899
|
+
expect(result2.changed).toBe(true)
|
|
900
|
+
expect(result2.add).toEqual({})
|
|
901
|
+
expect(result2.remove).toEqual({ b: 2 })
|
|
902
|
+
|
|
903
|
+
// @ts-expect-error Testing runtime behavior with non-plain object types
|
|
904
|
+
const result3 = diff(func, func)
|
|
905
|
+
expect(result3.changed).toBe(false)
|
|
906
|
+
expect(result3.add).toEqual({})
|
|
907
|
+
expect(result3.remove).toEqual({})
|
|
908
|
+
})
|
|
909
|
+
})
|
|
910
|
+
})
|
|
911
|
+
|
|
912
|
+
describe('sparse array handling', () => {
|
|
913
|
+
test('should properly diff sparse array representations', () => {
|
|
914
|
+
// Simulate what happens in store: sparse array [10, 30, 50] with keys ["0", "2", "4"]
|
|
915
|
+
// is represented as a regular array [10, 30, 50] when passed to diff()
|
|
916
|
+
const oldSparseArray: Record<number, number> = [10, 30, 50] // What current() returns for sparse store
|
|
917
|
+
const newDenseArray: Record<number, number> = [100, 200, 300] // What user wants to set
|
|
918
|
+
|
|
919
|
+
const result = diff(oldSparseArray, newDenseArray)
|
|
920
|
+
|
|
921
|
+
// The problem: diff sees this as simple value changes at indices 0, 1, 2
|
|
922
|
+
// But the store actually has sparse keys "0", "2", "4"
|
|
923
|
+
// So when reconcile tries to apply changes, only indices 0 and 2 work
|
|
924
|
+
expect(result.change).toEqual({
|
|
925
|
+
'0': 100, // This works (key "0" exists)
|
|
926
|
+
'1': 200, // This fails (key "1" doesn't exist in sparse structure)
|
|
927
|
+
'2': 300, // This works (key "2" exists)
|
|
928
|
+
})
|
|
929
|
+
expect(result.add).toEqual({})
|
|
930
|
+
expect(result.remove).toEqual({})
|
|
931
|
+
expect(result.changed).toBe(true)
|
|
932
|
+
})
|
|
933
|
+
|
|
934
|
+
test('should handle array-to-object conversion when context suggests sparse structure', () => {
|
|
935
|
+
// This test demonstrates the core issue: we need context about the original structure
|
|
936
|
+
// to properly handle sparse array replacement
|
|
937
|
+
const oldSparseAsObject = { '0': 10, '2': 30, '4': 50 } // Actual sparse structure
|
|
938
|
+
const newDenseArray: Record<number, number> = [100, 200, 300] // User input
|
|
939
|
+
|
|
940
|
+
const result = diff(oldSparseAsObject, newDenseArray)
|
|
941
|
+
|
|
942
|
+
// This should remove old sparse keys and add new dense keys
|
|
943
|
+
expect(result.remove).toEqual({
|
|
944
|
+
'4': UNSET, // Key "4" should be removed (key "2" gets reused)
|
|
945
|
+
})
|
|
946
|
+
expect(result.add).toEqual({
|
|
947
|
+
'1': 200, // Key "1" should be added
|
|
948
|
+
})
|
|
949
|
+
expect(result.change).toEqual({
|
|
950
|
+
'0': 100, // Key "0" changes value from 10 to 100
|
|
951
|
+
'2': 300, // Key "2" changes value from 30 to 300
|
|
952
|
+
})
|
|
953
|
+
expect(result.changed).toBe(true)
|
|
954
|
+
})
|
|
638
955
|
})
|