@zeix/cause-effect 0.17.2 → 0.17.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ai-context.md +11 -5
- package/.github/copilot-instructions.md +1 -1
- package/.zed/settings.json +3 -0
- package/CLAUDE.md +18 -79
- package/README.md +23 -37
- package/archive/benchmark.ts +0 -5
- package/archive/collection.ts +5 -62
- package/archive/composite.ts +85 -0
- package/archive/computed.ts +17 -20
- package/archive/list.ts +6 -67
- package/archive/memo.ts +13 -14
- package/archive/store.ts +7 -66
- package/archive/task.ts +18 -20
- package/index.dev.js +438 -614
- package/index.js +1 -1
- package/index.ts +8 -19
- package/package.json +6 -6
- package/src/classes/collection.ts +59 -112
- package/src/classes/computed.ts +146 -189
- package/src/classes/list.ts +138 -105
- package/src/classes/ref.ts +16 -42
- package/src/classes/state.ts +16 -45
- package/src/classes/store.ts +107 -72
- package/src/effect.ts +9 -12
- package/src/errors.ts +12 -8
- package/src/signal.ts +3 -1
- package/src/system.ts +136 -154
- package/test/batch.test.ts +4 -11
- package/test/benchmark.test.ts +4 -2
- package/test/collection.test.ts +46 -306
- package/test/computed.test.ts +205 -223
- package/test/list.test.ts +35 -303
- package/test/ref.test.ts +38 -66
- package/test/state.test.ts +6 -12
- package/test/store.test.ts +37 -489
- package/test/util/dependency-graph.ts +2 -2
- package/tsconfig.build.json +11 -0
- package/tsconfig.json +5 -7
- package/types/index.d.ts +2 -2
- package/types/src/classes/collection.d.ts +4 -6
- package/types/src/classes/computed.d.ts +17 -37
- package/types/src/classes/list.d.ts +8 -6
- package/types/src/classes/ref.d.ts +7 -20
- package/types/src/classes/state.d.ts +5 -17
- package/types/src/classes/store.d.ts +12 -11
- package/types/src/errors.d.ts +2 -4
- package/types/src/signal.d.ts +3 -2
- package/types/src/system.d.ts +41 -44
- package/src/classes/composite.ts +0 -171
- package/types/src/classes/composite.d.ts +0 -15
package/test/list.test.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
2
|
import {
|
|
3
3
|
createEffect,
|
|
4
|
-
List,
|
|
5
4
|
createStore,
|
|
6
5
|
isCollection,
|
|
7
6
|
isList,
|
|
8
7
|
isStore,
|
|
8
|
+
List,
|
|
9
9
|
Memo,
|
|
10
10
|
State,
|
|
11
11
|
UNSET,
|
|
@@ -130,17 +130,6 @@ describe('list', () => {
|
|
|
130
130
|
expect(names.get()).toEqual(['Alice', 'Bob', 'Charlie'])
|
|
131
131
|
})
|
|
132
132
|
|
|
133
|
-
test('triggers HOOK_SORT with new order', () => {
|
|
134
|
-
const numbers = new List([3, 1, 2])
|
|
135
|
-
let order: readonly string[] | undefined
|
|
136
|
-
numbers.on('sort', sort => {
|
|
137
|
-
order = sort
|
|
138
|
-
})
|
|
139
|
-
numbers.sort()
|
|
140
|
-
expect(order).toHaveLength(3)
|
|
141
|
-
expect(order).toEqual(['1', '2', '0'])
|
|
142
|
-
})
|
|
143
|
-
|
|
144
133
|
test('sort is reactive - watchers are notified', () => {
|
|
145
134
|
const numbers = new List([3, 1, 2])
|
|
146
135
|
let effectCount = 0
|
|
@@ -302,47 +291,6 @@ describe('list', () => {
|
|
|
302
291
|
})
|
|
303
292
|
})
|
|
304
293
|
|
|
305
|
-
describe('Hooks', () => {
|
|
306
|
-
test('trigger HOOK_ADD when adding items', () => {
|
|
307
|
-
const numbers = new List([1, 2])
|
|
308
|
-
let addedKeys: readonly string[] | undefined
|
|
309
|
-
let newArray: number[] = []
|
|
310
|
-
numbers.on('add', add => {
|
|
311
|
-
addedKeys = add
|
|
312
|
-
newArray = numbers.get()
|
|
313
|
-
})
|
|
314
|
-
numbers.add(3)
|
|
315
|
-
expect(addedKeys).toHaveLength(1)
|
|
316
|
-
expect(newArray).toEqual([1, 2, 3])
|
|
317
|
-
})
|
|
318
|
-
|
|
319
|
-
test('triggers HOOK_CHANGE when properties are modified', () => {
|
|
320
|
-
const items = new List([{ value: 10 }])
|
|
321
|
-
let changedKeys: readonly string[] | undefined
|
|
322
|
-
let newArray: { value: number }[] = []
|
|
323
|
-
items.on('change', change => {
|
|
324
|
-
changedKeys = change
|
|
325
|
-
newArray = items.get()
|
|
326
|
-
})
|
|
327
|
-
items.at(0)?.set({ value: 20 })
|
|
328
|
-
expect(changedKeys).toHaveLength(1)
|
|
329
|
-
expect(newArray).toEqual([{ value: 20 }])
|
|
330
|
-
})
|
|
331
|
-
|
|
332
|
-
test('triggers HOOK_REMOVE when items are removed', () => {
|
|
333
|
-
const items = new List([1, 2, 3])
|
|
334
|
-
let removedKeys: readonly string[] | undefined
|
|
335
|
-
let newArray: number[] = []
|
|
336
|
-
items.on('remove', remove => {
|
|
337
|
-
removedKeys = remove
|
|
338
|
-
newArray = items.get()
|
|
339
|
-
})
|
|
340
|
-
items.remove(1)
|
|
341
|
-
expect(removedKeys).toHaveLength(1)
|
|
342
|
-
expect(newArray).toEqual([1, 3])
|
|
343
|
-
})
|
|
344
|
-
})
|
|
345
|
-
|
|
346
294
|
describe('edge cases', () => {
|
|
347
295
|
test('handles empty lists correctly', () => {
|
|
348
296
|
const empty = new List([])
|
|
@@ -482,7 +430,10 @@ describe('list', () => {
|
|
|
482
430
|
])
|
|
483
431
|
|
|
484
432
|
const enrichedUsers = users.deriveCollection(
|
|
485
|
-
async (
|
|
433
|
+
async (
|
|
434
|
+
user: { id: number; name: string },
|
|
435
|
+
abort: AbortSignal,
|
|
436
|
+
) => {
|
|
486
437
|
// Simulate API call
|
|
487
438
|
await new Promise(resolve => setTimeout(resolve, 10))
|
|
488
439
|
if (abort.aborted) throw new Error('Aborted')
|
|
@@ -663,54 +614,6 @@ describe('list', () => {
|
|
|
663
614
|
})
|
|
664
615
|
})
|
|
665
616
|
|
|
666
|
-
describe('collection event handling', () => {
|
|
667
|
-
test('emits add events when source adds items', () => {
|
|
668
|
-
const numbers = new List([1, 2])
|
|
669
|
-
const doubled = numbers.deriveCollection(
|
|
670
|
-
(value: number) => value * 2,
|
|
671
|
-
)
|
|
672
|
-
|
|
673
|
-
let addedKeys: readonly string[] | undefined
|
|
674
|
-
doubled.on('add', keys => {
|
|
675
|
-
addedKeys = keys
|
|
676
|
-
})
|
|
677
|
-
|
|
678
|
-
numbers.add(3)
|
|
679
|
-
expect(addedKeys).toHaveLength(1)
|
|
680
|
-
})
|
|
681
|
-
|
|
682
|
-
test('emits remove events when source removes items', () => {
|
|
683
|
-
const numbers = new List([1, 2, 3])
|
|
684
|
-
const doubled = numbers.deriveCollection(
|
|
685
|
-
(value: number) => value * 2,
|
|
686
|
-
)
|
|
687
|
-
|
|
688
|
-
let removedKeys: readonly string[] | undefined
|
|
689
|
-
doubled.on('remove', keys => {
|
|
690
|
-
removedKeys = keys
|
|
691
|
-
})
|
|
692
|
-
|
|
693
|
-
numbers.remove(1)
|
|
694
|
-
expect(removedKeys).toHaveLength(1)
|
|
695
|
-
})
|
|
696
|
-
|
|
697
|
-
test('emits sort events when source is sorted', () => {
|
|
698
|
-
const numbers = new List([3, 1, 2])
|
|
699
|
-
const doubled = numbers.deriveCollection(
|
|
700
|
-
(value: number) => value * 2,
|
|
701
|
-
)
|
|
702
|
-
|
|
703
|
-
let sortedKeys: readonly string[] | undefined
|
|
704
|
-
doubled.on('sort', keys => {
|
|
705
|
-
sortedKeys = keys
|
|
706
|
-
})
|
|
707
|
-
|
|
708
|
-
numbers.sort()
|
|
709
|
-
expect(sortedKeys).toHaveLength(3)
|
|
710
|
-
expect(doubled.get()).toEqual([2, 4, 6]) // Sorted and doubled
|
|
711
|
-
})
|
|
712
|
-
})
|
|
713
|
-
|
|
714
617
|
describe('edge cases', () => {
|
|
715
618
|
test('handles empty list derivation', () => {
|
|
716
619
|
const empty = new List<number>([])
|
|
@@ -752,231 +655,60 @@ describe('list', () => {
|
|
|
752
655
|
})
|
|
753
656
|
})
|
|
754
657
|
|
|
755
|
-
describe('
|
|
756
|
-
test('List
|
|
757
|
-
|
|
758
|
-
let
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
}
|
|
658
|
+
describe('Watch Callbacks', () => {
|
|
659
|
+
test('List watched callback is called when effect accesses list.get()', () => {
|
|
660
|
+
let linkWatchedCalled = false
|
|
661
|
+
let listUnwatchedCalled = false
|
|
662
|
+
const numbers = new List([10, 20, 30], {
|
|
663
|
+
watched: () => {
|
|
664
|
+
linkWatchedCalled = true
|
|
665
|
+
},
|
|
666
|
+
unwatched: () => {
|
|
667
|
+
listUnwatchedCalled = true
|
|
668
|
+
},
|
|
767
669
|
})
|
|
768
670
|
|
|
769
|
-
expect(
|
|
671
|
+
expect(linkWatchedCalled).toBe(false)
|
|
770
672
|
|
|
771
|
-
// Access list via list.get() - this should trigger list's
|
|
673
|
+
// Access list via list.get() - this should trigger list's watched callback
|
|
772
674
|
let effectValue: number[] = []
|
|
773
675
|
const cleanup = createEffect(() => {
|
|
774
676
|
effectValue = numbers.get()
|
|
775
677
|
})
|
|
776
678
|
|
|
777
|
-
expect(
|
|
679
|
+
expect(linkWatchedCalled).toBe(true)
|
|
778
680
|
expect(effectValue).toEqual([10, 20, 30])
|
|
779
|
-
expect(
|
|
681
|
+
expect(listUnwatchedCalled).toBe(false)
|
|
780
682
|
|
|
781
683
|
// Cleanup effect - should trigger unwatch
|
|
782
684
|
cleanup()
|
|
783
|
-
expect(
|
|
685
|
+
expect(listUnwatchedCalled).toBe(true)
|
|
784
686
|
})
|
|
785
687
|
|
|
786
|
-
test('
|
|
787
|
-
|
|
788
|
-
let
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
return () => {
|
|
797
|
-
firstItemUnwatchCalled = true
|
|
798
|
-
}
|
|
688
|
+
test('List length access triggers List watched callback', () => {
|
|
689
|
+
let listWatchedCalled = false
|
|
690
|
+
let listUnwatchedCalled = false
|
|
691
|
+
const numbers = new List([1, 2, 3], {
|
|
692
|
+
watched: () => {
|
|
693
|
+
listWatchedCalled = true
|
|
694
|
+
},
|
|
695
|
+
unwatched: () => {
|
|
696
|
+
listUnwatchedCalled = true
|
|
697
|
+
},
|
|
799
698
|
})
|
|
800
699
|
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
// Access first item via signal.get() - this should trigger the State signal's HOOK_WATCH
|
|
804
|
-
let effectValue: string | undefined
|
|
805
|
-
const cleanup = createEffect(() => {
|
|
806
|
-
effectValue = firstItemSignal.get()
|
|
807
|
-
})
|
|
808
|
-
|
|
809
|
-
expect(firstItemHookCalled).toBe(true)
|
|
810
|
-
expect(effectValue).toBe('first')
|
|
811
|
-
expect(firstItemUnwatchCalled).toBe(false)
|
|
812
|
-
|
|
813
|
-
// Cleanup effect - should trigger State signal's unwatch
|
|
814
|
-
cleanup()
|
|
815
|
-
expect(firstItemUnwatchCalled).toBe(true)
|
|
816
|
-
})
|
|
817
|
-
|
|
818
|
-
test('State signal unwatch is called when item gets removed from list', () => {
|
|
819
|
-
const items = new List(['first', 'second'])
|
|
820
|
-
let firstItemUnwatchCalled = false
|
|
821
|
-
|
|
822
|
-
// Get the first item signal and set up its HOOK_WATCH
|
|
823
|
-
// biome-ignore lint/style/noNonNullAssertion: test
|
|
824
|
-
const firstItemSignal = items.at(0)!
|
|
825
|
-
firstItemSignal.on('watch', () => {
|
|
826
|
-
return () => {
|
|
827
|
-
firstItemUnwatchCalled = true
|
|
828
|
-
}
|
|
829
|
-
})
|
|
830
|
-
|
|
831
|
-
let effectValue: string | undefined
|
|
832
|
-
const cleanup = createEffect(() => {
|
|
833
|
-
effectValue = firstItemSignal.get()
|
|
834
|
-
})
|
|
835
|
-
|
|
836
|
-
expect(effectValue).toBe('first')
|
|
837
|
-
expect(firstItemUnwatchCalled).toBe(false)
|
|
838
|
-
|
|
839
|
-
// Remove the first item (index 0) - the State signal still exists but the list changed
|
|
840
|
-
items.remove(0)
|
|
841
|
-
|
|
842
|
-
// The State signal should still work (it's not automatically cleaned up)
|
|
843
|
-
expect(effectValue).toBe('first') // State signal retains its value
|
|
844
|
-
expect(firstItemUnwatchCalled).toBe(false) // Unwatch only happens when effect cleanup
|
|
845
|
-
|
|
846
|
-
// Cleanup the effect - this should call the State signal's unwatch
|
|
847
|
-
cleanup()
|
|
848
|
-
expect(firstItemUnwatchCalled).toBe(true)
|
|
849
|
-
})
|
|
850
|
-
|
|
851
|
-
test('new items added to list get independent State signals with their own hooks', () => {
|
|
852
|
-
const numbers = new List<number>([])
|
|
853
|
-
|
|
854
|
-
// Start with empty list - create effect that tries to access first item
|
|
855
|
-
let effectValue: number | undefined
|
|
856
|
-
const cleanup = createEffect(() => {
|
|
857
|
-
const firstItem = numbers.at(0)
|
|
858
|
-
effectValue = firstItem?.get()
|
|
859
|
-
})
|
|
860
|
-
|
|
861
|
-
// No items yet
|
|
862
|
-
expect(effectValue).toBe(undefined)
|
|
863
|
-
|
|
864
|
-
// Add first item
|
|
865
|
-
const key = numbers.add(42)
|
|
866
|
-
// biome-ignore lint/style/noNonNullAssertion: test
|
|
867
|
-
const newItemSignal = numbers.byKey(key)!
|
|
868
|
-
|
|
869
|
-
let newItemHookCalled = false
|
|
870
|
-
let newItemUnwatchCalled = false
|
|
871
|
-
newItemSignal.on('watch', () => {
|
|
872
|
-
newItemHookCalled = true
|
|
873
|
-
return () => {
|
|
874
|
-
newItemUnwatchCalled = true
|
|
875
|
-
}
|
|
876
|
-
})
|
|
877
|
-
|
|
878
|
-
// Create new effect to access the new item
|
|
879
|
-
let newEffectValue: number | undefined
|
|
880
|
-
const newCleanup = createEffect(() => {
|
|
881
|
-
newEffectValue = newItemSignal.get()
|
|
882
|
-
})
|
|
883
|
-
|
|
884
|
-
expect(newItemHookCalled).toBe(true)
|
|
885
|
-
expect(newEffectValue).toBe(42)
|
|
886
|
-
expect(newItemUnwatchCalled).toBe(false)
|
|
887
|
-
|
|
888
|
-
// Cleanup should trigger unwatch
|
|
889
|
-
newCleanup()
|
|
890
|
-
expect(newItemUnwatchCalled).toBe(true)
|
|
891
|
-
|
|
892
|
-
cleanup()
|
|
893
|
-
})
|
|
894
|
-
|
|
895
|
-
test('List length access triggers List HOOK_WATCH', () => {
|
|
896
|
-
const numbers = new List([1, 2, 3])
|
|
897
|
-
let listHookWatchCalled = false
|
|
898
|
-
let listUnwatchCalled = false
|
|
899
|
-
|
|
900
|
-
numbers.on('watch', () => {
|
|
901
|
-
listHookWatchCalled = true
|
|
902
|
-
return () => {
|
|
903
|
-
listUnwatchCalled = true
|
|
904
|
-
}
|
|
905
|
-
})
|
|
906
|
-
|
|
907
|
-
// Access via list.length - this should trigger list's HOOK_WATCH
|
|
700
|
+
// Access via list.length - this should trigger list's watched callback
|
|
908
701
|
let effectValue: number = 0
|
|
909
702
|
const cleanup = createEffect(() => {
|
|
910
703
|
effectValue = numbers.length
|
|
911
704
|
})
|
|
912
705
|
|
|
913
|
-
expect(
|
|
706
|
+
expect(listWatchedCalled).toBe(true)
|
|
914
707
|
expect(effectValue).toBe(3)
|
|
915
|
-
expect(
|
|
708
|
+
expect(listUnwatchedCalled).toBe(false)
|
|
916
709
|
|
|
917
710
|
cleanup()
|
|
918
|
-
expect(
|
|
919
|
-
})
|
|
920
|
-
|
|
921
|
-
test('exact scenario: List HOOK_WATCH triggered by list.at(0).get(), unwatch on item removal, restart on new item', () => {
|
|
922
|
-
const list = new List<number>([42])
|
|
923
|
-
let listHookWatchCallCount = 0
|
|
924
|
-
let listUnwatchCallCount = 0
|
|
925
|
-
|
|
926
|
-
// Set up List's HOOK_WATCH (this is triggered by list-level access like get() or length)
|
|
927
|
-
list.on('watch', () => {
|
|
928
|
-
listHookWatchCallCount++
|
|
929
|
-
return () => {
|
|
930
|
-
listUnwatchCallCount++
|
|
931
|
-
}
|
|
932
|
-
})
|
|
933
|
-
|
|
934
|
-
// Scenario 1: The list's HOOK_WATCH is called when an effect accesses the first item
|
|
935
|
-
// Note: list.at(0).get() accesses the State signal, not the list itself
|
|
936
|
-
// But if we access list.get() or list.length, it triggers the list's HOOK_WATCH
|
|
937
|
-
let effectValue: number | undefined
|
|
938
|
-
const cleanup1 = createEffect(() => {
|
|
939
|
-
// Access list first to trigger list HOOK_WATCH
|
|
940
|
-
const length = list.length
|
|
941
|
-
if (length > 0) {
|
|
942
|
-
effectValue = list.at(0)?.get()
|
|
943
|
-
} else {
|
|
944
|
-
effectValue = undefined
|
|
945
|
-
}
|
|
946
|
-
})
|
|
947
|
-
|
|
948
|
-
expect(listHookWatchCallCount).toBe(1) // List HOOK_WATCH called due to list.length access
|
|
949
|
-
expect(effectValue).toBe(42)
|
|
950
|
-
|
|
951
|
-
// Scenario 2: The list's unwatch callback is called when the only item with active subscription gets removed
|
|
952
|
-
list.remove(0)
|
|
953
|
-
// The effect should re-run due to list.length change and effectValue should now be undefined
|
|
954
|
-
expect(effectValue).toBe(undefined)
|
|
955
|
-
|
|
956
|
-
// The list unwatch is not called yet because the effect is still active (watching an empty list)
|
|
957
|
-
expect(listUnwatchCallCount).toBe(0)
|
|
958
|
-
|
|
959
|
-
// Clean up the first effect
|
|
960
|
-
cleanup1()
|
|
961
|
-
expect(listUnwatchCallCount).toBe(1) // Now unwatch is called
|
|
962
|
-
|
|
963
|
-
// Scenario 3: The list's HOOK_WATCH is restarted after a new item has been added that gets accessed by an effect
|
|
964
|
-
list.add(100)
|
|
965
|
-
|
|
966
|
-
const cleanup2 = createEffect(() => {
|
|
967
|
-
const length = list.length
|
|
968
|
-
if (length > 0) {
|
|
969
|
-
effectValue = list.at(0)?.get()
|
|
970
|
-
} else {
|
|
971
|
-
effectValue = undefined
|
|
972
|
-
}
|
|
973
|
-
})
|
|
974
|
-
|
|
975
|
-
expect(listHookWatchCallCount).toBe(2) // List HOOK_WATCH called again
|
|
976
|
-
expect(effectValue).toBe(100)
|
|
977
|
-
|
|
978
|
-
cleanup2()
|
|
979
|
-
expect(listUnwatchCallCount).toBe(2) // Second unwatch called
|
|
711
|
+
expect(listUnwatchedCalled).toBe(true)
|
|
980
712
|
})
|
|
981
713
|
})
|
|
982
714
|
})
|
package/test/ref.test.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { expect, mock, test } from 'bun:test'
|
|
2
2
|
import { isRef, Ref } from '../src/classes/ref'
|
|
3
3
|
import { createEffect } from '../src/effect'
|
|
4
|
-
import { HOOK_WATCH } from '../src/system'
|
|
5
4
|
|
|
6
5
|
test('Ref - basic functionality', () => {
|
|
7
6
|
const obj = { name: 'test', value: 42 }
|
|
@@ -35,8 +34,8 @@ test('Ref - validation with guard function', () => {
|
|
|
35
34
|
const validConfig = { host: 'localhost', port: 3000 }
|
|
36
35
|
const invalidConfig = { host: 'localhost' } // missing port
|
|
37
36
|
|
|
38
|
-
expect(() => new Ref(validConfig, isConfig)).not.toThrow()
|
|
39
|
-
expect(() => new Ref(invalidConfig, isConfig)).toThrow()
|
|
37
|
+
expect(() => new Ref(validConfig, { guard: isConfig })).not.toThrow()
|
|
38
|
+
expect(() => new Ref(invalidConfig, { guard: isConfig })).toThrow()
|
|
40
39
|
})
|
|
41
40
|
|
|
42
41
|
test('Ref - reactive subscriptions', () => {
|
|
@@ -227,30 +226,25 @@ test('Ref - handles complex nested objects', () => {
|
|
|
227
226
|
expect(userCount).toBe(2)
|
|
228
227
|
})
|
|
229
228
|
|
|
230
|
-
test('Ref -
|
|
229
|
+
test('Ref - options.watched lazy resource management', async () => {
|
|
231
230
|
// 1. Create Ref with current Date
|
|
232
|
-
const currentDate = new Date()
|
|
233
|
-
const ref = new Ref(currentDate)
|
|
234
|
-
|
|
235
231
|
let counter = 0
|
|
236
232
|
let intervalId: Timer | undefined
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
},
|
|
243
|
-
|
|
244
|
-
// Return cleanup function to clear interval
|
|
245
|
-
return () => {
|
|
233
|
+
const ref = new Ref(new Date(), {
|
|
234
|
+
watched: () => {
|
|
235
|
+
intervalId = setInterval(() => {
|
|
236
|
+
counter++
|
|
237
|
+
}, 10)
|
|
238
|
+
},
|
|
239
|
+
unwatched: () => {
|
|
246
240
|
if (intervalId) {
|
|
247
241
|
clearInterval(intervalId)
|
|
248
242
|
intervalId = undefined
|
|
249
243
|
}
|
|
250
|
-
}
|
|
244
|
+
},
|
|
251
245
|
})
|
|
252
246
|
|
|
253
|
-
//
|
|
247
|
+
// 2. Counter should not be running yet
|
|
254
248
|
expect(counter).toBe(0)
|
|
255
249
|
|
|
256
250
|
// Wait a bit to ensure counter doesn't increment
|
|
@@ -258,62 +252,51 @@ test('Ref - HOOK_WATCH lazy resource management', async () => {
|
|
|
258
252
|
expect(counter).toBe(0)
|
|
259
253
|
expect(intervalId).toBeUndefined()
|
|
260
254
|
|
|
261
|
-
//
|
|
255
|
+
// 3. Effect subscribes by .get()ting the signal value
|
|
262
256
|
const effectCleanup = createEffect(() => {
|
|
263
257
|
ref.get()
|
|
264
258
|
})
|
|
265
259
|
|
|
266
|
-
//
|
|
260
|
+
// 4. Counter should now be running
|
|
267
261
|
await new Promise(resolve => setTimeout(resolve, 50))
|
|
268
262
|
expect(counter).toBeGreaterThan(0)
|
|
269
263
|
expect(intervalId).toBeDefined()
|
|
270
264
|
|
|
271
|
-
//
|
|
265
|
+
// 5. Call effect cleanup, which should stop internal watcher and unsubscribe
|
|
272
266
|
effectCleanup()
|
|
273
267
|
const counterAfterStop = counter
|
|
274
268
|
|
|
275
|
-
//
|
|
269
|
+
// 6. Ref signal should call #unwatch() and counter should stop incrementing
|
|
276
270
|
await new Promise(resolve => setTimeout(resolve, 50))
|
|
277
271
|
expect(counter).toBe(counterAfterStop) // Counter should not have incremented
|
|
278
272
|
expect(intervalId).toBeUndefined() // Interval should be cleared
|
|
279
|
-
|
|
280
|
-
// Clean up hook callback registration
|
|
281
|
-
cleanupHookCallback()
|
|
282
273
|
})
|
|
283
274
|
|
|
284
|
-
test('Ref -
|
|
285
|
-
const ref = new Ref(
|
|
275
|
+
test('Ref - options.watched exception handling', async () => {
|
|
276
|
+
const ref = new Ref(
|
|
277
|
+
{ test: 'value' },
|
|
278
|
+
{
|
|
279
|
+
watched: () => {
|
|
280
|
+
throwingCallbackCalled = true
|
|
281
|
+
throw new Error('Test error in watched callback')
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
)
|
|
286
285
|
|
|
287
286
|
// Mock console.error to capture error logs
|
|
288
287
|
const originalError = console.error
|
|
289
288
|
const errorSpy = mock(() => {})
|
|
290
289
|
console.error = errorSpy
|
|
291
290
|
|
|
292
|
-
let successfulCallbackCalled = false
|
|
293
291
|
let throwingCallbackCalled = false
|
|
294
292
|
|
|
295
|
-
//
|
|
296
|
-
const cleanup1 = ref.on(HOOK_WATCH, () => {
|
|
297
|
-
throwingCallbackCalled = true
|
|
298
|
-
throw new Error('Test error in HOOK_WATCH callback')
|
|
299
|
-
})
|
|
300
|
-
|
|
301
|
-
// Add callback that works normally
|
|
302
|
-
const cleanup2 = ref.on(HOOK_WATCH, () => {
|
|
303
|
-
successfulCallbackCalled = true
|
|
304
|
-
return () => {
|
|
305
|
-
// cleanup function
|
|
306
|
-
}
|
|
307
|
-
})
|
|
308
|
-
|
|
309
|
-
// Subscribe to trigger HOOK_WATCH callbacks
|
|
293
|
+
// Subscribe to trigger watched callback
|
|
310
294
|
const effectCleanup = createEffect(() => {
|
|
311
295
|
ref.get()
|
|
312
296
|
})
|
|
313
297
|
|
|
314
298
|
// Both callbacks should have been called despite the exception
|
|
315
299
|
expect(throwingCallbackCalled).toBe(true)
|
|
316
|
-
expect(successfulCallbackCalled).toBe(true)
|
|
317
300
|
|
|
318
301
|
// Error should have been logged
|
|
319
302
|
expect(errorSpy).toHaveBeenCalledWith(
|
|
@@ -323,13 +306,20 @@ test('Ref - HOOK_WATCH exception handling', async () => {
|
|
|
323
306
|
|
|
324
307
|
// Cleanup
|
|
325
308
|
effectCleanup()
|
|
326
|
-
cleanup1()
|
|
327
|
-
cleanup2()
|
|
328
309
|
console.error = originalError
|
|
329
310
|
})
|
|
330
311
|
|
|
331
|
-
test('Ref -
|
|
332
|
-
const ref = new Ref(
|
|
312
|
+
test('Ref - options.unwatched exception handling', async () => {
|
|
313
|
+
const ref = new Ref(
|
|
314
|
+
{ test: 'value' },
|
|
315
|
+
{
|
|
316
|
+
watched: () => {},
|
|
317
|
+
unwatched: () => {
|
|
318
|
+
cleanup1Called = true
|
|
319
|
+
throw new Error('Test error in cleanup function')
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
)
|
|
333
323
|
|
|
334
324
|
// Mock console.error to capture error logs
|
|
335
325
|
const originalError = console.error
|
|
@@ -337,21 +327,6 @@ test('Ref - cleanup function exception handling', async () => {
|
|
|
337
327
|
console.error = errorSpy
|
|
338
328
|
|
|
339
329
|
let cleanup1Called = false
|
|
340
|
-
let cleanup2Called = false
|
|
341
|
-
|
|
342
|
-
// Add callbacks with cleanup functions, one throws
|
|
343
|
-
const hookCleanup1 = ref.on(HOOK_WATCH, () => {
|
|
344
|
-
return () => {
|
|
345
|
-
cleanup1Called = true
|
|
346
|
-
throw new Error('Test error in cleanup function')
|
|
347
|
-
}
|
|
348
|
-
})
|
|
349
|
-
|
|
350
|
-
const hookCleanup2 = ref.on(HOOK_WATCH, () => {
|
|
351
|
-
return () => {
|
|
352
|
-
cleanup2Called = true
|
|
353
|
-
}
|
|
354
|
-
})
|
|
355
330
|
|
|
356
331
|
// Subscribe and then unsubscribe to trigger cleanup
|
|
357
332
|
const effectCleanup = createEffect(() => {
|
|
@@ -366,7 +341,6 @@ test('Ref - cleanup function exception handling', async () => {
|
|
|
366
341
|
|
|
367
342
|
// Both cleanup functions should have been called despite the exception
|
|
368
343
|
expect(cleanup1Called).toBe(true)
|
|
369
|
-
expect(cleanup2Called).toBe(true)
|
|
370
344
|
|
|
371
345
|
// Error should have been logged
|
|
372
346
|
expect(errorSpy).toHaveBeenCalledWith(
|
|
@@ -375,7 +349,5 @@ test('Ref - cleanup function exception handling', async () => {
|
|
|
375
349
|
)
|
|
376
350
|
|
|
377
351
|
// Cleanup
|
|
378
|
-
hookCleanup1()
|
|
379
|
-
hookCleanup2()
|
|
380
352
|
console.error = originalError
|
|
381
353
|
})
|
package/test/state.test.ts
CHANGED
|
@@ -154,10 +154,8 @@ describe('State', () => {
|
|
|
154
154
|
expect(true).toBe(false) // Should not reach here
|
|
155
155
|
} catch (error) {
|
|
156
156
|
expect(error).toBeInstanceOf(TypeError)
|
|
157
|
-
expect(error.name).toBe('NullishSignalValueError')
|
|
158
|
-
expect(error.message).toBe(
|
|
159
|
-
'Nullish signal values are not allowed in State',
|
|
160
|
-
)
|
|
157
|
+
expect((error as Error).name).toBe('NullishSignalValueError')
|
|
158
|
+
expect((error as Error).message).toBe('Nullish signal values are not allowed in State')
|
|
161
159
|
}
|
|
162
160
|
|
|
163
161
|
const state = new State(42)
|
|
@@ -167,10 +165,8 @@ describe('State', () => {
|
|
|
167
165
|
expect(true).toBe(false) // Should not reach here
|
|
168
166
|
} catch (error) {
|
|
169
167
|
expect(error).toBeInstanceOf(TypeError)
|
|
170
|
-
expect(error.name).toBe('NullishSignalValueError')
|
|
171
|
-
expect(error.message).toBe(
|
|
172
|
-
'Nullish signal values are not allowed in State',
|
|
173
|
-
)
|
|
168
|
+
expect((error as Error).name).toBe('NullishSignalValueError')
|
|
169
|
+
expect((error as Error).message).toBe('Nullish signal values are not allowed in State')
|
|
174
170
|
}
|
|
175
171
|
})
|
|
176
172
|
|
|
@@ -240,10 +236,8 @@ describe('State', () => {
|
|
|
240
236
|
expect(true).toBe(false) // Should not reach here
|
|
241
237
|
} catch (error) {
|
|
242
238
|
expect(error).toBeInstanceOf(TypeError)
|
|
243
|
-
expect(error.name).toBe('InvalidCallbackError')
|
|
244
|
-
expect(error.message).toBe(
|
|
245
|
-
'Invalid State update callback null',
|
|
246
|
-
)
|
|
239
|
+
expect((error as Error).name).toBe('InvalidCallbackError')
|
|
240
|
+
expect((error as Error).message).toBe('Invalid State update callback null')
|
|
247
241
|
}
|
|
248
242
|
})
|
|
249
243
|
|