@zeix/cause-effect 0.17.1 → 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 (57) hide show
  1. package/.ai-context.md +13 -0
  2. package/.github/copilot-instructions.md +4 -0
  3. package/.zed/settings.json +3 -0
  4. package/CLAUDE.md +41 -7
  5. package/README.md +48 -25
  6. package/archive/benchmark.ts +0 -5
  7. package/archive/collection.ts +6 -65
  8. package/archive/composite.ts +85 -0
  9. package/archive/computed.ts +18 -20
  10. package/archive/list.ts +7 -75
  11. package/archive/memo.ts +15 -15
  12. package/archive/state.ts +2 -1
  13. package/archive/store.ts +8 -78
  14. package/archive/task.ts +20 -25
  15. package/index.dev.js +508 -526
  16. package/index.js +1 -1
  17. package/index.ts +9 -11
  18. package/package.json +6 -6
  19. package/src/classes/collection.ts +70 -107
  20. package/src/classes/computed.ts +165 -149
  21. package/src/classes/list.ts +145 -107
  22. package/src/classes/ref.ts +19 -17
  23. package/src/classes/state.ts +21 -17
  24. package/src/classes/store.ts +125 -73
  25. package/src/diff.ts +2 -1
  26. package/src/effect.ts +17 -10
  27. package/src/errors.ts +14 -1
  28. package/src/resolve.ts +1 -1
  29. package/src/signal.ts +3 -2
  30. package/src/system.ts +159 -61
  31. package/src/util.ts +0 -6
  32. package/test/batch.test.ts +4 -11
  33. package/test/benchmark.test.ts +4 -2
  34. package/test/collection.test.ts +106 -107
  35. package/test/computed.test.ts +351 -112
  36. package/test/effect.test.ts +2 -2
  37. package/test/list.test.ts +62 -102
  38. package/test/ref.test.ts +128 -2
  39. package/test/state.test.ts +16 -22
  40. package/test/store.test.ts +101 -108
  41. package/test/util/dependency-graph.ts +2 -2
  42. package/tsconfig.build.json +11 -0
  43. package/tsconfig.json +5 -7
  44. package/types/index.d.ts +3 -3
  45. package/types/src/classes/collection.d.ts +9 -10
  46. package/types/src/classes/computed.d.ts +17 -20
  47. package/types/src/classes/list.d.ts +8 -6
  48. package/types/src/classes/ref.d.ts +8 -12
  49. package/types/src/classes/state.d.ts +5 -8
  50. package/types/src/classes/store.d.ts +14 -13
  51. package/types/src/effect.d.ts +1 -2
  52. package/types/src/errors.d.ts +2 -1
  53. package/types/src/signal.d.ts +3 -2
  54. package/types/src/system.d.ts +47 -34
  55. package/types/src/util.d.ts +1 -2
  56. package/src/classes/composite.ts +0 -176
  57. package/types/src/classes/composite.d.ts +0 -15
@@ -229,7 +229,7 @@ describe('Computed', () => {
229
229
  test('should detect and throw error for circular dependencies', () => {
230
230
  const a = new State(1)
231
231
  const b = new Memo(() => c.get() + 1)
232
- const c = new Memo(() => b.get() + a.get())
232
+ const c = new Memo((): number => b.get() + a.get())
233
233
  expect(() => {
234
234
  b.get() // This should trigger the circular dependency
235
235
  }).toThrow('Circular dependency detected in memo')
@@ -268,7 +268,7 @@ describe('Computed', () => {
268
268
  expect(a.get()).toBe(1)
269
269
  expect(true).toBe(false) // This line should not be reached
270
270
  } catch (error) {
271
- expect(error.message).toBe('Calculation error')
271
+ expect((error as Error).message).toBe('Calculation error')
272
272
  } finally {
273
273
  expect(c.get()).toBe('c: recovered')
274
274
  expect(okCount).toBe(2)
@@ -429,73 +429,49 @@ describe('Computed', () => {
429
429
  expect(() => {
430
430
  // @ts-expect-error - Testing invalid input
431
431
  new Memo(null)
432
- }).toThrow('Invalid memo callback null')
432
+ }).toThrow('Invalid Memo callback null')
433
433
 
434
434
  expect(() => {
435
435
  // @ts-expect-error - Testing invalid input
436
436
  new Memo(undefined)
437
- }).toThrow('Invalid memo callback undefined')
437
+ }).toThrow('Invalid Memo callback undefined')
438
438
 
439
439
  expect(() => {
440
440
  // @ts-expect-error - Testing invalid input
441
441
  new Memo(42)
442
- }).toThrow('Invalid memo callback 42')
442
+ }).toThrow('Invalid Memo callback 42')
443
443
 
444
444
  expect(() => {
445
445
  // @ts-expect-error - Testing invalid input
446
446
  new Memo('not a function')
447
- }).toThrow('Invalid memo callback "not a function"')
447
+ }).toThrow('Invalid Memo callback "not a function"')
448
448
 
449
449
  expect(() => {
450
450
  // @ts-expect-error - Testing invalid input
451
451
  new Memo({ not: 'a function' })
452
- }).toThrow('Invalid memo callback {"not":"a function"}')
452
+ }).toThrow('Invalid Memo callback {"not":"a function"}')
453
453
 
454
454
  expect(() => {
455
455
  // @ts-expect-error - Testing invalid input
456
456
  new Memo((_a: unknown, _b: unknown, _c: unknown) => 42)
457
- }).toThrow('Invalid memo callback (_a, _b, _c) => 42')
457
+ }).toThrow('Invalid Memo callback (_a, _b, _c) => 42')
458
458
 
459
459
  expect(() => {
460
460
  // @ts-expect-error - Testing invalid input
461
461
  new Memo(async (_a: unknown, _b: unknown) => 42)
462
- }).toThrow('Invalid memo callback async (_a, _b) => 42')
462
+ }).toThrow('Invalid Memo callback async (_a, _b) => 42')
463
463
 
464
464
  expect(() => {
465
465
  // @ts-expect-error - Testing invalid input
466
466
  new Task((_a: unknown) => 42)
467
- }).toThrow('Invalid task callback (_a) => 42')
467
+ }).toThrow('Invalid Task callback (_a) => 42')
468
468
  })
469
469
 
470
- test('should throw NullishSignalValueError when initialValue is null', () => {
470
+ test('should expect type error if null is passed for options.initialValue', () => {
471
471
  expect(() => {
472
472
  // @ts-expect-error - Testing invalid input
473
- new Memo(() => 42, null)
474
- }).toThrow('Nullish signal values are not allowed in memo')
475
- })
476
-
477
- test('should throw specific error types for invalid inputs', () => {
478
- try {
479
- // @ts-expect-error - Testing invalid input
480
- new Memo(null)
481
- expect(true).toBe(false) // Should not reach here
482
- } catch (error) {
483
- expect(error).toBeInstanceOf(TypeError)
484
- expect(error.name).toBe('InvalidCallbackError')
485
- expect(error.message).toBe('Invalid memo callback null')
486
- }
487
-
488
- try {
489
- // @ts-expect-error - Testing invalid input
490
- new Memo(() => 42, null)
491
- expect(true).toBe(false) // Should not reach here
492
- } catch (error) {
493
- expect(error).toBeInstanceOf(TypeError)
494
- expect(error.name).toBe('NullishSignalValueError')
495
- expect(error.message).toBe(
496
- 'Nullish signal values are not allowed in memo',
497
- )
498
- }
473
+ new Memo(() => 42, { initialValue: null })
474
+ }).not.toThrow()
499
475
  })
500
476
 
501
477
  test('should allow valid callbacks and non-nullish initialValues', () => {
@@ -505,36 +481,43 @@ describe('Computed', () => {
505
481
  }).not.toThrow()
506
482
 
507
483
  expect(() => {
508
- new Memo(() => 42, 0)
484
+ new Memo(() => 42, { initialValue: 0 })
509
485
  }).not.toThrow()
510
486
 
511
487
  expect(() => {
512
- new Memo(() => 'foo', '')
488
+ new Memo(() => 'foo', { initialValue: '' })
513
489
  }).not.toThrow()
514
490
 
515
491
  expect(() => {
516
- new Memo(() => true, false)
492
+ new Memo(() => true, { initialValue: false })
517
493
  }).not.toThrow()
518
494
 
519
495
  expect(() => {
520
- new Task(async () => ({ id: 42, name: 'John' }), UNSET)
496
+ new Task(async () => ({ id: 42, name: 'John' }), {
497
+ initialValue: UNSET,
498
+ })
521
499
  }).not.toThrow()
522
500
  })
523
501
  })
524
502
 
525
503
  describe('Initial Value and Old Value', () => {
526
504
  test('should use initialValue when provided', () => {
527
- const computed = new Memo((oldValue: number) => oldValue + 1, 10)
505
+ const computed = new Memo((oldValue: number) => oldValue + 1, {
506
+ initialValue: 10,
507
+ })
528
508
  expect(computed.get()).toBe(11)
529
509
  })
530
510
 
531
511
  test('should pass current value as oldValue to callback', () => {
532
512
  const state = new State(5)
533
513
  let receivedOldValue: number | undefined
534
- const computed = new Memo((oldValue: number) => {
535
- receivedOldValue = oldValue
536
- return state.get() * 2
537
- }, 0)
514
+ const computed = new Memo(
515
+ (oldValue: number) => {
516
+ receivedOldValue = oldValue
517
+ return state.get() * 2
518
+ },
519
+ { initialValue: 0 },
520
+ )
538
521
 
539
522
  expect(computed.get()).toBe(10)
540
523
  expect(receivedOldValue).toBe(0)
@@ -546,10 +529,13 @@ describe('Computed', () => {
546
529
 
547
530
  test('should work as reducer function with oldValue', () => {
548
531
  const increment = new State(0)
549
- const sum = new Memo((oldValue: number) => {
550
- const inc = increment.get()
551
- return inc === 0 ? oldValue : oldValue + inc
552
- }, 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
+ )
553
539
 
554
540
  expect(sum.get()).toBe(0)
555
541
 
@@ -565,10 +551,13 @@ describe('Computed', () => {
565
551
 
566
552
  test('should handle array accumulation with oldValue', () => {
567
553
  const item = new State('')
568
- const items = new Memo((oldValue: string[]) => {
569
- const newItem = item.get()
570
- return newItem === '' ? oldValue : [...oldValue, newItem]
571
- }, [] 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
+ )
572
561
 
573
562
  expect(items.get()).toEqual([])
574
563
 
@@ -585,11 +574,16 @@ describe('Computed', () => {
585
574
  test('should handle counter with oldValue and multiple dependencies', () => {
586
575
  const reset = new State(false)
587
576
  const add = new State(0)
588
- const counter = new Memo((oldValue: number) => {
589
- if (reset.get()) return 0
590
- const increment = add.get()
591
- return increment === 0 ? oldValue : oldValue + increment
592
- }, 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
+ )
593
587
 
594
588
  expect(counter.get()).toBe(0)
595
589
 
@@ -622,11 +616,16 @@ describe('Computed', () => {
622
616
  test('should work with async computation and oldValue', async () => {
623
617
  let receivedOldValue: number | undefined
624
618
 
625
- const asyncComputed = new Task(async (oldValue: number) => {
626
- receivedOldValue = oldValue
627
- await wait(50)
628
- return oldValue + 5
629
- }, 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
+ )
630
629
 
631
630
  // Initially returns initialValue before async computation completes
632
631
  expect(asyncComputed.get()).toBe(10)
@@ -647,7 +646,7 @@ describe('Computed', () => {
647
646
  if (k === '' || v === '') return oldValue
648
647
  return { ...oldValue, [k]: v }
649
648
  },
650
- {} as Record<string, string>,
649
+ { initialValue: {} as Record<string, string> },
651
650
  )
652
651
 
653
652
  expect(obj.get()).toEqual({})
@@ -681,7 +680,9 @@ describe('Computed', () => {
681
680
 
682
681
  return source.get() + oldValue
683
682
  },
684
- 0,
683
+ {
684
+ initialValue: 0,
685
+ },
685
686
  )
686
687
 
687
688
  // Initial computation
@@ -703,14 +704,19 @@ describe('Computed', () => {
703
704
  const shouldError = new State(false)
704
705
  const counter = new State(1)
705
706
 
706
- const computed = new Memo((oldValue: number) => {
707
- if (shouldError.get()) {
708
- throw new Error('Computation failed')
709
- }
710
- // Handle UNSET case by treating it as 0
711
- const safeOldValue = oldValue === UNSET ? 0 : oldValue
712
- return safeOldValue + counter.get()
713
- }, 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
+ )
714
720
 
715
721
  expect(computed.get()).toBe(11) // 10 + 1
716
722
 
@@ -735,23 +741,28 @@ describe('Computed', () => {
735
741
  >('increment')
736
742
  const amount = new State(1)
737
743
 
738
- const calculator = new Memo((oldValue: number) => {
739
- const act = action.get()
740
- const amt = amount.get()
741
-
742
- switch (act) {
743
- case 'increment':
744
- return oldValue + amt
745
- case 'decrement':
746
- return oldValue - amt
747
- case 'multiply':
748
- return oldValue * amt
749
- case 'reset':
750
- return 0
751
- default:
752
- return oldValue
753
- }
754
- }, 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
+ )
755
766
 
756
767
  expect(calculator.get()).toBe(1) // 0 + 1
757
768
 
@@ -772,9 +783,10 @@ describe('Computed', () => {
772
783
 
773
784
  test('should handle edge cases with initialValue and oldValue', () => {
774
785
  // Test with null/undefined-like values
775
- const nullishComputed = new Memo((oldValue: string) => {
776
- return `${oldValue} updated`
777
- }, '')
786
+ const nullishComputed = new Memo(
787
+ oldValue => `${oldValue} updated`,
788
+ { initialValue: '' },
789
+ )
778
790
 
779
791
  expect(nullishComputed.get()).toBe(' updated')
780
792
 
@@ -793,9 +805,11 @@ describe('Computed', () => {
793
805
  items: [...oldValue.items, `item${oldValue.count + 1}`],
794
806
  }),
795
807
  {
796
- count: 0,
797
- items: [] as string[],
798
- meta: { created: now },
808
+ initialValue: {
809
+ count: 0,
810
+ items: [] as string[],
811
+ meta: { created: now },
812
+ },
799
813
  },
800
814
  )
801
815
 
@@ -807,18 +821,28 @@ describe('Computed', () => {
807
821
 
808
822
  test('should preserve initialValue type consistency', () => {
809
823
  // Test that oldValue type is consistent with initialValue
810
- const stringComputed = new Memo((oldValue: string) => {
811
- expect(typeof oldValue).toBe('string')
812
- return oldValue.toUpperCase()
813
- }, '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
+ )
814
833
 
815
834
  expect(stringComputed.get()).toBe('HELLO')
816
835
 
817
- const numberComputed = new Memo((oldValue: number) => {
818
- expect(typeof oldValue).toBe('number')
819
- expect(Number.isFinite(oldValue)).toBe(true)
820
- return oldValue * 2
821
- }, 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
+ )
822
846
 
823
847
  expect(numberComputed.get()).toBe(10)
824
848
  })
@@ -828,12 +852,16 @@ describe('Computed', () => {
828
852
 
829
853
  const first = new Memo(
830
854
  (oldValue: number) => oldValue + source.get(),
831
- 10,
855
+ {
856
+ initialValue: 10,
857
+ },
832
858
  )
833
859
 
834
860
  const second = new Memo(
835
861
  (oldValue: number) => oldValue + first.get(),
836
- 20,
862
+ {
863
+ initialValue: 20,
864
+ },
837
865
  )
838
866
 
839
867
  expect(first.get()).toBe(11) // 10 + 1
@@ -848,10 +876,15 @@ describe('Computed', () => {
848
876
  const trigger = new State(0)
849
877
  let computationCount = 0
850
878
 
851
- const accumulator = new Memo((oldValue: number) => {
852
- computationCount++
853
- return oldValue + trigger.get()
854
- }, 100)
879
+ const accumulator = new Memo(
880
+ (oldValue: number) => {
881
+ computationCount++
882
+ return oldValue + trigger.get()
883
+ },
884
+ {
885
+ initialValue: 100,
886
+ },
887
+ )
855
888
 
856
889
  expect(accumulator.get()).toBe(100) // 100 + 0
857
890
  expect(computationCount).toBe(1)
@@ -866,4 +899,210 @@ describe('Computed', () => {
866
899
  expect(accumulator.get()).toBe(115) // Final accumulated value
867
900
  })
868
901
  })
902
+
903
+ describe('Signal Options - Lazy Resource Management', () => {
904
+ test('Memo - should manage external resources lazily', async () => {
905
+ const source = new State(1)
906
+ let counter = 0
907
+ let intervalId: Timer | undefined
908
+
909
+ // Create memo that depends on source
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: () => {
918
+ if (intervalId) {
919
+ clearInterval(intervalId)
920
+ intervalId = undefined
921
+ }
922
+ },
923
+ })
924
+
925
+ // Counter should not be running yet
926
+ expect(counter).toBe(0)
927
+ await wait(50)
928
+ expect(counter).toBe(0)
929
+ expect(intervalId).toBeUndefined()
930
+
931
+ // Effect subscribes to computed, triggering watched callback
932
+ const effectCleanup = createEffect(() => {
933
+ computed.get()
934
+ })
935
+
936
+ // Counter should now be running
937
+ await wait(50)
938
+ expect(counter).toBeGreaterThan(0)
939
+ expect(intervalId).toBeDefined()
940
+
941
+ // Stop effect, should cleanup resources
942
+ effectCleanup()
943
+ const counterAfterStop = counter
944
+
945
+ // Counter should stop incrementing
946
+ await wait(50)
947
+ expect(counter).toBe(counterAfterStop)
948
+ expect(intervalId).toBeUndefined()
949
+ })
950
+
951
+ test('Task - should manage external resources lazily', async () => {
952
+ const source = new State('initial')
953
+ let counter = 0
954
+ let intervalId: Timer | undefined
955
+
956
+ // Create task that depends on source
957
+ const computed = new Task(
958
+ async (oldValue: string, abort: AbortSignal) => {
959
+ const value = source.get()
960
+ await wait(10) // Simulate async work
961
+
962
+ if (abort.aborted) throw new Error('Aborted')
963
+
964
+ return `${value}-processed-${oldValue || 'none'}`
965
+ },
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
+ },
980
+ )
981
+
982
+ // Counter should not be running yet
983
+ expect(counter).toBe(0)
984
+ await wait(50)
985
+ expect(counter).toBe(0)
986
+ expect(intervalId).toBeUndefined()
987
+
988
+ // Effect subscribes to computed
989
+ const effectCleanup = createEffect(() => {
990
+ computed.get()
991
+ })
992
+
993
+ // Wait for async computation and counter to start
994
+ await wait(100)
995
+ expect(counter).toBeGreaterThan(0)
996
+ expect(intervalId).toBeDefined()
997
+
998
+ // Stop effect
999
+ effectCleanup()
1000
+ const counterAfterStop = counter
1001
+
1002
+ // Counter should stop incrementing
1003
+ await wait(50)
1004
+ expect(counter).toBe(counterAfterStop)
1005
+ expect(intervalId).toBeUndefined()
1006
+ })
1007
+
1008
+ test('Memo - multiple watchers should share resources', async () => {
1009
+ const source = new State(10)
1010
+ let subscriptionCount = 0
1011
+
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
+ )
1024
+
1025
+ expect(subscriptionCount).toBe(0)
1026
+
1027
+ // Create multiple effects
1028
+ const effect1 = createEffect(() => {
1029
+ computed.get()
1030
+ })
1031
+ const effect2 = createEffect(() => {
1032
+ computed.get()
1033
+ })
1034
+
1035
+ // Should only increment once
1036
+ expect(subscriptionCount).toBe(1)
1037
+
1038
+ // Stop first effect
1039
+ effect1()
1040
+ expect(subscriptionCount).toBe(1) // Still active due to second watcher
1041
+
1042
+ // Stop second effect
1043
+ effect2()
1044
+ expect(subscriptionCount).toBe(0) // Now cleaned up
1045
+ })
1046
+
1047
+ test('Task - should handle abort signals in external resources', async () => {
1048
+ const source = new State('test')
1049
+ let controller: AbortController | undefined
1050
+ const abortedControllers: AbortController[] = []
1051
+
1052
+ const computed = new Task(
1053
+ async (oldValue: string, abort: AbortSignal) => {
1054
+ await wait(20)
1055
+ if (abort.aborted) throw new Error('Aborted')
1056
+ return `${source.get()}-${oldValue || 'initial'}`
1057
+ },
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
+ },
1088
+ )
1089
+
1090
+ const effect1 = createEffect(() => {
1091
+ computed.get()
1092
+ })
1093
+
1094
+ // Change source to trigger recomputation
1095
+ source.set('updated')
1096
+
1097
+ // Stop effect to trigger cleanup
1098
+ effect1()
1099
+
1100
+ // Wait for cleanup to complete
1101
+ await wait(100)
1102
+
1103
+ // Should have aborted external controllers
1104
+ expect(abortedControllers.length).toBeGreaterThan(0)
1105
+ expect(abortedControllers[0].signal.aborted).toBe(true)
1106
+ })
1107
+ })
869
1108
  })
@@ -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
- 'Effect callback error:',
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
- 'Async effect error:',
510
+ 'Error in async effect callback:',
511
511
  expect.any(Error),
512
512
  )
513
513
  } finally {