@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
@@ -10,7 +10,6 @@ import {
10
10
  Task,
11
11
  UNSET,
12
12
  } from '../index.ts'
13
- import { HOOK_WATCH } from '../src/system'
14
13
 
15
14
  /* === Utility Functions === */
16
15
 
@@ -230,7 +229,7 @@ describe('Computed', () => {
230
229
  test('should detect and throw error for circular dependencies', () => {
231
230
  const a = new State(1)
232
231
  const b = new Memo(() => c.get() + 1)
233
- const c = new Memo(() => b.get() + a.get())
232
+ const c = new Memo((): number => b.get() + a.get())
234
233
  expect(() => {
235
234
  b.get() // This should trigger the circular dependency
236
235
  }).toThrow('Circular dependency detected in memo')
@@ -269,7 +268,7 @@ describe('Computed', () => {
269
268
  expect(a.get()).toBe(1)
270
269
  expect(true).toBe(false) // This line should not be reached
271
270
  } catch (error) {
272
- expect(error.message).toBe('Calculation error')
271
+ expect((error as Error).message).toBe('Calculation error')
273
272
  } finally {
274
273
  expect(c.get()).toBe('c: recovered')
275
274
  expect(okCount).toBe(2)
@@ -468,35 +467,11 @@ describe('Computed', () => {
468
467
  }).toThrow('Invalid Task callback (_a) => 42')
469
468
  })
470
469
 
471
- test('should throw NullishSignalValueError when initialValue is null', () => {
470
+ test('should expect type error if null is passed for options.initialValue', () => {
472
471
  expect(() => {
473
472
  // @ts-expect-error - Testing invalid input
474
- new Memo(() => 42, null)
475
- }).toThrow('Nullish signal values are not allowed in Memo')
476
- })
477
-
478
- test('should throw specific error types for invalid inputs', () => {
479
- try {
480
- // @ts-expect-error - Testing invalid input
481
- new Memo(null)
482
- expect(true).toBe(false) // Should not reach here
483
- } catch (error) {
484
- expect(error).toBeInstanceOf(TypeError)
485
- expect(error.name).toBe('InvalidCallbackError')
486
- expect(error.message).toBe('Invalid Memo callback null')
487
- }
488
-
489
- try {
490
- // @ts-expect-error - Testing invalid input
491
- new Memo(() => 42, null)
492
- expect(true).toBe(false) // Should not reach here
493
- } catch (error) {
494
- expect(error).toBeInstanceOf(TypeError)
495
- expect(error.name).toBe('NullishSignalValueError')
496
- expect(error.message).toBe(
497
- 'Nullish signal values are not allowed in Memo',
498
- )
499
- }
473
+ new Memo(() => 42, { initialValue: null })
474
+ }).not.toThrow()
500
475
  })
501
476
 
502
477
  test('should allow valid callbacks and non-nullish initialValues', () => {
@@ -506,36 +481,43 @@ describe('Computed', () => {
506
481
  }).not.toThrow()
507
482
 
508
483
  expect(() => {
509
- new Memo(() => 42, 0)
484
+ new Memo(() => 42, { initialValue: 0 })
510
485
  }).not.toThrow()
511
486
 
512
487
  expect(() => {
513
- new Memo(() => 'foo', '')
488
+ new Memo(() => 'foo', { initialValue: '' })
514
489
  }).not.toThrow()
515
490
 
516
491
  expect(() => {
517
- new Memo(() => true, false)
492
+ new Memo(() => true, { initialValue: false })
518
493
  }).not.toThrow()
519
494
 
520
495
  expect(() => {
521
- new Task(async () => ({ id: 42, name: 'John' }), UNSET)
496
+ new Task(async () => ({ id: 42, name: 'John' }), {
497
+ initialValue: UNSET,
498
+ })
522
499
  }).not.toThrow()
523
500
  })
524
501
  })
525
502
 
526
503
  describe('Initial Value and Old Value', () => {
527
504
  test('should use initialValue when provided', () => {
528
- const computed = new Memo((oldValue: number) => oldValue + 1, 10)
505
+ const computed = new Memo((oldValue: number) => oldValue + 1, {
506
+ initialValue: 10,
507
+ })
529
508
  expect(computed.get()).toBe(11)
530
509
  })
531
510
 
532
511
  test('should pass current value as oldValue to callback', () => {
533
512
  const state = new State(5)
534
513
  let receivedOldValue: number | undefined
535
- const computed = new Memo((oldValue: number) => {
536
- receivedOldValue = oldValue
537
- return state.get() * 2
538
- }, 0)
514
+ const computed = new Memo(
515
+ (oldValue: number) => {
516
+ receivedOldValue = oldValue
517
+ return state.get() * 2
518
+ },
519
+ { initialValue: 0 },
520
+ )
539
521
 
540
522
  expect(computed.get()).toBe(10)
541
523
  expect(receivedOldValue).toBe(0)
@@ -547,10 +529,13 @@ describe('Computed', () => {
547
529
 
548
530
  test('should work as reducer function with oldValue', () => {
549
531
  const increment = new State(0)
550
- const sum = new Memo((oldValue: number) => {
551
- const inc = increment.get()
552
- return inc === 0 ? oldValue : oldValue + inc
553
- }, 0)
532
+ const sum = new Memo(
533
+ (oldValue: number) => {
534
+ const inc = increment.get()
535
+ return inc === 0 ? oldValue : oldValue + inc
536
+ },
537
+ { initialValue: 0 },
538
+ )
554
539
 
555
540
  expect(sum.get()).toBe(0)
556
541
 
@@ -566,10 +551,13 @@ describe('Computed', () => {
566
551
 
567
552
  test('should handle array accumulation with oldValue', () => {
568
553
  const item = new State('')
569
- const items = new Memo((oldValue: string[]) => {
570
- const newItem = item.get()
571
- return newItem === '' ? oldValue : [...oldValue, newItem]
572
- }, [] as string[])
554
+ const items = new Memo(
555
+ (oldValue: string[]) => {
556
+ const newItem = item.get()
557
+ return newItem === '' ? oldValue : [...oldValue, newItem]
558
+ },
559
+ { initialValue: [] as string[] },
560
+ )
573
561
 
574
562
  expect(items.get()).toEqual([])
575
563
 
@@ -586,11 +574,16 @@ describe('Computed', () => {
586
574
  test('should handle counter with oldValue and multiple dependencies', () => {
587
575
  const reset = new State(false)
588
576
  const add = new State(0)
589
- const counter = new Memo((oldValue: number) => {
590
- if (reset.get()) return 0
591
- const increment = add.get()
592
- return increment === 0 ? oldValue : oldValue + increment
593
- }, 0)
577
+ const counter = new Memo(
578
+ (oldValue: number) => {
579
+ if (reset.get()) return 0
580
+ const increment = add.get()
581
+ return increment === 0 ? oldValue : oldValue + increment
582
+ },
583
+ {
584
+ initialValue: 0,
585
+ },
586
+ )
594
587
 
595
588
  expect(counter.get()).toBe(0)
596
589
 
@@ -623,11 +616,16 @@ describe('Computed', () => {
623
616
  test('should work with async computation and oldValue', async () => {
624
617
  let receivedOldValue: number | undefined
625
618
 
626
- const asyncComputed = new Task(async (oldValue: number) => {
627
- receivedOldValue = oldValue
628
- await wait(50)
629
- return oldValue + 5
630
- }, 10)
619
+ const asyncComputed = new Task(
620
+ async (oldValue: number) => {
621
+ receivedOldValue = oldValue
622
+ await wait(50)
623
+ return oldValue + 5
624
+ },
625
+ {
626
+ initialValue: 10,
627
+ },
628
+ )
631
629
 
632
630
  // Initially returns initialValue before async computation completes
633
631
  expect(asyncComputed.get()).toBe(10)
@@ -648,7 +646,7 @@ describe('Computed', () => {
648
646
  if (k === '' || v === '') return oldValue
649
647
  return { ...oldValue, [k]: v }
650
648
  },
651
- {} as Record<string, string>,
649
+ { initialValue: {} as Record<string, string> },
652
650
  )
653
651
 
654
652
  expect(obj.get()).toEqual({})
@@ -682,7 +680,9 @@ describe('Computed', () => {
682
680
 
683
681
  return source.get() + oldValue
684
682
  },
685
- 0,
683
+ {
684
+ initialValue: 0,
685
+ },
686
686
  )
687
687
 
688
688
  // Initial computation
@@ -704,14 +704,19 @@ describe('Computed', () => {
704
704
  const shouldError = new State(false)
705
705
  const counter = new State(1)
706
706
 
707
- const computed = new Memo((oldValue: number) => {
708
- if (shouldError.get()) {
709
- throw new Error('Computation failed')
710
- }
711
- // Handle UNSET case by treating it as 0
712
- const safeOldValue = oldValue === UNSET ? 0 : oldValue
713
- return safeOldValue + counter.get()
714
- }, 10)
707
+ const computed = new Memo(
708
+ (oldValue: number) => {
709
+ if (shouldError.get()) {
710
+ throw new Error('Computation failed')
711
+ }
712
+ // Handle UNSET case by treating it as 0
713
+ const safeOldValue = oldValue === UNSET ? 0 : oldValue
714
+ return safeOldValue + counter.get()
715
+ },
716
+ {
717
+ initialValue: 10,
718
+ },
719
+ )
715
720
 
716
721
  expect(computed.get()).toBe(11) // 10 + 1
717
722
 
@@ -736,23 +741,28 @@ describe('Computed', () => {
736
741
  >('increment')
737
742
  const amount = new State(1)
738
743
 
739
- const calculator = new Memo((oldValue: number) => {
740
- const act = action.get()
741
- const amt = amount.get()
742
-
743
- switch (act) {
744
- case 'increment':
745
- return oldValue + amt
746
- case 'decrement':
747
- return oldValue - amt
748
- case 'multiply':
749
- return oldValue * amt
750
- case 'reset':
751
- return 0
752
- default:
753
- return oldValue
754
- }
755
- }, 0)
744
+ const calculator = new Memo(
745
+ (oldValue: number) => {
746
+ const act = action.get()
747
+ const amt = amount.get()
748
+
749
+ switch (act) {
750
+ case 'increment':
751
+ return oldValue + amt
752
+ case 'decrement':
753
+ return oldValue - amt
754
+ case 'multiply':
755
+ return oldValue * amt
756
+ case 'reset':
757
+ return 0
758
+ default:
759
+ return oldValue
760
+ }
761
+ },
762
+ {
763
+ initialValue: 0,
764
+ },
765
+ )
756
766
 
757
767
  expect(calculator.get()).toBe(1) // 0 + 1
758
768
 
@@ -773,9 +783,10 @@ describe('Computed', () => {
773
783
 
774
784
  test('should handle edge cases with initialValue and oldValue', () => {
775
785
  // Test with null/undefined-like values
776
- const nullishComputed = new Memo((oldValue: string) => {
777
- return `${oldValue} updated`
778
- }, '')
786
+ const nullishComputed = new Memo(
787
+ oldValue => `${oldValue} updated`,
788
+ { initialValue: '' },
789
+ )
779
790
 
780
791
  expect(nullishComputed.get()).toBe(' updated')
781
792
 
@@ -794,9 +805,11 @@ describe('Computed', () => {
794
805
  items: [...oldValue.items, `item${oldValue.count + 1}`],
795
806
  }),
796
807
  {
797
- count: 0,
798
- items: [] as string[],
799
- meta: { created: now },
808
+ initialValue: {
809
+ count: 0,
810
+ items: [] as string[],
811
+ meta: { created: now },
812
+ },
800
813
  },
801
814
  )
802
815
 
@@ -808,18 +821,28 @@ describe('Computed', () => {
808
821
 
809
822
  test('should preserve initialValue type consistency', () => {
810
823
  // Test that oldValue type is consistent with initialValue
811
- const stringComputed = new Memo((oldValue: string) => {
812
- expect(typeof oldValue).toBe('string')
813
- return oldValue.toUpperCase()
814
- }, 'hello')
824
+ const stringComputed = new Memo(
825
+ (oldValue: string) => {
826
+ expect(typeof oldValue).toBe('string')
827
+ return oldValue.toUpperCase()
828
+ },
829
+ {
830
+ initialValue: 'hello',
831
+ },
832
+ )
815
833
 
816
834
  expect(stringComputed.get()).toBe('HELLO')
817
835
 
818
- const numberComputed = new Memo((oldValue: number) => {
819
- expect(typeof oldValue).toBe('number')
820
- expect(Number.isFinite(oldValue)).toBe(true)
821
- return oldValue * 2
822
- }, 5)
836
+ const numberComputed = new Memo(
837
+ (oldValue: number) => {
838
+ expect(typeof oldValue).toBe('number')
839
+ expect(Number.isFinite(oldValue)).toBe(true)
840
+ return oldValue * 2
841
+ },
842
+ {
843
+ initialValue: 5,
844
+ },
845
+ )
823
846
 
824
847
  expect(numberComputed.get()).toBe(10)
825
848
  })
@@ -829,12 +852,16 @@ describe('Computed', () => {
829
852
 
830
853
  const first = new Memo(
831
854
  (oldValue: number) => oldValue + source.get(),
832
- 10,
855
+ {
856
+ initialValue: 10,
857
+ },
833
858
  )
834
859
 
835
860
  const second = new Memo(
836
861
  (oldValue: number) => oldValue + first.get(),
837
- 20,
862
+ {
863
+ initialValue: 20,
864
+ },
838
865
  )
839
866
 
840
867
  expect(first.get()).toBe(11) // 10 + 1
@@ -849,10 +876,15 @@ describe('Computed', () => {
849
876
  const trigger = new State(0)
850
877
  let computationCount = 0
851
878
 
852
- const accumulator = new Memo((oldValue: number) => {
853
- computationCount++
854
- return oldValue + trigger.get()
855
- }, 100)
879
+ const accumulator = new Memo(
880
+ (oldValue: number) => {
881
+ computationCount++
882
+ return oldValue + trigger.get()
883
+ },
884
+ {
885
+ initialValue: 100,
886
+ },
887
+ )
856
888
 
857
889
  expect(accumulator.get()).toBe(100) // 100 + 0
858
890
  expect(computationCount).toBe(1)
@@ -868,30 +900,26 @@ describe('Computed', () => {
868
900
  })
869
901
  })
870
902
 
871
- describe('HOOK_WATCH - Lazy Resource Management', () => {
903
+ describe('Signal Options - Lazy Resource Management', () => {
872
904
  test('Memo - should manage external resources lazily', async () => {
873
905
  const source = new State(1)
874
906
  let counter = 0
875
907
  let intervalId: Timer | undefined
876
908
 
877
909
  // Create memo that depends on source
878
- const computed = new Memo((oldValue: number) => {
879
- return source.get() * 2 + (oldValue || 0)
880
- }, 0)
881
-
882
- // Add HOOK_WATCH callback that starts interval
883
- const cleanupHookCallback = computed.on(HOOK_WATCH, () => {
884
- intervalId = setInterval(() => {
885
- counter++
886
- }, 10) // Fast interval for testing
887
-
888
- // Return cleanup function
889
- return () => {
910
+ const computed = new Memo(oldValue => source.get() * 2 + oldValue, {
911
+ initialValue: 0,
912
+ watched: () => {
913
+ intervalId = setInterval(() => {
914
+ counter++
915
+ }, 10)
916
+ },
917
+ unwatched: () => {
890
918
  if (intervalId) {
891
919
  clearInterval(intervalId)
892
920
  intervalId = undefined
893
921
  }
894
- }
922
+ },
895
923
  })
896
924
 
897
925
  // Counter should not be running yet
@@ -900,7 +928,7 @@ describe('Computed', () => {
900
928
  expect(counter).toBe(0)
901
929
  expect(intervalId).toBeUndefined()
902
930
 
903
- // Effect subscribes to computed, triggering HOOK_WATCH
931
+ // Effect subscribes to computed, triggering watched callback
904
932
  const effectCleanup = createEffect(() => {
905
933
  computed.get()
906
934
  })
@@ -918,9 +946,6 @@ describe('Computed', () => {
918
946
  await wait(50)
919
947
  expect(counter).toBe(counterAfterStop)
920
948
  expect(intervalId).toBeUndefined()
921
-
922
- // Cleanup
923
- cleanupHookCallback()
924
949
  })
925
950
 
926
951
  test('Task - should manage external resources lazily', async () => {
@@ -938,23 +963,22 @@ describe('Computed', () => {
938
963
 
939
964
  return `${value}-processed-${oldValue || 'none'}`
940
965
  },
941
- 'default',
966
+ {
967
+ initialValue: 'default',
968
+ watched: () => {
969
+ intervalId = setInterval(() => {
970
+ counter++
971
+ }, 10)
972
+ },
973
+ unwatched: () => {
974
+ if (intervalId) {
975
+ clearInterval(intervalId)
976
+ intervalId = undefined
977
+ }
978
+ },
979
+ },
942
980
  )
943
981
 
944
- // Add HOOK_WATCH callback
945
- const cleanupHookCallback = computed.on(HOOK_WATCH, () => {
946
- intervalId = setInterval(() => {
947
- counter++
948
- }, 10)
949
-
950
- return () => {
951
- if (intervalId) {
952
- clearInterval(intervalId)
953
- intervalId = undefined
954
- }
955
- }
956
- })
957
-
958
982
  // Counter should not be running yet
959
983
  expect(counter).toBe(0)
960
984
  await wait(50)
@@ -979,26 +1003,24 @@ describe('Computed', () => {
979
1003
  await wait(50)
980
1004
  expect(counter).toBe(counterAfterStop)
981
1005
  expect(intervalId).toBeUndefined()
982
-
983
- // Cleanup
984
- cleanupHookCallback()
985
1006
  })
986
1007
 
987
1008
  test('Memo - multiple watchers should share resources', async () => {
988
1009
  const source = new State(10)
989
1010
  let subscriptionCount = 0
990
1011
 
991
- const computed = new Memo((oldValue: number) => {
992
- return source.get() + (oldValue || 0)
993
- }, 0)
994
-
995
- // HOOK_WATCH should only be called once for multiple watchers
996
- const cleanupHookCallback = computed.on(HOOK_WATCH, () => {
997
- subscriptionCount++
998
- return () => {
999
- subscriptionCount--
1000
- }
1001
- })
1012
+ const computed = new Memo(
1013
+ (oldValue: number) => source.get() + oldValue,
1014
+ {
1015
+ initialValue: 0,
1016
+ watched: () => {
1017
+ subscriptionCount++
1018
+ },
1019
+ unwatched: () => {
1020
+ subscriptionCount--
1021
+ },
1022
+ },
1023
+ )
1002
1024
 
1003
1025
  expect(subscriptionCount).toBe(0)
1004
1026
 
@@ -1020,13 +1042,11 @@ describe('Computed', () => {
1020
1042
  // Stop second effect
1021
1043
  effect2()
1022
1044
  expect(subscriptionCount).toBe(0) // Now cleaned up
1023
-
1024
- // Cleanup
1025
- cleanupHookCallback()
1026
1045
  })
1027
1046
 
1028
1047
  test('Task - should handle abort signals in external resources', async () => {
1029
1048
  const source = new State('test')
1049
+ let controller: AbortController | undefined
1030
1050
  const abortedControllers: AbortController[] = []
1031
1051
 
1032
1052
  const computed = new Task(
@@ -1035,37 +1055,38 @@ describe('Computed', () => {
1035
1055
  if (abort.aborted) throw new Error('Aborted')
1036
1056
  return `${source.get()}-${oldValue || 'initial'}`
1037
1057
  },
1038
- 'default',
1058
+ {
1059
+ initialValue: 'default',
1060
+ watched: () => {
1061
+ controller = new AbortController()
1062
+
1063
+ // Simulate external async operation (catch rejections to avoid unhandled errors)
1064
+ new Promise(resolve => {
1065
+ const timeout = setTimeout(() => {
1066
+ if (!controller) return
1067
+ if (controller.signal.aborted) {
1068
+ resolve('External operation aborted')
1069
+ } else {
1070
+ resolve('External operation completed')
1071
+ }
1072
+ }, 50)
1073
+
1074
+ controller?.signal.addEventListener('abort', () => {
1075
+ clearTimeout(timeout)
1076
+ resolve('External operation aborted')
1077
+ })
1078
+ }).catch(() => {
1079
+ // Ignore promise rejections in test
1080
+ })
1081
+ },
1082
+ unwatched: () => {
1083
+ if (!controller) return
1084
+ controller.abort()
1085
+ abortedControllers.push(controller)
1086
+ },
1087
+ },
1039
1088
  )
1040
1089
 
1041
- // HOOK_WATCH that creates external resources with abort handling
1042
- const cleanupHookCallback = computed.on(HOOK_WATCH, () => {
1043
- const controller = new AbortController()
1044
-
1045
- // Simulate external async operation (catch rejections to avoid unhandled errors)
1046
- new Promise(resolve => {
1047
- const timeout = setTimeout(() => {
1048
- if (controller.signal.aborted) {
1049
- resolve('External operation aborted')
1050
- } else {
1051
- resolve('External operation completed')
1052
- }
1053
- }, 50)
1054
-
1055
- controller.signal.addEventListener('abort', () => {
1056
- clearTimeout(timeout)
1057
- resolve('External operation aborted')
1058
- })
1059
- }).catch(() => {
1060
- // Ignore promise rejections in test
1061
- })
1062
-
1063
- return () => {
1064
- controller.abort()
1065
- abortedControllers.push(controller)
1066
- }
1067
- })
1068
-
1069
1090
  const effect1 = createEffect(() => {
1070
1091
  computed.get()
1071
1092
  })
@@ -1082,45 +1103,6 @@ describe('Computed', () => {
1082
1103
  // Should have aborted external controllers
1083
1104
  expect(abortedControllers.length).toBeGreaterThan(0)
1084
1105
  expect(abortedControllers[0].signal.aborted).toBe(true)
1085
-
1086
- // Cleanup
1087
- cleanupHookCallback()
1088
- })
1089
-
1090
- test('Exception handling in computed HOOK_WATCH callbacks', async () => {
1091
- const source = new State(1)
1092
- const computed = new Memo(() => source.get() * 2)
1093
-
1094
- let successfulCallbackCalled = false
1095
- let throwingCallbackCalled = false
1096
-
1097
- // Add throwing callback
1098
- const cleanup1 = computed.on(HOOK_WATCH, () => {
1099
- throwingCallbackCalled = true
1100
- throw new Error('Test error in computed HOOK_WATCH')
1101
- })
1102
-
1103
- // Add successful callback
1104
- const cleanup2 = computed.on(HOOK_WATCH, () => {
1105
- successfulCallbackCalled = true
1106
- return () => {
1107
- // cleanup
1108
- }
1109
- })
1110
-
1111
- // Trigger callbacks - should throw due to exception in callback
1112
- expect(() => computed.get()).toThrow(
1113
- 'Test error in computed HOOK_WATCH',
1114
- )
1115
-
1116
- // Throwing callback should have been called
1117
- expect(throwingCallbackCalled).toBe(true)
1118
- // Successful callback should also have been called (resilient collection)
1119
- expect(successfulCallbackCalled).toBe(true)
1120
-
1121
- // Cleanup
1122
- cleanup1()
1123
- cleanup2()
1124
1106
  })
1125
1107
  })
1126
1108
  })