@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.
Files changed (50) hide show
  1. package/.ai-context.md +11 -5
  2. package/.github/copilot-instructions.md +1 -1
  3. package/.zed/settings.json +3 -0
  4. package/CLAUDE.md +18 -79
  5. package/README.md +23 -37
  6. package/archive/benchmark.ts +0 -5
  7. package/archive/collection.ts +5 -62
  8. package/archive/composite.ts +85 -0
  9. package/archive/computed.ts +17 -20
  10. package/archive/list.ts +6 -67
  11. package/archive/memo.ts +13 -14
  12. package/archive/store.ts +7 -66
  13. package/archive/task.ts +18 -20
  14. package/index.dev.js +438 -614
  15. package/index.js +1 -1
  16. package/index.ts +8 -19
  17. package/package.json +6 -6
  18. package/src/classes/collection.ts +59 -112
  19. package/src/classes/computed.ts +146 -189
  20. package/src/classes/list.ts +138 -105
  21. package/src/classes/ref.ts +16 -42
  22. package/src/classes/state.ts +16 -45
  23. package/src/classes/store.ts +107 -72
  24. package/src/effect.ts +9 -12
  25. package/src/errors.ts +12 -8
  26. package/src/signal.ts +3 -1
  27. package/src/system.ts +136 -154
  28. package/test/batch.test.ts +4 -11
  29. package/test/benchmark.test.ts +4 -2
  30. package/test/collection.test.ts +46 -306
  31. package/test/computed.test.ts +205 -223
  32. package/test/list.test.ts +35 -303
  33. package/test/ref.test.ts +38 -66
  34. package/test/state.test.ts +6 -12
  35. package/test/store.test.ts +37 -489
  36. package/test/util/dependency-graph.ts +2 -2
  37. package/tsconfig.build.json +11 -0
  38. package/tsconfig.json +5 -7
  39. package/types/index.d.ts +2 -2
  40. package/types/src/classes/collection.d.ts +4 -6
  41. package/types/src/classes/computed.d.ts +17 -37
  42. package/types/src/classes/list.d.ts +8 -6
  43. package/types/src/classes/ref.d.ts +7 -20
  44. package/types/src/classes/state.d.ts +5 -17
  45. package/types/src/classes/store.d.ts +12 -11
  46. package/types/src/errors.d.ts +2 -4
  47. package/types/src/signal.d.ts +3 -2
  48. package/types/src/system.d.ts +41 -44
  49. package/src/classes/composite.ts +0 -171
  50. 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 (user, abort: AbortSignal) => {
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('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
- }
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(listHookWatchCalled).toBe(false)
671
+ expect(linkWatchedCalled).toBe(false)
770
672
 
771
- // Access list via list.get() - this should trigger list's HOOK_WATCH
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(listHookWatchCalled).toBe(true)
679
+ expect(linkWatchedCalled).toBe(true)
778
680
  expect(effectValue).toEqual([10, 20, 30])
779
- expect(listUnwatchCalled).toBe(false)
681
+ expect(listUnwatchedCalled).toBe(false)
780
682
 
781
683
  // Cleanup effect - should trigger unwatch
782
684
  cleanup()
783
- expect(listUnwatchCalled).toBe(true)
685
+ expect(listUnwatchedCalled).toBe(true)
784
686
  })
785
687
 
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
- }
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
- 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
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(listHookWatchCalled).toBe(true)
706
+ expect(listWatchedCalled).toBe(true)
914
707
  expect(effectValue).toBe(3)
915
- expect(listUnwatchCalled).toBe(false)
708
+ expect(listUnwatchedCalled).toBe(false)
916
709
 
917
710
  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
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 - HOOK_WATCH lazy resource management', async () => {
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
- // 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 () => {
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
- // 3. Counter should not be running yet
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
- // 4. Effect subscribes by .get()ting the signal value
255
+ // 3. Effect subscribes by .get()ting the signal value
262
256
  const effectCleanup = createEffect(() => {
263
257
  ref.get()
264
258
  })
265
259
 
266
- // 5. Counter should now be running
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
- // 6. Call effect cleanup, which should stop internal watcher and unsubscribe
265
+ // 5. Call effect cleanup, which should stop internal watcher and unsubscribe
272
266
  effectCleanup()
273
267
  const counterAfterStop = counter
274
268
 
275
- // 7. Ref signal should call #unwatch() and counter should stop incrementing
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 - HOOK_WATCH exception handling', async () => {
285
- const ref = new Ref({ test: 'value' })
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
- // 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
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 - cleanup function exception handling', async () => {
332
- const ref = new Ref({ test: 'value' })
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
  })
@@ -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