@zeix/cause-effect 0.15.0 → 0.15.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.
@@ -0,0 +1,323 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import {
3
+ type Computed,
4
+ computed,
5
+ isComputed,
6
+ isState,
7
+ isStore,
8
+ type Signal,
9
+ type State,
10
+ type Store,
11
+ state,
12
+ store,
13
+ toSignal,
14
+ type UnknownRecord,
15
+ } from '..'
16
+
17
+ /* === Tests === */
18
+
19
+ describe('toSignal', () => {
20
+ describe('type inference and runtime behavior', () => {
21
+ test('converts array to Store<Record<string, T>>', () => {
22
+ const result = toSignal([
23
+ { id: 1, name: 'Alice' },
24
+ { id: 2, name: 'Bob' },
25
+ ])
26
+
27
+ // Runtime behavior
28
+ expect(isStore(result)).toBe(true)
29
+ expect(result['0'].get()).toEqual({ id: 1, name: 'Alice' })
30
+ expect(result['1'].get()).toEqual({ id: 2, name: 'Bob' })
31
+
32
+ // Type inference test - now correctly returns Store<Record<number, {id: number, name: string}>>
33
+ const typedResult: Store<{ id: number; name: string }[]> = result
34
+ expect(typedResult).toBeDefined()
35
+ })
36
+
37
+ test('converts empty array to Store<Record<string, never>>', () => {
38
+ const result = toSignal([])
39
+
40
+ // Runtime behavior
41
+ expect(isStore(result)).toBe(true)
42
+ expect(result.length).toBe(0)
43
+ expect(Object.keys(result).length).toBe(1) // length property
44
+ })
45
+
46
+ test('converts record to Store<T>', () => {
47
+ const record = { name: 'Alice', age: 30 }
48
+ const result = toSignal(record)
49
+
50
+ // Runtime behavior
51
+ expect(isStore(result)).toBe(true)
52
+ expect(result.name.get()).toBe('Alice')
53
+ expect(result.age.get()).toBe(30)
54
+
55
+ // Type inference test - should be Store<{name: string, age: number}>
56
+ const typedResult: Store<{ name: string; age: number }> = result
57
+ expect(typedResult).toBeDefined()
58
+ })
59
+
60
+ test('passes through existing Store unchanged', () => {
61
+ const originalStore = store({ count: 5 })
62
+ const result = toSignal(originalStore)
63
+
64
+ // Runtime behavior
65
+ expect(result).toBe(originalStore) // Should be the same instance
66
+ expect(isStore(result)).toBe(true)
67
+ expect(result.count.get()).toBe(5)
68
+
69
+ // Type inference test - should remain Store<{count: number}>
70
+ const typedResult: Store<{ count: number }> = result
71
+ expect(typedResult).toBeDefined()
72
+ })
73
+
74
+ test('passes through existing State unchanged', () => {
75
+ const originalState = state(42)
76
+ const result = toSignal(originalState)
77
+
78
+ // Runtime behavior
79
+ expect(result).toBe(originalState) // Should be the same instance
80
+ expect(isState(result)).toBe(true)
81
+ expect(result.get()).toBe(42)
82
+
83
+ // Type inference test - should remain State<number>
84
+ const typedResult: State<number> = result
85
+ expect(typedResult).toBeDefined()
86
+ })
87
+
88
+ test('passes through existing Computed unchanged', () => {
89
+ const originalComputed = computed(() => 'hello world')
90
+ const result = toSignal(originalComputed)
91
+
92
+ // Runtime behavior
93
+ expect(result).toBe(originalComputed) // Should be the same instance
94
+ expect(isComputed(result)).toBe(true)
95
+ expect(result.get()).toBe('hello world')
96
+
97
+ // Type inference test - should remain Computed<string>
98
+ const typedResult: Computed<string> = result
99
+ expect(typedResult).toBeDefined()
100
+ })
101
+
102
+ test('converts function to Computed<T>', () => {
103
+ const fn = () => Math.random()
104
+ const result = toSignal(fn)
105
+
106
+ // Runtime behavior - functions are correctly converted to Computed
107
+ expect(isComputed(result)).toBe(true)
108
+ expect(typeof result.get()).toBe('number')
109
+
110
+ // Type inference test - should be Computed<number>
111
+ const typedResult: Computed<number> = result
112
+ expect(typedResult).toBeDefined()
113
+ })
114
+
115
+ test('converts primitive to State<T>', () => {
116
+ const num = 42
117
+ const result = toSignal(num)
118
+
119
+ // Runtime behavior - primitives are correctly converted to State
120
+ expect(isState(result)).toBe(true)
121
+ expect(result.get()).toBe(42)
122
+
123
+ // Type inference test - should be State<number>
124
+ const typedResult: State<number> = result
125
+ expect(typedResult).toBeDefined()
126
+ })
127
+
128
+ test('converts object to State<T>', () => {
129
+ const obj = new Date('2024-01-01')
130
+ const result = toSignal(obj)
131
+
132
+ // Runtime behavior - objects are correctly converted to State
133
+ expect(isState(result)).toBe(true)
134
+ expect(result.get()).toBe(obj)
135
+
136
+ // Type inference test - should be State<Date>
137
+ const typedResult: State<Date> = result
138
+ expect(typedResult).toBeDefined()
139
+ })
140
+ })
141
+
142
+ describe('edge cases', () => {
143
+ test('handles nested arrays', () => {
144
+ const result = toSignal([
145
+ [1, 2],
146
+ [3, 4],
147
+ ])
148
+
149
+ expect(isStore(result)).toBe(true)
150
+ // With the fixed behavior, nested arrays should be recovered as arrays
151
+ const firstElement = result[0].get()
152
+ const secondElement = result[1].get()
153
+
154
+ // The expected behavior - nested arrays are recovered as arrays
155
+ expect(firstElement).toEqual([1, 2])
156
+ expect(secondElement).toEqual([3, 4])
157
+ })
158
+
159
+ test('handles arrays with mixed types', () => {
160
+ const mixedArr = [1, 'hello', { key: 'value' }]
161
+ const result = toSignal(mixedArr)
162
+
163
+ expect(isStore(result)).toBe(true)
164
+ expect(result['0'].get()).toBe(1)
165
+ expect(result['1'].get()).toBe('hello')
166
+ expect(result['2'].get()).toEqual({ key: 'value' })
167
+ })
168
+
169
+ test('handles sparse arrays', () => {
170
+ const sparseArr = new Array(3)
171
+ sparseArr[1] = 'middle'
172
+ const result = toSignal(sparseArr)
173
+
174
+ expect(isStore(result)).toBe(true)
175
+ expect('0' in result).toBe(false)
176
+ expect(result['1'].get()).toBe('middle')
177
+ expect('2' in result).toBe(false)
178
+ })
179
+ })
180
+ })
181
+
182
+ describe('Signal compatibility', () => {
183
+ test('all results implement Signal<T> interface', () => {
184
+ const arraySignal = toSignal([1, 2, 3])
185
+ const recordSignal = toSignal({ a: 1, b: 2 })
186
+ const primitiveSignal = toSignal(42)
187
+ const functionSignal = toSignal(() => 'hello')
188
+ const stateSignal = toSignal(state(true))
189
+
190
+ // All should have get() method
191
+ expect(typeof arraySignal.get).toBe('function')
192
+ expect(typeof recordSignal.get).toBe('function')
193
+ expect(typeof primitiveSignal.get).toBe('function')
194
+ expect(typeof functionSignal.get).toBe('function')
195
+ expect(typeof stateSignal.get).toBe('function')
196
+
197
+ // All should be assignable to Signal<T>
198
+ const signals: Signal<unknown & {}>[] = [
199
+ arraySignal,
200
+ recordSignal,
201
+ primitiveSignal,
202
+ functionSignal,
203
+ stateSignal,
204
+ ]
205
+ expect(signals.length).toBe(5)
206
+ })
207
+ })
208
+
209
+ describe('Type precision tests', () => {
210
+ test('array type should infer element type correctly', () => {
211
+ // Test that arrays infer the correct element type
212
+ const stringArray = ['a', 'b', 'c']
213
+ const stringArraySignal = toSignal(stringArray)
214
+
215
+ // Should be Store<Record<string, string>>
216
+ expect(stringArraySignal['0'].get()).toBe('a')
217
+
218
+ const numberArray = [1, 2, 3]
219
+ const numberArraySignal = toSignal(numberArray)
220
+
221
+ // Should be Store<Record<string, number>>
222
+ expect(typeof numberArraySignal['0'].get()).toBe('number')
223
+ })
224
+
225
+ test('complex object arrays maintain precise typing', () => {
226
+ interface User {
227
+ id: number
228
+ name: string
229
+ email: string
230
+ }
231
+
232
+ const users: User[] = [
233
+ { id: 1, name: 'Alice', email: 'alice@example.com' },
234
+ { id: 2, name: 'Bob', email: 'bob@example.com' },
235
+ ]
236
+
237
+ const usersSignal = toSignal(users)
238
+
239
+ // Should maintain User type for each element
240
+ const firstUser = usersSignal['0'].get()
241
+ expect(firstUser.id).toBe(1)
242
+ expect(firstUser.name).toBe('Alice')
243
+ expect(firstUser.email).toBe('alice@example.com')
244
+ })
245
+
246
+ describe('Type inference issues', () => {
247
+ test('demonstrates current type inference problem', () => {
248
+ const result = toSignal([{ id: 1 }, { id: 2 }])
249
+
250
+ // Let's verify the actual behavior
251
+ expect(isStore(result)).toBe(true)
252
+ expect(result['0'].get()).toEqual({ id: 1 })
253
+ expect(result['1'].get()).toEqual({ id: 2 })
254
+
255
+ // Type assertion test - this should now work with correct typing
256
+ const typedResult: Store<{ id: number }[]> = result
257
+ expect(typedResult).toBeDefined()
258
+
259
+ // Simulate external library usage where P[K] represents element type
260
+ interface ExternalLibraryConstraint<P extends UnknownRecord> {
261
+ process<K extends keyof P>(signal: Signal<P[K]>): void
262
+ }
263
+
264
+ // This should work if types are correct
265
+ const processor: ExternalLibraryConstraint<
266
+ Record<string, { id: number }>
267
+ > = {
268
+ process: <K extends keyof Record<string, { id: number }>>(
269
+ signal: Signal<Record<string, { id: number }>[K]>,
270
+ ) => {
271
+ // Process the signal
272
+ const value = signal.get()
273
+ expect(value).toHaveProperty('id')
274
+ },
275
+ }
276
+
277
+ // This call should work without type errors
278
+ processor.process(result['0'])
279
+ })
280
+
281
+ test('verifies fixed type inference for external library compatibility', () => {
282
+ const items = [
283
+ { id: 1, name: 'Alice' },
284
+ { id: 2, name: 'Bob' },
285
+ ]
286
+ const signal = toSignal(items)
287
+ const firstItemSignal = signal['0']
288
+ const secondItemSignal = signal['1']
289
+
290
+ // Runtime behavior works correctly
291
+ expect(isStore(signal)).toBe(true)
292
+ expect(firstItemSignal.get()).toEqual({ id: 1, name: 'Alice' })
293
+ expect(secondItemSignal.get()).toEqual({ id: 2, name: 'Bob' })
294
+
295
+ // Type inference should now work correctly:
296
+ const properlyTyped: Store<{ id: number; name: string }[]> = signal
297
+ expect(properlyTyped).toBeDefined()
298
+
299
+ // These should work without type errors in external libraries
300
+ // that expect Signal<P[K]> where P[K] is the individual element type
301
+ interface ExternalAPI<P extends UnknownRecord> {
302
+ process<K extends keyof P>(key: K, signal: Signal<P[K]>): P[K]
303
+ }
304
+
305
+ const api: ExternalAPI<
306
+ Record<string, { id: number; name: string }>
307
+ > = {
308
+ process: (_key, signal) => signal.get(),
309
+ }
310
+
311
+ // These calls should work with proper typing now
312
+ const result1 = api.process('0', firstItemSignal)
313
+ const result2 = api.process('1', secondItemSignal)
314
+
315
+ expect(result1).toEqual({ id: 1, name: 'Alice' })
316
+ expect(result2).toEqual({ id: 2, name: 'Bob' })
317
+
318
+ // Verify the types are precise
319
+ expect(typeof result1.id).toBe('number')
320
+ expect(typeof result1.name).toBe('string')
321
+ })
322
+ })
323
+ })