@zeix/cause-effect 0.17.1 → 0.17.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.
- package/.ai-context.md +7 -0
- package/.github/copilot-instructions.md +4 -0
- package/CLAUDE.md +96 -1
- package/README.md +44 -7
- package/archive/collection.ts +23 -25
- package/archive/computed.ts +3 -2
- package/archive/list.ts +21 -28
- package/archive/memo.ts +2 -1
- package/archive/state.ts +2 -1
- package/archive/store.ts +21 -32
- package/archive/task.ts +4 -7
- package/index.dev.js +356 -198
- package/index.js +1 -1
- package/index.ts +15 -6
- package/package.json +1 -1
- package/src/classes/collection.ts +69 -53
- package/src/classes/composite.ts +28 -33
- package/src/classes/computed.ts +87 -28
- package/src/classes/list.ts +31 -26
- package/src/classes/ref.ts +33 -5
- package/src/classes/state.ts +41 -8
- package/src/classes/store.ts +47 -30
- package/src/diff.ts +2 -1
- package/src/effect.ts +19 -9
- package/src/errors.ts +10 -1
- package/src/resolve.ts +1 -1
- package/src/signal.ts +0 -1
- package/src/system.ts +159 -43
- package/src/util.ts +0 -6
- package/test/collection.test.ts +279 -20
- package/test/computed.test.ts +268 -11
- package/test/effect.test.ts +2 -2
- package/test/list.test.ts +249 -21
- package/test/ref.test.ts +154 -0
- package/test/state.test.ts +13 -13
- package/test/store.test.ts +473 -28
- package/types/index.d.ts +3 -3
- package/types/src/classes/collection.d.ts +8 -7
- package/types/src/classes/composite.d.ts +4 -4
- package/types/src/classes/computed.d.ts +17 -0
- package/types/src/classes/list.d.ts +2 -2
- package/types/src/classes/ref.d.ts +10 -1
- package/types/src/classes/state.d.ts +9 -0
- package/types/src/classes/store.d.ts +4 -4
- package/types/src/effect.d.ts +1 -2
- package/types/src/errors.d.ts +4 -1
- package/types/src/system.d.ts +40 -24
- package/types/src/util.d.ts +1 -2
package/test/effect.test.ts
CHANGED
|
@@ -223,7 +223,7 @@ describe('Effect', () => {
|
|
|
223
223
|
|
|
224
224
|
// Check if console.error was called with the error message
|
|
225
225
|
expect(mockConsoleError).toHaveBeenCalledWith(
|
|
226
|
-
'
|
|
226
|
+
'Error in effect callback:',
|
|
227
227
|
expect.any(Error),
|
|
228
228
|
)
|
|
229
229
|
} finally {
|
|
@@ -507,7 +507,7 @@ describe('Effect - Async with AbortSignal', () => {
|
|
|
507
507
|
|
|
508
508
|
// Should have logged the async error
|
|
509
509
|
expect(mockConsoleError).toHaveBeenCalledWith(
|
|
510
|
-
'
|
|
510
|
+
'Error in async effect callback:',
|
|
511
511
|
expect.any(Error),
|
|
512
512
|
)
|
|
513
513
|
} finally {
|
package/test/list.test.ts
CHANGED
|
@@ -130,15 +130,15 @@ describe('list', () => {
|
|
|
130
130
|
expect(names.get()).toEqual(['Alice', 'Bob', 'Charlie'])
|
|
131
131
|
})
|
|
132
132
|
|
|
133
|
-
test('
|
|
133
|
+
test('triggers HOOK_SORT with new order', () => {
|
|
134
134
|
const numbers = new List([3, 1, 2])
|
|
135
|
-
let
|
|
135
|
+
let order: readonly string[] | undefined
|
|
136
136
|
numbers.on('sort', sort => {
|
|
137
|
-
|
|
137
|
+
order = sort
|
|
138
138
|
})
|
|
139
139
|
numbers.sort()
|
|
140
|
-
expect(
|
|
141
|
-
expect(
|
|
140
|
+
expect(order).toHaveLength(3)
|
|
141
|
+
expect(order).toEqual(['1', '2', '0'])
|
|
142
142
|
})
|
|
143
143
|
|
|
144
144
|
test('sort is reactive - watchers are notified', () => {
|
|
@@ -302,43 +302,43 @@ describe('list', () => {
|
|
|
302
302
|
})
|
|
303
303
|
})
|
|
304
304
|
|
|
305
|
-
describe('
|
|
306
|
-
test('
|
|
305
|
+
describe('Hooks', () => {
|
|
306
|
+
test('trigger HOOK_ADD when adding items', () => {
|
|
307
307
|
const numbers = new List([1, 2])
|
|
308
|
-
let
|
|
308
|
+
let addedKeys: readonly string[] | undefined
|
|
309
309
|
let newArray: number[] = []
|
|
310
310
|
numbers.on('add', add => {
|
|
311
|
-
|
|
311
|
+
addedKeys = add
|
|
312
312
|
newArray = numbers.get()
|
|
313
313
|
})
|
|
314
314
|
numbers.add(3)
|
|
315
|
-
expect(
|
|
315
|
+
expect(addedKeys).toHaveLength(1)
|
|
316
316
|
expect(newArray).toEqual([1, 2, 3])
|
|
317
317
|
})
|
|
318
318
|
|
|
319
|
-
test('
|
|
319
|
+
test('triggers HOOK_CHANGE when properties are modified', () => {
|
|
320
320
|
const items = new List([{ value: 10 }])
|
|
321
|
-
let
|
|
321
|
+
let changedKeys: readonly string[] | undefined
|
|
322
322
|
let newArray: { value: number }[] = []
|
|
323
323
|
items.on('change', change => {
|
|
324
|
-
|
|
324
|
+
changedKeys = change
|
|
325
325
|
newArray = items.get()
|
|
326
326
|
})
|
|
327
327
|
items.at(0)?.set({ value: 20 })
|
|
328
|
-
expect(
|
|
328
|
+
expect(changedKeys).toHaveLength(1)
|
|
329
329
|
expect(newArray).toEqual([{ value: 20 }])
|
|
330
330
|
})
|
|
331
331
|
|
|
332
|
-
test('
|
|
332
|
+
test('triggers HOOK_REMOVE when items are removed', () => {
|
|
333
333
|
const items = new List([1, 2, 3])
|
|
334
|
-
let
|
|
334
|
+
let removedKeys: readonly string[] | undefined
|
|
335
335
|
let newArray: number[] = []
|
|
336
336
|
items.on('remove', remove => {
|
|
337
|
-
|
|
337
|
+
removedKeys = remove
|
|
338
338
|
newArray = items.get()
|
|
339
339
|
})
|
|
340
340
|
items.remove(1)
|
|
341
|
-
expect(
|
|
341
|
+
expect(removedKeys).toHaveLength(1)
|
|
342
342
|
expect(newArray).toEqual([1, 3])
|
|
343
343
|
})
|
|
344
344
|
})
|
|
@@ -670,7 +670,7 @@ describe('list', () => {
|
|
|
670
670
|
(value: number) => value * 2,
|
|
671
671
|
)
|
|
672
672
|
|
|
673
|
-
let addedKeys: readonly string[]
|
|
673
|
+
let addedKeys: readonly string[] | undefined
|
|
674
674
|
doubled.on('add', keys => {
|
|
675
675
|
addedKeys = keys
|
|
676
676
|
})
|
|
@@ -685,7 +685,7 @@ describe('list', () => {
|
|
|
685
685
|
(value: number) => value * 2,
|
|
686
686
|
)
|
|
687
687
|
|
|
688
|
-
let removedKeys: readonly string[]
|
|
688
|
+
let removedKeys: readonly string[] | undefined
|
|
689
689
|
doubled.on('remove', keys => {
|
|
690
690
|
removedKeys = keys
|
|
691
691
|
})
|
|
@@ -700,7 +700,7 @@ describe('list', () => {
|
|
|
700
700
|
(value: number) => value * 2,
|
|
701
701
|
)
|
|
702
702
|
|
|
703
|
-
let sortedKeys: readonly string[]
|
|
703
|
+
let sortedKeys: readonly string[] | undefined
|
|
704
704
|
doubled.on('sort', keys => {
|
|
705
705
|
sortedKeys = keys
|
|
706
706
|
})
|
|
@@ -751,4 +751,232 @@ describe('list', () => {
|
|
|
751
751
|
})
|
|
752
752
|
})
|
|
753
753
|
})
|
|
754
|
+
|
|
755
|
+
describe('hooks system', () => {
|
|
756
|
+
test('List HOOK_WATCH is called when effect accesses list.get()', () => {
|
|
757
|
+
const numbers = new List([10, 20, 30])
|
|
758
|
+
let listHookWatchCalled = false
|
|
759
|
+
let listUnwatchCalled = false
|
|
760
|
+
|
|
761
|
+
// Set up HOOK_WATCH callback on the list itself
|
|
762
|
+
numbers.on('watch', () => {
|
|
763
|
+
listHookWatchCalled = true
|
|
764
|
+
return () => {
|
|
765
|
+
listUnwatchCalled = true
|
|
766
|
+
}
|
|
767
|
+
})
|
|
768
|
+
|
|
769
|
+
expect(listHookWatchCalled).toBe(false)
|
|
770
|
+
|
|
771
|
+
// Access list via list.get() - this should trigger list's HOOK_WATCH
|
|
772
|
+
let effectValue: number[] = []
|
|
773
|
+
const cleanup = createEffect(() => {
|
|
774
|
+
effectValue = numbers.get()
|
|
775
|
+
})
|
|
776
|
+
|
|
777
|
+
expect(listHookWatchCalled).toBe(true)
|
|
778
|
+
expect(effectValue).toEqual([10, 20, 30])
|
|
779
|
+
expect(listUnwatchCalled).toBe(false)
|
|
780
|
+
|
|
781
|
+
// Cleanup effect - should trigger unwatch
|
|
782
|
+
cleanup()
|
|
783
|
+
expect(listUnwatchCalled).toBe(true)
|
|
784
|
+
})
|
|
785
|
+
|
|
786
|
+
test('individual State signals have independent HOOK_WATCH when accessed via list.at().get()', () => {
|
|
787
|
+
const items = new List(['first', 'second'])
|
|
788
|
+
let firstItemHookCalled = false
|
|
789
|
+
let firstItemUnwatchCalled = false
|
|
790
|
+
|
|
791
|
+
// Get the first item signal and set up its HOOK_WATCH
|
|
792
|
+
// biome-ignore lint/style/noNonNullAssertion: test
|
|
793
|
+
const firstItemSignal = items.at(0)!
|
|
794
|
+
firstItemSignal.on('watch', () => {
|
|
795
|
+
firstItemHookCalled = true
|
|
796
|
+
return () => {
|
|
797
|
+
firstItemUnwatchCalled = true
|
|
798
|
+
}
|
|
799
|
+
})
|
|
800
|
+
|
|
801
|
+
expect(firstItemHookCalled).toBe(false)
|
|
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
|
|
908
|
+
let effectValue: number = 0
|
|
909
|
+
const cleanup = createEffect(() => {
|
|
910
|
+
effectValue = numbers.length
|
|
911
|
+
})
|
|
912
|
+
|
|
913
|
+
expect(listHookWatchCalled).toBe(true)
|
|
914
|
+
expect(effectValue).toBe(3)
|
|
915
|
+
expect(listUnwatchCalled).toBe(false)
|
|
916
|
+
|
|
917
|
+
cleanup()
|
|
918
|
+
expect(listUnwatchCalled).toBe(true)
|
|
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
|
|
980
|
+
})
|
|
981
|
+
})
|
|
754
982
|
})
|
package/test/ref.test.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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'
|
|
4
5
|
|
|
5
6
|
test('Ref - basic functionality', () => {
|
|
6
7
|
const obj = { name: 'test', value: 42 }
|
|
@@ -225,3 +226,156 @@ test('Ref - handles complex nested objects', () => {
|
|
|
225
226
|
|
|
226
227
|
expect(userCount).toBe(2)
|
|
227
228
|
})
|
|
229
|
+
|
|
230
|
+
test('Ref - HOOK_WATCH lazy resource management', async () => {
|
|
231
|
+
// 1. Create Ref with current Date
|
|
232
|
+
const currentDate = new Date()
|
|
233
|
+
const ref = new Ref(currentDate)
|
|
234
|
+
|
|
235
|
+
let counter = 0
|
|
236
|
+
let intervalId: Timer | undefined
|
|
237
|
+
|
|
238
|
+
// 2. Add HOOK_WATCH callback that starts setInterval and returns cleanup
|
|
239
|
+
const cleanupHookCallback = ref.on(HOOK_WATCH, () => {
|
|
240
|
+
intervalId = setInterval(() => {
|
|
241
|
+
counter++
|
|
242
|
+
}, 10) // Use short interval for faster test
|
|
243
|
+
|
|
244
|
+
// Return cleanup function to clear interval
|
|
245
|
+
return () => {
|
|
246
|
+
if (intervalId) {
|
|
247
|
+
clearInterval(intervalId)
|
|
248
|
+
intervalId = undefined
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
// 3. Counter should not be running yet
|
|
254
|
+
expect(counter).toBe(0)
|
|
255
|
+
|
|
256
|
+
// Wait a bit to ensure counter doesn't increment
|
|
257
|
+
await new Promise(resolve => setTimeout(resolve, 50))
|
|
258
|
+
expect(counter).toBe(0)
|
|
259
|
+
expect(intervalId).toBeUndefined()
|
|
260
|
+
|
|
261
|
+
// 4. Effect subscribes by .get()ting the signal value
|
|
262
|
+
const effectCleanup = createEffect(() => {
|
|
263
|
+
ref.get()
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
// 5. Counter should now be running
|
|
267
|
+
await new Promise(resolve => setTimeout(resolve, 50))
|
|
268
|
+
expect(counter).toBeGreaterThan(0)
|
|
269
|
+
expect(intervalId).toBeDefined()
|
|
270
|
+
|
|
271
|
+
// 6. Call effect cleanup, which should stop internal watcher and unsubscribe
|
|
272
|
+
effectCleanup()
|
|
273
|
+
const counterAfterStop = counter
|
|
274
|
+
|
|
275
|
+
// 7. Ref signal should call #unwatch() and counter should stop incrementing
|
|
276
|
+
await new Promise(resolve => setTimeout(resolve, 50))
|
|
277
|
+
expect(counter).toBe(counterAfterStop) // Counter should not have incremented
|
|
278
|
+
expect(intervalId).toBeUndefined() // Interval should be cleared
|
|
279
|
+
|
|
280
|
+
// Clean up hook callback registration
|
|
281
|
+
cleanupHookCallback()
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
test('Ref - HOOK_WATCH exception handling', async () => {
|
|
285
|
+
const ref = new Ref({ test: 'value' })
|
|
286
|
+
|
|
287
|
+
// Mock console.error to capture error logs
|
|
288
|
+
const originalError = console.error
|
|
289
|
+
const errorSpy = mock(() => {})
|
|
290
|
+
console.error = errorSpy
|
|
291
|
+
|
|
292
|
+
let successfulCallbackCalled = false
|
|
293
|
+
let throwingCallbackCalled = false
|
|
294
|
+
|
|
295
|
+
// Add callback that throws an exception
|
|
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
|
|
310
|
+
const effectCleanup = createEffect(() => {
|
|
311
|
+
ref.get()
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
// Both callbacks should have been called despite the exception
|
|
315
|
+
expect(throwingCallbackCalled).toBe(true)
|
|
316
|
+
expect(successfulCallbackCalled).toBe(true)
|
|
317
|
+
|
|
318
|
+
// Error should have been logged
|
|
319
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
320
|
+
'Error in effect callback:',
|
|
321
|
+
expect.any(Error),
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
// Cleanup
|
|
325
|
+
effectCleanup()
|
|
326
|
+
cleanup1()
|
|
327
|
+
cleanup2()
|
|
328
|
+
console.error = originalError
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
test('Ref - cleanup function exception handling', async () => {
|
|
332
|
+
const ref = new Ref({ test: 'value' })
|
|
333
|
+
|
|
334
|
+
// Mock console.error to capture error logs
|
|
335
|
+
const originalError = console.error
|
|
336
|
+
const errorSpy = mock(() => {})
|
|
337
|
+
console.error = errorSpy
|
|
338
|
+
|
|
339
|
+
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
|
+
|
|
356
|
+
// Subscribe and then unsubscribe to trigger cleanup
|
|
357
|
+
const effectCleanup = createEffect(() => {
|
|
358
|
+
ref.get()
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
// Unsubscribe to trigger cleanup functions
|
|
362
|
+
effectCleanup()
|
|
363
|
+
|
|
364
|
+
// Wait a bit for cleanup to complete
|
|
365
|
+
await new Promise(resolve => setTimeout(resolve, 10))
|
|
366
|
+
|
|
367
|
+
// Both cleanup functions should have been called despite the exception
|
|
368
|
+
expect(cleanup1Called).toBe(true)
|
|
369
|
+
expect(cleanup2Called).toBe(true)
|
|
370
|
+
|
|
371
|
+
// Error should have been logged
|
|
372
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
373
|
+
'Error in effect cleanup:',
|
|
374
|
+
expect.any(Error),
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
// Cleanup
|
|
378
|
+
hookCleanup1()
|
|
379
|
+
hookCleanup2()
|
|
380
|
+
console.error = originalError
|
|
381
|
+
})
|
package/test/state.test.ts
CHANGED
|
@@ -125,12 +125,12 @@ describe('State', () => {
|
|
|
125
125
|
expect(() => {
|
|
126
126
|
// @ts-expect-error - Testing invalid input
|
|
127
127
|
new State(null)
|
|
128
|
-
}).toThrow('Nullish signal values are not allowed in
|
|
128
|
+
}).toThrow('Nullish signal values are not allowed in State')
|
|
129
129
|
|
|
130
130
|
expect(() => {
|
|
131
131
|
// @ts-expect-error - Testing invalid input
|
|
132
132
|
new State(undefined)
|
|
133
|
-
}).toThrow('Nullish signal values are not allowed in
|
|
133
|
+
}).toThrow('Nullish signal values are not allowed in State')
|
|
134
134
|
})
|
|
135
135
|
|
|
136
136
|
test('should throw NullishSignalValueError when newValue is nullish in set()', () => {
|
|
@@ -139,12 +139,12 @@ describe('State', () => {
|
|
|
139
139
|
expect(() => {
|
|
140
140
|
// @ts-expect-error - Testing invalid input
|
|
141
141
|
state.set(null)
|
|
142
|
-
}).toThrow('Nullish signal values are not allowed in
|
|
142
|
+
}).toThrow('Nullish signal values are not allowed in State')
|
|
143
143
|
|
|
144
144
|
expect(() => {
|
|
145
145
|
// @ts-expect-error - Testing invalid input
|
|
146
146
|
state.set(undefined)
|
|
147
|
-
}).toThrow('Nullish signal values are not allowed in
|
|
147
|
+
}).toThrow('Nullish signal values are not allowed in State')
|
|
148
148
|
})
|
|
149
149
|
|
|
150
150
|
test('should throw specific error types for nullish values', () => {
|
|
@@ -156,7 +156,7 @@ describe('State', () => {
|
|
|
156
156
|
expect(error).toBeInstanceOf(TypeError)
|
|
157
157
|
expect(error.name).toBe('NullishSignalValueError')
|
|
158
158
|
expect(error.message).toBe(
|
|
159
|
-
'Nullish signal values are not allowed in
|
|
159
|
+
'Nullish signal values are not allowed in State',
|
|
160
160
|
)
|
|
161
161
|
}
|
|
162
162
|
|
|
@@ -169,7 +169,7 @@ describe('State', () => {
|
|
|
169
169
|
expect(error).toBeInstanceOf(TypeError)
|
|
170
170
|
expect(error.name).toBe('NullishSignalValueError')
|
|
171
171
|
expect(error.message).toBe(
|
|
172
|
-
'Nullish signal values are not allowed in
|
|
172
|
+
'Nullish signal values are not allowed in State',
|
|
173
173
|
)
|
|
174
174
|
}
|
|
175
175
|
})
|
|
@@ -213,22 +213,22 @@ describe('State', () => {
|
|
|
213
213
|
expect(() => {
|
|
214
214
|
// @ts-expect-error - Testing invalid input
|
|
215
215
|
state.update(null)
|
|
216
|
-
}).toThrow('Invalid
|
|
216
|
+
}).toThrow('Invalid State update callback null')
|
|
217
217
|
|
|
218
218
|
expect(() => {
|
|
219
219
|
// @ts-expect-error - Testing invalid input
|
|
220
220
|
state.update(undefined)
|
|
221
|
-
}).toThrow('Invalid
|
|
221
|
+
}).toThrow('Invalid State update callback undefined')
|
|
222
222
|
|
|
223
223
|
expect(() => {
|
|
224
224
|
// @ts-expect-error - Testing invalid input
|
|
225
225
|
state.update('not a function')
|
|
226
|
-
}).toThrow('Invalid
|
|
226
|
+
}).toThrow('Invalid State update callback "not a function"')
|
|
227
227
|
|
|
228
228
|
expect(() => {
|
|
229
229
|
// @ts-expect-error - Testing invalid input
|
|
230
230
|
state.update(42)
|
|
231
|
-
}).toThrow('Invalid
|
|
231
|
+
}).toThrow('Invalid State update callback 42')
|
|
232
232
|
})
|
|
233
233
|
|
|
234
234
|
test('should throw specific error type for non-function updater', () => {
|
|
@@ -242,7 +242,7 @@ describe('State', () => {
|
|
|
242
242
|
expect(error).toBeInstanceOf(TypeError)
|
|
243
243
|
expect(error.name).toBe('InvalidCallbackError')
|
|
244
244
|
expect(error.message).toBe(
|
|
245
|
-
'Invalid
|
|
245
|
+
'Invalid State update callback null',
|
|
246
246
|
)
|
|
247
247
|
}
|
|
248
248
|
})
|
|
@@ -266,12 +266,12 @@ describe('State', () => {
|
|
|
266
266
|
expect(() => {
|
|
267
267
|
// @ts-expect-error - Testing invalid return value
|
|
268
268
|
state.update(() => null)
|
|
269
|
-
}).toThrow('Nullish signal values are not allowed in
|
|
269
|
+
}).toThrow('Nullish signal values are not allowed in State')
|
|
270
270
|
|
|
271
271
|
expect(() => {
|
|
272
272
|
// @ts-expect-error - Testing invalid return value
|
|
273
273
|
state.update(() => undefined)
|
|
274
|
-
}).toThrow('Nullish signal values are not allowed in
|
|
274
|
+
}).toThrow('Nullish signal values are not allowed in State')
|
|
275
275
|
|
|
276
276
|
// State should remain unchanged after error
|
|
277
277
|
expect(state.get()).toBe(42)
|