@zeix/cause-effect 0.17.3 → 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.
- package/.ai-context.md +163 -232
- package/.cursorrules +41 -35
- package/.github/copilot-instructions.md +166 -116
- package/ARCHITECTURE.md +274 -0
- package/CLAUDE.md +199 -143
- package/COLLECTION_REFACTORING.md +161 -0
- package/GUIDE.md +298 -0
- package/README.md +232 -197
- package/REQUIREMENTS.md +100 -0
- package/bench/reactivity.bench.ts +577 -0
- package/index.dev.js +1325 -997
- package/index.js +1 -1
- package/index.ts +58 -74
- package/package.json +4 -1
- package/src/errors.ts +118 -74
- package/src/graph.ts +601 -0
- package/src/nodes/collection.ts +474 -0
- package/src/nodes/effect.ts +149 -0
- package/src/nodes/list.ts +588 -0
- package/src/nodes/memo.ts +120 -0
- package/src/nodes/sensor.ts +139 -0
- package/src/nodes/state.ts +135 -0
- package/src/nodes/store.ts +383 -0
- package/src/nodes/task.ts +146 -0
- package/src/signal.ts +112 -66
- package/src/util.ts +26 -57
- package/test/batch.test.ts +96 -62
- package/test/benchmark.test.ts +473 -487
- package/test/collection.test.ts +466 -706
- package/test/effect.test.ts +293 -696
- package/test/list.test.ts +335 -592
- package/test/memo.test.ts +380 -0
- package/test/regression.test.ts +156 -0
- package/test/scope.test.ts +191 -0
- package/test/sensor.test.ts +454 -0
- package/test/signal.test.ts +220 -213
- package/test/state.test.ts +217 -265
- package/test/store.test.ts +346 -446
- package/test/task.test.ts +395 -0
- package/test/untrack.test.ts +167 -0
- package/types/index.d.ts +13 -15
- package/types/src/errors.d.ts +73 -17
- package/types/src/graph.d.ts +208 -0
- package/types/src/nodes/collection.d.ts +64 -0
- package/types/src/nodes/effect.d.ts +48 -0
- package/types/src/nodes/list.d.ts +65 -0
- package/types/src/nodes/memo.d.ts +57 -0
- package/types/src/nodes/sensor.d.ts +75 -0
- package/types/src/nodes/state.d.ts +78 -0
- package/types/src/nodes/store.d.ts +51 -0
- package/types/src/nodes/task.d.ts +73 -0
- package/types/src/signal.d.ts +43 -29
- package/types/src/util.d.ts +9 -16
- package/archive/benchmark.ts +0 -683
- package/archive/collection.ts +0 -253
- package/archive/composite.ts +0 -85
- package/archive/computed.ts +0 -195
- package/archive/list.ts +0 -483
- package/archive/memo.ts +0 -139
- package/archive/state.ts +0 -90
- package/archive/store.ts +0 -298
- package/archive/task.ts +0 -189
- package/src/classes/collection.ts +0 -245
- package/src/classes/computed.ts +0 -349
- package/src/classes/list.ts +0 -343
- package/src/classes/ref.ts +0 -70
- package/src/classes/state.ts +0 -102
- package/src/classes/store.ts +0 -262
- package/src/diff.ts +0 -138
- package/src/effect.ts +0 -93
- package/src/match.ts +0 -45
- package/src/resolve.ts +0 -49
- package/src/system.ts +0 -257
- package/test/computed.test.ts +0 -1108
- package/test/diff.test.ts +0 -955
- package/test/match.test.ts +0 -388
- package/test/ref.test.ts +0 -353
- package/test/resolve.test.ts +0 -154
- package/types/src/classes/collection.d.ts +0 -45
- package/types/src/classes/computed.d.ts +0 -94
- package/types/src/classes/list.d.ts +0 -43
- package/types/src/classes/ref.d.ts +0 -35
- package/types/src/classes/state.d.ts +0 -49
- package/types/src/classes/store.d.ts +0 -52
- package/types/src/diff.d.ts +0 -28
- package/types/src/effect.d.ts +0 -15
- package/types/src/match.d.ts +0 -21
- package/types/src/resolve.d.ts +0 -29
- package/types/src/system.d.ts +0 -78
package/test/state.test.ts
CHANGED
|
@@ -1,337 +1,289 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import {
|
|
2
|
+
import { createEffect, createState, isMemo, isState } from '../index.ts'
|
|
3
3
|
|
|
4
4
|
/* === Tests === */
|
|
5
5
|
|
|
6
6
|
describe('State', () => {
|
|
7
|
-
describe('
|
|
8
|
-
test('
|
|
9
|
-
const count =
|
|
10
|
-
expect(
|
|
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
|
|
22
|
-
|
|
23
|
-
expect(
|
|
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
|
|
27
|
-
const
|
|
28
|
-
expect(
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
expect(
|
|
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('
|
|
45
|
-
test('should
|
|
46
|
-
const
|
|
47
|
-
|
|
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
|
|
51
|
-
const
|
|
52
|
-
|
|
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
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
expect(
|
|
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
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
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('
|
|
69
|
-
test('should
|
|
70
|
-
const
|
|
71
|
-
|
|
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
|
|
75
|
-
const
|
|
76
|
-
|
|
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
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
expect(
|
|
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
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
expect(
|
|
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('
|
|
93
|
-
test('should
|
|
94
|
-
const
|
|
95
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
expect(cause.get()).toEqual([1, 2, 3])
|
|
101
|
-
})
|
|
111
|
+
state.set({ x: 1 }) // structurally equal
|
|
112
|
+
expect(effectCount).toBe(1)
|
|
102
113
|
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
137
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
207
|
-
|
|
180
|
+
state.set(NaN)
|
|
181
|
+
expect(effectCount).toBe(2) // NaN !== NaN, so it propagates
|
|
208
182
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
231
|
-
|
|
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
|
-
|
|
245
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
275
|
-
|
|
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('
|
|
306
|
-
test('should
|
|
307
|
-
|
|
308
|
-
|
|
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
|
|
312
|
-
const
|
|
313
|
-
expect(
|
|
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
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
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
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
})
|