@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.
- package/README.md +40 -10
- package/index.dev.js +508 -382
- package/index.js +1 -1
- package/index.ts +26 -5
- package/package.json +1 -1
- package/src/computed.ts +2 -2
- package/src/diff.ts +70 -45
- package/src/effect.ts +3 -8
- package/src/errors.ts +56 -0
- package/src/match.ts +14 -19
- package/src/resolve.ts +8 -18
- package/src/signal.ts +38 -46
- package/src/state.ts +3 -2
- package/src/store.ts +410 -188
- package/src/util.ts +62 -20
- package/test/computed.test.ts +1 -1
- package/test/diff.test.ts +321 -4
- package/test/effect.test.ts +1 -1
- package/test/match.test.ts +13 -3
- package/test/signal.test.ts +323 -0
- package/test/store.test.ts +971 -1
- package/types/index.d.ts +6 -5
- package/types/src/diff.d.ts +8 -3
- package/types/src/errors.d.ts +19 -0
- package/types/src/match.d.ts +4 -4
- package/types/src/resolve.d.ts +3 -3
- package/types/src/signal.d.ts +15 -18
- package/types/src/store.d.ts +56 -33
- package/types/src/util.d.ts +9 -7
- package/index.d.ts +0 -36
- package/types/test-new-effect.d.ts +0 -1
|
@@ -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
|
+
})
|