@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.
Files changed (48) hide show
  1. package/.ai-context.md +7 -0
  2. package/.github/copilot-instructions.md +4 -0
  3. package/CLAUDE.md +96 -1
  4. package/README.md +44 -7
  5. package/archive/collection.ts +23 -25
  6. package/archive/computed.ts +3 -2
  7. package/archive/list.ts +21 -28
  8. package/archive/memo.ts +2 -1
  9. package/archive/state.ts +2 -1
  10. package/archive/store.ts +21 -32
  11. package/archive/task.ts +4 -7
  12. package/index.dev.js +356 -198
  13. package/index.js +1 -1
  14. package/index.ts +15 -6
  15. package/package.json +1 -1
  16. package/src/classes/collection.ts +69 -53
  17. package/src/classes/composite.ts +28 -33
  18. package/src/classes/computed.ts +87 -28
  19. package/src/classes/list.ts +31 -26
  20. package/src/classes/ref.ts +33 -5
  21. package/src/classes/state.ts +41 -8
  22. package/src/classes/store.ts +47 -30
  23. package/src/diff.ts +2 -1
  24. package/src/effect.ts +19 -9
  25. package/src/errors.ts +10 -1
  26. package/src/resolve.ts +1 -1
  27. package/src/signal.ts +0 -1
  28. package/src/system.ts +159 -43
  29. package/src/util.ts +0 -6
  30. package/test/collection.test.ts +279 -20
  31. package/test/computed.test.ts +268 -11
  32. package/test/effect.test.ts +2 -2
  33. package/test/list.test.ts +249 -21
  34. package/test/ref.test.ts +154 -0
  35. package/test/state.test.ts +13 -13
  36. package/test/store.test.ts +473 -28
  37. package/types/index.d.ts +3 -3
  38. package/types/src/classes/collection.d.ts +8 -7
  39. package/types/src/classes/composite.d.ts +4 -4
  40. package/types/src/classes/computed.d.ts +17 -0
  41. package/types/src/classes/list.d.ts +2 -2
  42. package/types/src/classes/ref.d.ts +10 -1
  43. package/types/src/classes/state.d.ts +9 -0
  44. package/types/src/classes/store.d.ts +4 -4
  45. package/types/src/effect.d.ts +1 -2
  46. package/types/src/errors.d.ts +4 -1
  47. package/types/src/system.d.ts +40 -24
  48. package/types/src/util.d.ts +1 -2
@@ -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 {
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('emits sort notification with new order', () => {
133
+ test('triggers HOOK_SORT with new order', () => {
134
134
  const numbers = new List([3, 1, 2])
135
- let sortNotification: readonly string[] = []
135
+ let order: readonly string[] | undefined
136
136
  numbers.on('sort', sort => {
137
- sortNotification = sort
137
+ order = sort
138
138
  })
139
139
  numbers.sort()
140
- expect(sortNotification).toHaveLength(3)
141
- expect(sortNotification).toEqual(['1', '2', '0'])
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('change notifications', () => {
306
- test('emits add notifications', () => {
305
+ describe('Hooks', () => {
306
+ test('trigger HOOK_ADD when adding items', () => {
307
307
  const numbers = new List([1, 2])
308
- let arrayAddNotification: readonly string[] = []
308
+ let addedKeys: readonly string[] | undefined
309
309
  let newArray: number[] = []
310
310
  numbers.on('add', add => {
311
- arrayAddNotification = add
311
+ addedKeys = add
312
312
  newArray = numbers.get()
313
313
  })
314
314
  numbers.add(3)
315
- expect(arrayAddNotification).toHaveLength(1)
315
+ expect(addedKeys).toHaveLength(1)
316
316
  expect(newArray).toEqual([1, 2, 3])
317
317
  })
318
318
 
319
- test('emits change notifications when properties are modified', () => {
319
+ test('triggers HOOK_CHANGE when properties are modified', () => {
320
320
  const items = new List([{ value: 10 }])
321
- let arrayChangeNotification: readonly string[] = []
321
+ let changedKeys: readonly string[] | undefined
322
322
  let newArray: { value: number }[] = []
323
323
  items.on('change', change => {
324
- arrayChangeNotification = change
324
+ changedKeys = change
325
325
  newArray = items.get()
326
326
  })
327
327
  items.at(0)?.set({ value: 20 })
328
- expect(arrayChangeNotification).toHaveLength(1)
328
+ expect(changedKeys).toHaveLength(1)
329
329
  expect(newArray).toEqual([{ value: 20 }])
330
330
  })
331
331
 
332
- test('emits remove notifications when items are removed', () => {
332
+ test('triggers HOOK_REMOVE when items are removed', () => {
333
333
  const items = new List([1, 2, 3])
334
- let arrayRemoveNotification: readonly string[] = []
334
+ let removedKeys: readonly string[] | undefined
335
335
  let newArray: number[] = []
336
336
  items.on('remove', remove => {
337
- arrayRemoveNotification = remove
337
+ removedKeys = remove
338
338
  newArray = items.get()
339
339
  })
340
340
  items.remove(1)
341
- expect(arrayRemoveNotification).toHaveLength(1)
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
+ })
@@ -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 state')
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 state')
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 state')
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 state')
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 state',
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 state',
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 state update callback null')
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 state update callback undefined')
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 state update callback "not a function"')
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 state update callback 42')
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 state update callback null',
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 state')
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 state')
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)