@zeix/cause-effect 0.17.2 → 0.18.0

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 (94) hide show
  1. package/.ai-context.md +163 -226
  2. package/.cursorrules +41 -35
  3. package/.github/copilot-instructions.md +166 -116
  4. package/.zed/settings.json +3 -0
  5. package/ARCHITECTURE.md +274 -0
  6. package/CLAUDE.md +197 -202
  7. package/COLLECTION_REFACTORING.md +161 -0
  8. package/GUIDE.md +298 -0
  9. package/README.md +241 -220
  10. package/REQUIREMENTS.md +100 -0
  11. package/bench/reactivity.bench.ts +577 -0
  12. package/index.dev.js +1326 -1174
  13. package/index.js +1 -1
  14. package/index.ts +58 -85
  15. package/package.json +9 -6
  16. package/src/errors.ts +118 -70
  17. package/src/graph.ts +601 -0
  18. package/src/nodes/collection.ts +474 -0
  19. package/src/nodes/effect.ts +149 -0
  20. package/src/nodes/list.ts +588 -0
  21. package/src/nodes/memo.ts +120 -0
  22. package/src/nodes/sensor.ts +139 -0
  23. package/src/nodes/state.ts +135 -0
  24. package/src/nodes/store.ts +383 -0
  25. package/src/nodes/task.ts +146 -0
  26. package/src/signal.ts +112 -64
  27. package/src/util.ts +26 -57
  28. package/test/batch.test.ts +96 -69
  29. package/test/benchmark.test.ts +473 -485
  30. package/test/collection.test.ts +455 -955
  31. package/test/effect.test.ts +293 -696
  32. package/test/list.test.ts +332 -857
  33. package/test/memo.test.ts +380 -0
  34. package/test/regression.test.ts +156 -0
  35. package/test/scope.test.ts +191 -0
  36. package/test/sensor.test.ts +454 -0
  37. package/test/signal.test.ts +220 -213
  38. package/test/state.test.ts +217 -271
  39. package/test/store.test.ts +346 -898
  40. package/test/task.test.ts +395 -0
  41. package/test/untrack.test.ts +167 -0
  42. package/test/util/dependency-graph.ts +2 -2
  43. package/tsconfig.build.json +11 -0
  44. package/tsconfig.json +5 -7
  45. package/types/index.d.ts +13 -15
  46. package/types/src/errors.d.ts +73 -19
  47. package/types/src/graph.d.ts +208 -0
  48. package/types/src/nodes/collection.d.ts +64 -0
  49. package/types/src/nodes/effect.d.ts +48 -0
  50. package/types/src/nodes/list.d.ts +65 -0
  51. package/types/src/nodes/memo.d.ts +57 -0
  52. package/types/src/nodes/sensor.d.ts +75 -0
  53. package/types/src/nodes/state.d.ts +78 -0
  54. package/types/src/nodes/store.d.ts +51 -0
  55. package/types/src/nodes/task.d.ts +73 -0
  56. package/types/src/signal.d.ts +43 -28
  57. package/types/src/util.d.ts +9 -16
  58. package/archive/benchmark.ts +0 -688
  59. package/archive/collection.ts +0 -310
  60. package/archive/computed.ts +0 -198
  61. package/archive/list.ts +0 -544
  62. package/archive/memo.ts +0 -140
  63. package/archive/state.ts +0 -90
  64. package/archive/store.ts +0 -357
  65. package/archive/task.ts +0 -191
  66. package/src/classes/collection.ts +0 -298
  67. package/src/classes/composite.ts +0 -171
  68. package/src/classes/computed.ts +0 -392
  69. package/src/classes/list.ts +0 -310
  70. package/src/classes/ref.ts +0 -96
  71. package/src/classes/state.ts +0 -131
  72. package/src/classes/store.ts +0 -227
  73. package/src/diff.ts +0 -138
  74. package/src/effect.ts +0 -96
  75. package/src/match.ts +0 -45
  76. package/src/resolve.ts +0 -49
  77. package/src/system.ts +0 -275
  78. package/test/computed.test.ts +0 -1126
  79. package/test/diff.test.ts +0 -955
  80. package/test/match.test.ts +0 -388
  81. package/test/ref.test.ts +0 -381
  82. package/test/resolve.test.ts +0 -154
  83. package/types/src/classes/collection.d.ts +0 -47
  84. package/types/src/classes/composite.d.ts +0 -15
  85. package/types/src/classes/computed.d.ts +0 -114
  86. package/types/src/classes/list.d.ts +0 -41
  87. package/types/src/classes/ref.d.ts +0 -48
  88. package/types/src/classes/state.d.ts +0 -61
  89. package/types/src/classes/store.d.ts +0 -51
  90. package/types/src/diff.d.ts +0 -28
  91. package/types/src/effect.d.ts +0 -15
  92. package/types/src/match.d.ts +0 -21
  93. package/types/src/resolve.d.ts +0 -29
  94. package/types/src/system.d.ts +0 -81
@@ -1,343 +1,289 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
- import { isComputed, isState, State } from '../index.ts'
2
+ import { createEffect, createState, isMemo, isState } from '../index.ts'
3
3
 
4
4
  /* === Tests === */
5
5
 
6
6
  describe('State', () => {
7
- describe('State type guard', () => {
8
- test('isState identifies state signals', () => {
9
- const count = new State(42)
10
- expect(isState(count)).toBe(true)
11
- expect(isComputed(count)).toBe(false)
12
- })
13
- })
14
-
15
- describe('Boolean cause', () => {
16
- test('should be boolean', () => {
17
- const cause = new State(false)
18
- expect(typeof cause.get()).toBe('boolean')
7
+ describe('createState', () => {
8
+ test('should return initial value from get()', () => {
9
+ const count = createState(0)
10
+ expect(count.get()).toBe(0)
19
11
  })
20
12
 
21
- test('should set initial value to false', () => {
22
- const cause = new State(false)
23
- expect(cause.get()).toBe(false)
13
+ test('should work with different value types', () => {
14
+ expect(createState(false).get()).toBe(false)
15
+ expect(createState('foo').get()).toBe('foo')
16
+ expect(createState([1, 2, 3]).get()).toEqual([1, 2, 3])
17
+ expect(createState({ a: 1 }).get()).toEqual({ a: 1 })
24
18
  })
25
19
 
26
- test('should set initial value to true', () => {
27
- const cause = new State(true)
28
- expect(cause.get()).toBe(true)
20
+ test('should have Symbol.toStringTag of "State"', () => {
21
+ const state = createState(0)
22
+ expect(state[Symbol.toStringTag]).toBe('State')
29
23
  })
24
+ })
30
25
 
31
- test('should set new value with .set(true)', () => {
32
- const cause = new State(false)
33
- cause.set(true)
34
- expect(cause.get()).toBe(true)
26
+ describe('isState', () => {
27
+ test('should identify state signals', () => {
28
+ expect(isState(createState(0))).toBe(true)
35
29
  })
36
30
 
37
- test('should toggle initial value with .set(v => !v)', () => {
38
- const cause = new State(false)
39
- cause.update(v => !v)
40
- expect(cause.get()).toBe(true)
31
+ test('should return false for non-state values', () => {
32
+ expect(isState(42)).toBe(false)
33
+ expect(isState(null)).toBe(false)
34
+ expect(isState({})).toBe(false)
35
+ expect(isMemo(createState(0))).toBe(false)
41
36
  })
42
37
  })
43
38
 
44
- describe('Number cause', () => {
45
- test('should be number', () => {
46
- const cause = new State(0)
47
- expect(typeof cause.get()).toBe('number')
39
+ describe('set', () => {
40
+ test('should update value', () => {
41
+ const state = createState(0)
42
+ state.set(42)
43
+ expect(state.get()).toBe(42)
48
44
  })
49
45
 
50
- test('should set initial value to 0', () => {
51
- const cause = new State(0)
52
- expect(cause.get()).toBe(0)
46
+ test('should replace value entirely for objects', () => {
47
+ const state = createState<Record<string, unknown>>({ a: 1 })
48
+ state.set({ b: 2 })
49
+ expect(state.get()).toEqual({ b: 2 })
53
50
  })
54
51
 
55
- test('should set new value with .set(42)', () => {
56
- const cause = new State(0)
57
- cause.set(42)
58
- expect(cause.get()).toBe(42)
52
+ test('should replace value entirely for arrays', () => {
53
+ const state = createState([1, 2, 3])
54
+ state.set([4, 5, 6])
55
+ expect(state.get()).toEqual([4, 5, 6])
59
56
  })
60
57
 
61
- test('should increment value with .set(v => ++v)', () => {
62
- const cause = new State(0)
63
- cause.update(v => ++v)
64
- expect(cause.get()).toBe(1)
58
+ test('should skip update when value is equal by reference', () => {
59
+ const obj = { a: 1 }
60
+ const state = createState(obj)
61
+ let effectCount = 0
62
+ createEffect(() => {
63
+ state.get()
64
+ effectCount++
65
+ })
66
+ expect(effectCount).toBe(1)
67
+ state.set(obj) // same reference
68
+ expect(effectCount).toBe(1)
65
69
  })
66
70
  })
67
71
 
68
- describe('String cause', () => {
69
- test('should be string', () => {
70
- const cause = new State('foo')
71
- expect(typeof cause.get()).toBe('string')
72
+ describe('update', () => {
73
+ test('should update value via callback', () => {
74
+ const state = createState(0)
75
+ state.update(v => v + 1)
76
+ expect(state.get()).toBe(1)
72
77
  })
73
78
 
74
- test('should set initial value to "foo"', () => {
75
- const cause = new State('foo')
76
- expect(cause.get()).toBe('foo')
79
+ test('should pass current value to callback', () => {
80
+ const state = createState('hello')
81
+ state.update(v => v.toUpperCase())
82
+ expect(state.get()).toBe('HELLO')
77
83
  })
78
84
 
79
- test('should set new value with .set("bar")', () => {
80
- const cause = new State('foo')
81
- cause.set('bar')
82
- expect(cause.get()).toBe('bar')
85
+ test('should work with arrays', () => {
86
+ const state = createState([1, 2, 3])
87
+ state.update(arr => [...arr, 4])
88
+ expect(state.get()).toEqual([1, 2, 3, 4])
83
89
  })
84
90
 
85
- test('should upper case value with .set(v => v.toUpperCase())', () => {
86
- const cause = new State('foo')
87
- cause.update(v => (v ? v.toUpperCase() : ''))
88
- expect(cause.get()).toBe('FOO')
91
+ test('should work with objects', () => {
92
+ const state = createState({ count: 0 })
93
+ state.update(obj => ({ ...obj, count: obj.count + 1 }))
94
+ expect(state.get()).toEqual({ count: 1 })
89
95
  })
90
96
  })
91
97
 
92
- describe('Array cause', () => {
93
- test('should be array', () => {
94
- const cause = new State([1, 2, 3])
95
- expect(Array.isArray(cause.get())).toBe(true)
96
- })
98
+ describe('options.equals', () => {
99
+ test('should use custom equality function to skip updates', () => {
100
+ const state = createState(
101
+ { x: 1 },
102
+ { equals: (a, b) => a.x === b.x },
103
+ )
104
+ let effectCount = 0
105
+ createEffect(() => {
106
+ state.get()
107
+ effectCount++
108
+ })
109
+ expect(effectCount).toBe(1)
97
110
 
98
- test('should set initial value to [1, 2, 3]', () => {
99
- const cause = new State([1, 2, 3])
100
- expect(cause.get()).toEqual([1, 2, 3])
101
- })
111
+ state.set({ x: 1 }) // structurally equal
112
+ expect(effectCount).toBe(1)
102
113
 
103
- test('should set new value with .set([4, 5, 6])', () => {
104
- const cause = new State([1, 2, 3])
105
- cause.set([4, 5, 6])
106
- expect(cause.get()).toEqual([4, 5, 6])
114
+ state.set({ x: 2 }) // different
115
+ expect(effectCount).toBe(2)
107
116
  })
108
117
 
109
- test('should reflect current value of array after modification', () => {
110
- const array = [1, 2, 3]
111
- const cause = new State(array)
112
- array.push(4) // don't do this! the result will be correct, but we can't trigger effects
113
- expect(cause.get()).toEqual([1, 2, 3, 4])
118
+ test('should default to reference equality', () => {
119
+ const state = createState({ x: 1 })
120
+ let effectCount = 0
121
+ createEffect(() => {
122
+ state.get()
123
+ effectCount++
124
+ })
125
+ expect(effectCount).toBe(1)
126
+
127
+ state.set({ x: 1 }) // new reference, same shape
128
+ expect(effectCount).toBe(2)
114
129
  })
130
+ })
115
131
 
116
- test('should set new value with .set([...array, 4])', () => {
117
- const array = [1, 2, 3]
118
- const cause = new State(array)
119
- cause.set([...array, 4]) // use destructuring instead!
120
- expect(cause.get()).toEqual([1, 2, 3, 4])
132
+ describe('options.guard', () => {
133
+ test('should validate initial value against guard', () => {
134
+ expect(() => {
135
+ createState(0, {
136
+ guard: (v): v is number => typeof v === 'number' && v > 0,
137
+ })
138
+ }).toThrow('[State] Signal value 0 is invalid')
121
139
  })
122
140
 
123
- describe('Input Validation', () => {
124
- test('should throw NullishSignalValueError when initialValue is nullish', () => {
125
- expect(() => {
126
- // @ts-expect-error - Testing invalid input
127
- new State(null)
128
- }).toThrow('Nullish signal values are not allowed in State')
129
-
130
- expect(() => {
131
- // @ts-expect-error - Testing invalid input
132
- new State(undefined)
133
- }).toThrow('Nullish signal values are not allowed in State')
141
+ test('should validate set() values against guard', () => {
142
+ const state = createState(1, {
143
+ guard: (v): v is number => typeof v === 'number' && v > 0,
134
144
  })
145
+ expect(() => state.set(0)).toThrow(
146
+ '[State] Signal value 0 is invalid',
147
+ )
148
+ expect(state.get()).toBe(1) // unchanged
149
+ })
135
150
 
136
- test('should throw NullishSignalValueError when newValue is nullish in set()', () => {
137
- const state = new State(42)
138
-
139
- expect(() => {
140
- // @ts-expect-error - Testing invalid input
141
- state.set(null)
142
- }).toThrow('Nullish signal values are not allowed in State')
143
-
144
- expect(() => {
145
- // @ts-expect-error - Testing invalid input
146
- state.set(undefined)
147
- }).toThrow('Nullish signal values are not allowed in State')
151
+ test('should validate update() return values against guard', () => {
152
+ const state = createState(1, {
153
+ guard: (v): v is number => typeof v === 'number' && v > 0,
148
154
  })
155
+ expect(() => state.update(() => 0)).toThrow(
156
+ '[State] Signal value 0 is invalid',
157
+ )
158
+ expect(state.get()).toBe(1) // unchanged
159
+ })
149
160
 
150
- test('should throw specific error types for nullish values', () => {
151
- try {
152
- // @ts-expect-error - Testing invalid input
153
- new State(null)
154
- expect(true).toBe(false) // Should not reach here
155
- } catch (error) {
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
- )
161
- }
162
-
163
- const state = new State(42)
164
- try {
165
- // @ts-expect-error - Testing invalid input
166
- state.set(null)
167
- expect(true).toBe(false) // Should not reach here
168
- } catch (error) {
169
- 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
- )
174
- }
161
+ test('should allow values that pass the guard', () => {
162
+ const state = createState(1, {
163
+ guard: (v): v is number => typeof v === 'number' && v > 0,
175
164
  })
165
+ state.set(5)
166
+ expect(state.get()).toBe(5)
167
+ })
168
+ })
176
169
 
177
- test('should allow valid non-nullish values', () => {
178
- // These should not throw
179
- expect(() => {
180
- new State(0)
181
- }).not.toThrow()
182
-
183
- expect(() => {
184
- new State('')
185
- }).not.toThrow()
186
-
187
- expect(() => {
188
- new State(false)
189
- }).not.toThrow()
190
-
191
- expect(() => {
192
- new State({})
193
- }).not.toThrow()
194
-
195
- expect(() => {
196
- new State([])
197
- }).not.toThrow()
198
-
199
- const state = new State(42)
200
- expect(() => {
201
- state.set(0)
202
- }).not.toThrow()
203
-
204
- expect(() => {
205
- // @ts-expect-error - Testing valid input of invalid type
206
- state.set('')
207
- }).not.toThrow()
170
+ describe('Edge cases: NaN and special numbers', () => {
171
+ test('should propagate on every set(NaN) since NaN !== NaN', () => {
172
+ const state = createState(NaN)
173
+ let effectCount = 0
174
+ createEffect(() => {
175
+ state.get()
176
+ effectCount++
208
177
  })
178
+ expect(effectCount).toBe(1)
209
179
 
210
- test('should throw InvalidCallbackError for non-function updater in update()', () => {
211
- const state = new State(42)
180
+ state.set(NaN)
181
+ expect(effectCount).toBe(2) // NaN !== NaN, so it propagates
212
182
 
213
- expect(() => {
214
- // @ts-expect-error - Testing invalid input
215
- state.update(null)
216
- }).toThrow('Invalid State update callback null')
217
-
218
- expect(() => {
219
- // @ts-expect-error - Testing invalid input
220
- state.update(undefined)
221
- }).toThrow('Invalid State update callback undefined')
222
-
223
- expect(() => {
224
- // @ts-expect-error - Testing invalid input
225
- state.update('not a function')
226
- }).toThrow('Invalid State update callback "not a function"')
227
-
228
- expect(() => {
229
- // @ts-expect-error - Testing invalid input
230
- state.update(42)
231
- }).toThrow('Invalid State update callback 42')
232
- })
183
+ state.set(NaN)
184
+ expect(effectCount).toBe(3)
185
+ })
233
186
 
234
- test('should throw specific error type for non-function updater', () => {
235
- const state = new State(42)
236
-
237
- try {
238
- // @ts-expect-error - Testing invalid input
239
- state.update(null)
240
- expect(true).toBe(false) // Should not reach here
241
- } catch (error) {
242
- expect(error).toBeInstanceOf(TypeError)
243
- expect(error.name).toBe('InvalidCallbackError')
244
- expect(error.message).toBe(
245
- 'Invalid State update callback null',
246
- )
247
- }
187
+ test('should reject NaN with a Number.isFinite guard', () => {
188
+ const state = createState(1, {
189
+ guard: (v): v is number => Number.isFinite(v),
248
190
  })
191
+ expect(() => state.set(NaN)).toThrow(
192
+ '[State] Signal value NaN is invalid',
193
+ )
194
+ expect(state.get()).toBe(1)
195
+ })
249
196
 
250
- test('should handle updater function that throws an error', () => {
251
- const state = new State(42)
252
-
253
- expect(() => {
254
- state.update(() => {
255
- throw new Error('Updater error')
256
- })
257
- }).toThrow('Updater error')
258
-
259
- // State should remain unchanged after error
260
- expect(state.get()).toBe(42)
197
+ test('should reject Infinity with a Number.isFinite guard', () => {
198
+ const state = createState(1, {
199
+ guard: (v): v is number => Number.isFinite(v),
261
200
  })
201
+ expect(() => state.set(Infinity)).toThrow(
202
+ '[State] Signal value Infinity is invalid',
203
+ )
204
+ expect(() => state.set(-Infinity)).toThrow(
205
+ '[State] Signal value -Infinity is invalid',
206
+ )
207
+ expect(state.get()).toBe(1)
208
+ })
262
209
 
263
- test('should handle updater function that returns nullish value', () => {
264
- const state = new State(42)
265
-
266
- expect(() => {
267
- // @ts-expect-error - Testing invalid return value
268
- state.update(() => null)
269
- }).toThrow('Nullish signal values are not allowed in State')
270
-
271
- expect(() => {
272
- // @ts-expect-error - Testing invalid return value
273
- state.update(() => undefined)
274
- }).toThrow('Nullish signal values are not allowed in State')
275
-
276
- // State should remain unchanged after error
277
- expect(state.get()).toBe(42)
210
+ test('should treat +0 and -0 as equal by default (===)', () => {
211
+ const state = createState(0)
212
+ let effectCount = 0
213
+ createEffect(() => {
214
+ state.get()
215
+ effectCount++
278
216
  })
217
+ expect(effectCount).toBe(1)
279
218
 
280
- test('should handle valid updater functions', () => {
281
- const numberState = new State(10)
282
- expect(() => {
283
- numberState.update(x => x + 5)
284
- }).not.toThrow()
285
- expect(numberState.get()).toBe(15)
286
-
287
- const stringState = new State('hello')
288
- expect(() => {
289
- stringState.update(x => x.toUpperCase())
290
- }).not.toThrow()
291
- expect(stringState.get()).toBe('HELLO')
292
-
293
- const arrayState = new State([1, 2, 3])
294
- expect(() => {
295
- arrayState.update(arr => [...arr, 4])
296
- }).not.toThrow()
297
- expect(arrayState.get()).toEqual([1, 2, 3, 4])
298
-
299
- const objectState = new State({ count: 0 })
300
- expect(() => {
301
- objectState.update(obj => ({
302
- ...obj,
303
- count: obj.count + 1,
304
- }))
305
- }).not.toThrow()
306
- expect(objectState.get()).toEqual({ count: 1 })
307
- })
219
+ state.set(-0) // +0 === -0 is true
220
+ expect(effectCount).toBe(1) // no propagation
308
221
  })
309
222
  })
310
223
 
311
- describe('Object cause', () => {
312
- test('should be object', () => {
313
- const cause = new State({ a: 'a', b: 1 })
314
- expect(typeof cause.get()).toBe('object')
224
+ describe('Input Validation', () => {
225
+ test('should throw NullishSignalValueError for null or undefined initial value', () => {
226
+ expect(() => {
227
+ // @ts-expect-error - Testing invalid input
228
+ createState(null)
229
+ }).toThrow('[State] Signal value cannot be null or undefined')
230
+
231
+ expect(() => {
232
+ // @ts-expect-error - Testing invalid input
233
+ createState(undefined)
234
+ }).toThrow('[State] Signal value cannot be null or undefined')
315
235
  })
316
236
 
317
- test('should set initial value to { a: "a", b: 1 }', () => {
318
- const cause = new State({ a: 'a', b: 1 })
319
- expect(cause.get()).toEqual({ a: 'a', b: 1 })
237
+ test('should throw NullishSignalValueError for null or undefined in set()', () => {
238
+ const state = createState(42)
239
+ expect(() => {
240
+ // @ts-expect-error - Testing invalid input
241
+ state.set(null)
242
+ }).toThrow('[State] Signal value cannot be null or undefined')
243
+
244
+ expect(() => {
245
+ // @ts-expect-error - Testing invalid input
246
+ state.set(undefined)
247
+ }).toThrow('[State] Signal value cannot be null or undefined')
320
248
  })
321
249
 
322
- test('should set new value with .set({ c: true })', () => {
323
- const cause = new State<Record<string, unknown>>({ a: 'a', b: 1 })
324
- cause.set({ c: true })
325
- expect(cause.get()).toEqual({ c: true })
250
+ test('should throw NullishSignalValueError for nullish return from update()', () => {
251
+ const state = createState(42)
252
+ expect(() => {
253
+ // @ts-expect-error - Testing invalid return value
254
+ state.update(() => null)
255
+ }).toThrow('[State] Signal value cannot be null or undefined')
256
+
257
+ expect(() => {
258
+ // @ts-expect-error - Testing invalid return value
259
+ state.update(() => undefined)
260
+ }).toThrow('[State] Signal value cannot be null or undefined')
261
+
262
+ expect(state.get()).toBe(42) // unchanged
326
263
  })
327
264
 
328
- test('should reflect current value of object after modification', () => {
329
- const obj = { a: 'a', b: 1 }
330
- const cause = new State<Record<string, unknown>>(obj)
331
- // @ts-expect-error Property 'c' does not exist on type '{ a: string; b: number; }'. (ts 2339)
332
- obj.c = true // don't do this! the result will be correct, but we can't trigger effects
333
- expect(cause.get()).toEqual({ a: 'a', b: 1, c: true })
265
+ test('should throw InvalidCallbackError for non-function in update()', () => {
266
+ const state = createState(42)
267
+ expect(() => {
268
+ // @ts-expect-error - Testing invalid input
269
+ state.update(null)
270
+ }).toThrow('[State] Callback null is invalid')
271
+
272
+ expect(() => {
273
+ // @ts-expect-error - Testing invalid input
274
+ state.update('not a function')
275
+ }).toThrow('[State] Callback "not a function" is invalid')
334
276
  })
335
277
 
336
- test('should set new value with .set({...obj, c: true})', () => {
337
- const obj = { a: 'a', b: 1 }
338
- const cause = new State<Record<string, unknown>>(obj)
339
- cause.set({ ...obj, c: true }) // use destructuring instead!
340
- expect(cause.get()).toEqual({ a: 'a', b: 1, c: true })
278
+ test('should propagate errors thrown by update callback', () => {
279
+ const state = createState(42)
280
+ expect(() => {
281
+ state.update(() => {
282
+ throw new Error('Updater error')
283
+ })
284
+ }).toThrow('Updater error')
285
+
286
+ expect(state.get()).toBe(42) // unchanged
341
287
  })
342
288
  })
343
289
  })