@zeix/cause-effect 0.17.3 → 0.18.1

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 +169 -227
  2. package/.cursorrules +41 -35
  3. package/.github/copilot-instructions.md +176 -116
  4. package/ARCHITECTURE.md +276 -0
  5. package/CHANGELOG.md +29 -0
  6. package/CLAUDE.md +201 -143
  7. package/GUIDE.md +298 -0
  8. package/README.md +246 -193
  9. package/REQUIREMENTS.md +100 -0
  10. package/bench/reactivity.bench.ts +577 -0
  11. package/context7.json +4 -0
  12. package/examples/events-sensor.ts +187 -0
  13. package/examples/selector-sensor.ts +173 -0
  14. package/index.dev.js +1390 -1008
  15. package/index.js +1 -1
  16. package/index.ts +60 -74
  17. package/package.json +5 -2
  18. package/skills/changelog-keeper/SKILL.md +59 -0
  19. package/skills/changelog-keeper/agents/openai.yaml +4 -0
  20. package/src/errors.ts +118 -74
  21. package/src/graph.ts +612 -0
  22. package/src/nodes/collection.ts +512 -0
  23. package/src/nodes/effect.ts +149 -0
  24. package/src/nodes/list.ts +589 -0
  25. package/src/nodes/memo.ts +148 -0
  26. package/src/nodes/sensor.ts +149 -0
  27. package/src/nodes/state.ts +135 -0
  28. package/src/nodes/store.ts +378 -0
  29. package/src/nodes/task.ts +174 -0
  30. package/src/signal.ts +112 -66
  31. package/src/util.ts +26 -57
  32. package/test/batch.test.ts +96 -62
  33. package/test/benchmark.test.ts +473 -487
  34. package/test/collection.test.ts +456 -707
  35. package/test/effect.test.ts +293 -696
  36. package/test/list.test.ts +335 -592
  37. package/test/memo.test.ts +574 -0
  38. package/test/regression.test.ts +156 -0
  39. package/test/scope.test.ts +191 -0
  40. package/test/sensor.test.ts +454 -0
  41. package/test/signal.test.ts +220 -213
  42. package/test/state.test.ts +217 -265
  43. package/test/store.test.ts +346 -446
  44. package/test/task.test.ts +529 -0
  45. package/test/untrack.test.ts +167 -0
  46. package/types/index.d.ts +13 -15
  47. package/types/src/errors.d.ts +73 -17
  48. package/types/src/graph.d.ts +218 -0
  49. package/types/src/nodes/collection.d.ts +69 -0
  50. package/types/src/nodes/effect.d.ts +48 -0
  51. package/types/src/nodes/list.d.ts +66 -0
  52. package/types/src/nodes/memo.d.ts +63 -0
  53. package/types/src/nodes/sensor.d.ts +81 -0
  54. package/types/src/nodes/state.d.ts +78 -0
  55. package/types/src/nodes/store.d.ts +51 -0
  56. package/types/src/nodes/task.d.ts +79 -0
  57. package/types/src/signal.d.ts +43 -29
  58. package/types/src/util.d.ts +9 -16
  59. package/archive/benchmark.ts +0 -683
  60. package/archive/collection.ts +0 -253
  61. package/archive/composite.ts +0 -85
  62. package/archive/computed.ts +0 -195
  63. package/archive/list.ts +0 -483
  64. package/archive/memo.ts +0 -139
  65. package/archive/state.ts +0 -90
  66. package/archive/store.ts +0 -298
  67. package/archive/task.ts +0 -189
  68. package/src/classes/collection.ts +0 -245
  69. package/src/classes/computed.ts +0 -349
  70. package/src/classes/list.ts +0 -343
  71. package/src/classes/ref.ts +0 -70
  72. package/src/classes/state.ts +0 -102
  73. package/src/classes/store.ts +0 -262
  74. package/src/diff.ts +0 -138
  75. package/src/effect.ts +0 -93
  76. package/src/match.ts +0 -45
  77. package/src/resolve.ts +0 -49
  78. package/src/system.ts +0 -257
  79. package/test/computed.test.ts +0 -1108
  80. package/test/diff.test.ts +0 -955
  81. package/test/match.test.ts +0 -388
  82. package/test/ref.test.ts +0 -353
  83. package/test/resolve.test.ts +0 -154
  84. package/types/src/classes/collection.d.ts +0 -45
  85. package/types/src/classes/computed.d.ts +0 -94
  86. package/types/src/classes/list.d.ts +0 -43
  87. package/types/src/classes/ref.d.ts +0 -35
  88. package/types/src/classes/state.d.ts +0 -49
  89. package/types/src/classes/store.d.ts +0 -52
  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 -78
@@ -1,337 +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 as Error).name).toBe('NullishSignalValueError')
158
- expect((error as Error).message).toBe('Nullish signal values are not allowed in State')
159
- }
160
-
161
- const state = new State(42)
162
- try {
163
- // @ts-expect-error - Testing invalid input
164
- state.set(null)
165
- expect(true).toBe(false) // Should not reach here
166
- } catch (error) {
167
- expect(error).toBeInstanceOf(TypeError)
168
- expect((error as Error).name).toBe('NullishSignalValueError')
169
- expect((error as Error).message).toBe('Nullish signal values are not allowed in State')
170
- }
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,
171
164
  })
165
+ state.set(5)
166
+ expect(state.get()).toBe(5)
167
+ })
168
+ })
172
169
 
173
- test('should allow valid non-nullish values', () => {
174
- // These should not throw
175
- expect(() => {
176
- new State(0)
177
- }).not.toThrow()
178
-
179
- expect(() => {
180
- new State('')
181
- }).not.toThrow()
182
-
183
- expect(() => {
184
- new State(false)
185
- }).not.toThrow()
186
-
187
- expect(() => {
188
- new State({})
189
- }).not.toThrow()
190
-
191
- expect(() => {
192
- new State([])
193
- }).not.toThrow()
194
-
195
- const state = new State(42)
196
- expect(() => {
197
- state.set(0)
198
- }).not.toThrow()
199
-
200
- expect(() => {
201
- // @ts-expect-error - Testing valid input of invalid type
202
- state.set('')
203
- }).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++
204
177
  })
178
+ expect(effectCount).toBe(1)
205
179
 
206
- test('should throw InvalidCallbackError for non-function updater in update()', () => {
207
- const state = new State(42)
180
+ state.set(NaN)
181
+ expect(effectCount).toBe(2) // NaN !== NaN, so it propagates
208
182
 
209
- expect(() => {
210
- // @ts-expect-error - Testing invalid input
211
- state.update(null)
212
- }).toThrow('Invalid State update callback null')
213
-
214
- expect(() => {
215
- // @ts-expect-error - Testing invalid input
216
- state.update(undefined)
217
- }).toThrow('Invalid State update callback undefined')
218
-
219
- expect(() => {
220
- // @ts-expect-error - Testing invalid input
221
- state.update('not a function')
222
- }).toThrow('Invalid State update callback "not a function"')
223
-
224
- expect(() => {
225
- // @ts-expect-error - Testing invalid input
226
- state.update(42)
227
- }).toThrow('Invalid State update callback 42')
228
- })
183
+ state.set(NaN)
184
+ expect(effectCount).toBe(3)
185
+ })
229
186
 
230
- test('should throw specific error type for non-function updater', () => {
231
- const state = new State(42)
232
-
233
- try {
234
- // @ts-expect-error - Testing invalid input
235
- state.update(null)
236
- expect(true).toBe(false) // Should not reach here
237
- } catch (error) {
238
- expect(error).toBeInstanceOf(TypeError)
239
- expect((error as Error).name).toBe('InvalidCallbackError')
240
- expect((error as Error).message).toBe('Invalid State update callback null')
241
- }
187
+ test('should reject NaN with a Number.isFinite guard', () => {
188
+ const state = createState(1, {
189
+ guard: (v): v is number => Number.isFinite(v),
242
190
  })
191
+ expect(() => state.set(NaN)).toThrow(
192
+ '[State] Signal value NaN is invalid',
193
+ )
194
+ expect(state.get()).toBe(1)
195
+ })
243
196
 
244
- test('should handle updater function that throws an error', () => {
245
- const state = new State(42)
246
-
247
- expect(() => {
248
- state.update(() => {
249
- throw new Error('Updater error')
250
- })
251
- }).toThrow('Updater error')
252
-
253
- // State should remain unchanged after error
254
- 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),
255
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
+ })
256
209
 
257
- test('should handle updater function that returns nullish value', () => {
258
- const state = new State(42)
259
-
260
- expect(() => {
261
- // @ts-expect-error - Testing invalid return value
262
- state.update(() => null)
263
- }).toThrow('Nullish signal values are not allowed in State')
264
-
265
- expect(() => {
266
- // @ts-expect-error - Testing invalid return value
267
- state.update(() => undefined)
268
- }).toThrow('Nullish signal values are not allowed in State')
269
-
270
- // State should remain unchanged after error
271
- 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++
272
216
  })
217
+ expect(effectCount).toBe(1)
273
218
 
274
- test('should handle valid updater functions', () => {
275
- const numberState = new State(10)
276
- expect(() => {
277
- numberState.update(x => x + 5)
278
- }).not.toThrow()
279
- expect(numberState.get()).toBe(15)
280
-
281
- const stringState = new State('hello')
282
- expect(() => {
283
- stringState.update(x => x.toUpperCase())
284
- }).not.toThrow()
285
- expect(stringState.get()).toBe('HELLO')
286
-
287
- const arrayState = new State([1, 2, 3])
288
- expect(() => {
289
- arrayState.update(arr => [...arr, 4])
290
- }).not.toThrow()
291
- expect(arrayState.get()).toEqual([1, 2, 3, 4])
292
-
293
- const objectState = new State({ count: 0 })
294
- expect(() => {
295
- objectState.update(obj => ({
296
- ...obj,
297
- count: obj.count + 1,
298
- }))
299
- }).not.toThrow()
300
- expect(objectState.get()).toEqual({ count: 1 })
301
- })
219
+ state.set(-0) // +0 === -0 is true
220
+ expect(effectCount).toBe(1) // no propagation
302
221
  })
303
222
  })
304
223
 
305
- describe('Object cause', () => {
306
- test('should be object', () => {
307
- const cause = new State({ a: 'a', b: 1 })
308
- 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')
309
235
  })
310
236
 
311
- test('should set initial value to { a: "a", b: 1 }', () => {
312
- const cause = new State({ a: 'a', b: 1 })
313
- 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')
314
248
  })
315
249
 
316
- test('should set new value with .set({ c: true })', () => {
317
- const cause = new State<Record<string, unknown>>({ a: 'a', b: 1 })
318
- cause.set({ c: true })
319
- 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
320
263
  })
321
264
 
322
- test('should reflect current value of object after modification', () => {
323
- const obj = { a: 'a', b: 1 }
324
- const cause = new State<Record<string, unknown>>(obj)
325
- // @ts-expect-error Property 'c' does not exist on type '{ a: string; b: number; }'. (ts 2339)
326
- obj.c = true // don't do this! the result will be correct, but we can't trigger effects
327
- 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')
328
276
  })
329
277
 
330
- test('should set new value with .set({...obj, c: true})', () => {
331
- const obj = { a: 'a', b: 1 }
332
- const cause = new State<Record<string, unknown>>(obj)
333
- cause.set({ ...obj, c: true }) // use destructuring instead!
334
- 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
335
287
  })
336
288
  })
337
289
  })