@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.
- package/.ai-context.md +169 -227
- package/.cursorrules +41 -35
- package/.github/copilot-instructions.md +176 -116
- package/ARCHITECTURE.md +276 -0
- package/CHANGELOG.md +29 -0
- package/CLAUDE.md +201 -143
- package/GUIDE.md +298 -0
- package/README.md +246 -193
- package/REQUIREMENTS.md +100 -0
- package/bench/reactivity.bench.ts +577 -0
- package/context7.json +4 -0
- package/examples/events-sensor.ts +187 -0
- package/examples/selector-sensor.ts +173 -0
- package/index.dev.js +1390 -1008
- package/index.js +1 -1
- package/index.ts +60 -74
- package/package.json +5 -2
- package/skills/changelog-keeper/SKILL.md +59 -0
- package/skills/changelog-keeper/agents/openai.yaml +4 -0
- package/src/errors.ts +118 -74
- package/src/graph.ts +612 -0
- package/src/nodes/collection.ts +512 -0
- package/src/nodes/effect.ts +149 -0
- package/src/nodes/list.ts +589 -0
- package/src/nodes/memo.ts +148 -0
- package/src/nodes/sensor.ts +149 -0
- package/src/nodes/state.ts +135 -0
- package/src/nodes/store.ts +378 -0
- package/src/nodes/task.ts +174 -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 +456 -707
- package/test/effect.test.ts +293 -696
- package/test/list.test.ts +335 -592
- package/test/memo.test.ts +574 -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 +529 -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 +218 -0
- package/types/src/nodes/collection.d.ts +69 -0
- package/types/src/nodes/effect.d.ts +48 -0
- package/types/src/nodes/list.d.ts +66 -0
- package/types/src/nodes/memo.d.ts +63 -0
- package/types/src/nodes/sensor.d.ts +81 -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 +79 -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/match.test.ts
DELETED
|
@@ -1,388 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import { Memo, match, resolve, State, Task, UNSET } from '../index.ts'
|
|
3
|
-
|
|
4
|
-
/* === Tests === */
|
|
5
|
-
|
|
6
|
-
describe('Match Function', () => {
|
|
7
|
-
test('should call ok handler for successful resolution', () => {
|
|
8
|
-
const a = new State(10)
|
|
9
|
-
const b = new State('hello')
|
|
10
|
-
let okCalled = false
|
|
11
|
-
let okValues: { a: number; b: string } | null = null
|
|
12
|
-
|
|
13
|
-
match(resolve({ a, b }), {
|
|
14
|
-
ok: values => {
|
|
15
|
-
okCalled = true
|
|
16
|
-
okValues = values
|
|
17
|
-
},
|
|
18
|
-
err: () => {
|
|
19
|
-
throw new Error('Should not be called')
|
|
20
|
-
},
|
|
21
|
-
nil: () => {
|
|
22
|
-
throw new Error('Should not be called')
|
|
23
|
-
},
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
expect(okCalled).toBe(true)
|
|
27
|
-
expect(okValues).toBeTruthy()
|
|
28
|
-
expect((okValues as unknown as { a: number; b: string }).a).toBe(10)
|
|
29
|
-
expect((okValues as unknown as { a: number; b: string }).b).toBe(
|
|
30
|
-
'hello',
|
|
31
|
-
)
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
test('should call nil handler for pending signals', () => {
|
|
35
|
-
const a = new State(10)
|
|
36
|
-
const b = new State(UNSET)
|
|
37
|
-
let nilCalled = false
|
|
38
|
-
|
|
39
|
-
match(resolve({ a, b }), {
|
|
40
|
-
ok: () => {
|
|
41
|
-
throw new Error('Should not be called')
|
|
42
|
-
},
|
|
43
|
-
err: () => {
|
|
44
|
-
throw new Error('Should not be called')
|
|
45
|
-
},
|
|
46
|
-
nil: () => {
|
|
47
|
-
nilCalled = true
|
|
48
|
-
},
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
expect(nilCalled).toBe(true)
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
test('should call error handler for error signals', () => {
|
|
55
|
-
const a = new State(10)
|
|
56
|
-
const b = new Memo(() => {
|
|
57
|
-
throw new Error('Test error')
|
|
58
|
-
})
|
|
59
|
-
let errCalled = false
|
|
60
|
-
let errValue: Error | null = null
|
|
61
|
-
|
|
62
|
-
match(resolve({ a, b }), {
|
|
63
|
-
ok: () => {
|
|
64
|
-
throw new Error('Should not be called')
|
|
65
|
-
},
|
|
66
|
-
err: errors => {
|
|
67
|
-
errCalled = true
|
|
68
|
-
errValue = errors[0]
|
|
69
|
-
},
|
|
70
|
-
nil: () => {
|
|
71
|
-
throw new Error('Should not be called')
|
|
72
|
-
},
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
expect(errCalled).toBe(true)
|
|
76
|
-
expect(errValue).toBeTruthy()
|
|
77
|
-
expect((errValue as unknown as Error).message).toBe('Test error')
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
test('should handle missing optional handlers gracefully', () => {
|
|
81
|
-
const a = new State(10)
|
|
82
|
-
const result = resolve({ a })
|
|
83
|
-
|
|
84
|
-
// Should not throw even with only required ok handler (err and nil are optional)
|
|
85
|
-
expect(() => {
|
|
86
|
-
match(result, {
|
|
87
|
-
ok: () => {
|
|
88
|
-
// This handler is required, but err and nil are optional
|
|
89
|
-
},
|
|
90
|
-
})
|
|
91
|
-
}).not.toThrow()
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
test('should return void always', () => {
|
|
95
|
-
const a = new State(42)
|
|
96
|
-
|
|
97
|
-
const returnValue = match(resolve({ a }), {
|
|
98
|
-
ok: () => {
|
|
99
|
-
// Even if we try to return something, match should return void
|
|
100
|
-
return 'something'
|
|
101
|
-
},
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
expect(returnValue).toBeUndefined()
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
test('should handle handler errors by calling error handler', () => {
|
|
108
|
-
const a = new State(10)
|
|
109
|
-
let handlerErrorCalled = false
|
|
110
|
-
let handlerError: Error | null = null
|
|
111
|
-
|
|
112
|
-
match(resolve({ a }), {
|
|
113
|
-
ok: () => {
|
|
114
|
-
throw new Error('Handler error')
|
|
115
|
-
},
|
|
116
|
-
err: errors => {
|
|
117
|
-
handlerErrorCalled = true
|
|
118
|
-
handlerError = errors[errors.length - 1] // Last error should be the handler error
|
|
119
|
-
},
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
expect(handlerErrorCalled).toBe(true)
|
|
123
|
-
expect(handlerError).toBeTruthy()
|
|
124
|
-
expect((handlerError as unknown as Error).message).toBe('Handler error')
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
test('should rethrow handler errors if no error handler available', () => {
|
|
128
|
-
const a = new State(10)
|
|
129
|
-
|
|
130
|
-
expect(() => {
|
|
131
|
-
match(resolve({ a }), {
|
|
132
|
-
ok: () => {
|
|
133
|
-
throw new Error('Handler error')
|
|
134
|
-
},
|
|
135
|
-
})
|
|
136
|
-
}).toThrow('Handler error')
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
test('should combine existing errors with handler errors', () => {
|
|
140
|
-
const a = new Memo(() => {
|
|
141
|
-
throw new Error('Signal error')
|
|
142
|
-
})
|
|
143
|
-
let allErrors: readonly Error[] | null = null
|
|
144
|
-
|
|
145
|
-
match(resolve({ a }), {
|
|
146
|
-
ok: () => {
|
|
147
|
-
// This won't be called since there are errors, but it's required
|
|
148
|
-
},
|
|
149
|
-
err: errors => {
|
|
150
|
-
// First call with signal error
|
|
151
|
-
if (errors.length === 1) {
|
|
152
|
-
throw new Error('Handler error')
|
|
153
|
-
}
|
|
154
|
-
// Second call with both errors
|
|
155
|
-
allErrors = errors
|
|
156
|
-
},
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
expect(allErrors).toBeTruthy()
|
|
160
|
-
expect((allErrors as unknown as readonly Error[]).length).toBe(2)
|
|
161
|
-
expect((allErrors as unknown as readonly Error[])[0].message).toBe(
|
|
162
|
-
'Signal error',
|
|
163
|
-
)
|
|
164
|
-
expect((allErrors as unknown as readonly Error[])[1].message).toBe(
|
|
165
|
-
'Handler error',
|
|
166
|
-
)
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
test('should work with complex type inference', () => {
|
|
170
|
-
const user = new State({ id: 1, name: 'Alice' })
|
|
171
|
-
const posts = new State([{ id: 1, title: 'Hello' }])
|
|
172
|
-
const settings = new State({ theme: 'dark' })
|
|
173
|
-
|
|
174
|
-
let typeTestPassed = false
|
|
175
|
-
|
|
176
|
-
match(resolve({ user, posts, settings }), {
|
|
177
|
-
ok: values => {
|
|
178
|
-
// TypeScript should infer these types perfectly
|
|
179
|
-
const userId: number = values.user.id
|
|
180
|
-
const userName: string = values.user.name
|
|
181
|
-
const firstPost = values.posts[0]
|
|
182
|
-
const postTitle: string = firstPost.title
|
|
183
|
-
const theme: string = values.settings.theme
|
|
184
|
-
|
|
185
|
-
expect(userId).toBe(1)
|
|
186
|
-
expect(userName).toBe('Alice')
|
|
187
|
-
expect(postTitle).toBe('Hello')
|
|
188
|
-
expect(theme).toBe('dark')
|
|
189
|
-
typeTestPassed = true
|
|
190
|
-
},
|
|
191
|
-
})
|
|
192
|
-
|
|
193
|
-
expect(typeTestPassed).toBe(true)
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
test('should handle side effects only pattern', () => {
|
|
197
|
-
const count = new State(5)
|
|
198
|
-
const name = new State('test')
|
|
199
|
-
let sideEffectExecuted = false
|
|
200
|
-
let capturedData = ''
|
|
201
|
-
|
|
202
|
-
match(resolve({ count, name }), {
|
|
203
|
-
ok: values => {
|
|
204
|
-
// Pure side effect - no return value expected
|
|
205
|
-
sideEffectExecuted = true
|
|
206
|
-
capturedData = `${values.name}: ${values.count}`
|
|
207
|
-
// Even if we try to return something, it should be ignored
|
|
208
|
-
return 'ignored'
|
|
209
|
-
},
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
expect(sideEffectExecuted).toBe(true)
|
|
213
|
-
expect(capturedData).toBe('test: 5')
|
|
214
|
-
})
|
|
215
|
-
|
|
216
|
-
test('should handle multiple error types correctly', () => {
|
|
217
|
-
const error1 = new Memo(() => {
|
|
218
|
-
throw new Error('First error')
|
|
219
|
-
})
|
|
220
|
-
const error2 = new Memo(() => {
|
|
221
|
-
throw new Error('Second error')
|
|
222
|
-
})
|
|
223
|
-
let errorMessages: string[] = []
|
|
224
|
-
|
|
225
|
-
match(resolve({ error1, error2 }), {
|
|
226
|
-
ok: () => {
|
|
227
|
-
// This won't be called since there are errors, but it's required
|
|
228
|
-
},
|
|
229
|
-
err: errors => {
|
|
230
|
-
errorMessages = errors.map(e => e.message)
|
|
231
|
-
},
|
|
232
|
-
})
|
|
233
|
-
|
|
234
|
-
expect(errorMessages).toHaveLength(2)
|
|
235
|
-
expect(errorMessages).toContain('First error')
|
|
236
|
-
expect(errorMessages).toContain('Second error')
|
|
237
|
-
})
|
|
238
|
-
|
|
239
|
-
test('should work with async computed signals', async () => {
|
|
240
|
-
const wait = (ms: number) =>
|
|
241
|
-
new Promise(resolve => setTimeout(resolve, ms))
|
|
242
|
-
|
|
243
|
-
const asyncSignal = new Task(async () => {
|
|
244
|
-
await wait(10)
|
|
245
|
-
return 'async result'
|
|
246
|
-
})
|
|
247
|
-
|
|
248
|
-
// Initially should be pending
|
|
249
|
-
let pendingCalled = false
|
|
250
|
-
let okCalled = false
|
|
251
|
-
let finalValue = ''
|
|
252
|
-
|
|
253
|
-
let result = resolve({ asyncSignal })
|
|
254
|
-
match(result, {
|
|
255
|
-
ok: values => {
|
|
256
|
-
okCalled = true
|
|
257
|
-
finalValue = values.asyncSignal
|
|
258
|
-
},
|
|
259
|
-
nil: () => {
|
|
260
|
-
pendingCalled = true
|
|
261
|
-
},
|
|
262
|
-
})
|
|
263
|
-
|
|
264
|
-
expect(pendingCalled).toBe(true)
|
|
265
|
-
expect(okCalled).toBe(false)
|
|
266
|
-
|
|
267
|
-
// Wait for resolution
|
|
268
|
-
await wait(20)
|
|
269
|
-
|
|
270
|
-
result = resolve({ asyncSignal })
|
|
271
|
-
match(result, {
|
|
272
|
-
ok: values => {
|
|
273
|
-
okCalled = true
|
|
274
|
-
finalValue = values.asyncSignal
|
|
275
|
-
},
|
|
276
|
-
})
|
|
277
|
-
|
|
278
|
-
expect(okCalled).toBe(true)
|
|
279
|
-
expect(finalValue).toBe('async result')
|
|
280
|
-
})
|
|
281
|
-
|
|
282
|
-
test('should maintain referential transparency', () => {
|
|
283
|
-
const a = new State(42)
|
|
284
|
-
const result = resolve({ a })
|
|
285
|
-
let callCount = 0
|
|
286
|
-
|
|
287
|
-
// Calling match multiple times with same result should be consistent
|
|
288
|
-
match(result, {
|
|
289
|
-
ok: () => {
|
|
290
|
-
callCount++
|
|
291
|
-
},
|
|
292
|
-
})
|
|
293
|
-
|
|
294
|
-
match(result, {
|
|
295
|
-
ok: () => {
|
|
296
|
-
callCount++
|
|
297
|
-
},
|
|
298
|
-
})
|
|
299
|
-
|
|
300
|
-
expect(callCount).toBe(2)
|
|
301
|
-
})
|
|
302
|
-
})
|
|
303
|
-
|
|
304
|
-
describe('Match Function Integration', () => {
|
|
305
|
-
test('should work seamlessly with resolve', () => {
|
|
306
|
-
const data = new State({ id: 1, value: 'test' })
|
|
307
|
-
let processed = false
|
|
308
|
-
let processedValue = ''
|
|
309
|
-
|
|
310
|
-
match(resolve({ data }), {
|
|
311
|
-
ok: values => {
|
|
312
|
-
processed = true
|
|
313
|
-
processedValue = values.data.value
|
|
314
|
-
},
|
|
315
|
-
})
|
|
316
|
-
|
|
317
|
-
expect(processed).toBe(true)
|
|
318
|
-
expect(processedValue).toBe('test')
|
|
319
|
-
})
|
|
320
|
-
|
|
321
|
-
test('should handle real-world scenario with mixed states', async () => {
|
|
322
|
-
const wait = (ms: number) =>
|
|
323
|
-
new Promise(resolve => setTimeout(resolve, ms))
|
|
324
|
-
|
|
325
|
-
const syncData = new State('available')
|
|
326
|
-
const asyncData = new Task(async () => {
|
|
327
|
-
await wait(10)
|
|
328
|
-
return 'loaded'
|
|
329
|
-
})
|
|
330
|
-
const errorData = new Memo(() => {
|
|
331
|
-
throw new Error('Failed to load')
|
|
332
|
-
})
|
|
333
|
-
|
|
334
|
-
let pendingCount = 0
|
|
335
|
-
let errorCount = 0
|
|
336
|
-
let successCount = 0
|
|
337
|
-
|
|
338
|
-
// Should be pending initially
|
|
339
|
-
let result = resolve({ syncData, asyncData })
|
|
340
|
-
match(result, {
|
|
341
|
-
ok: () => successCount++,
|
|
342
|
-
err: () => errorCount++,
|
|
343
|
-
nil: () => pendingCount++,
|
|
344
|
-
})
|
|
345
|
-
|
|
346
|
-
expect(pendingCount).toBe(1)
|
|
347
|
-
|
|
348
|
-
// Should have errors when including error signal
|
|
349
|
-
result = resolve({ syncData, asyncData, errorData })
|
|
350
|
-
match(result, {
|
|
351
|
-
ok: () => successCount++,
|
|
352
|
-
err: () => errorCount++,
|
|
353
|
-
nil: () => pendingCount++,
|
|
354
|
-
})
|
|
355
|
-
|
|
356
|
-
expect(pendingCount).toBe(2) // Still pending due to async
|
|
357
|
-
|
|
358
|
-
// Wait for async to resolve
|
|
359
|
-
await wait(20)
|
|
360
|
-
|
|
361
|
-
// Should succeed with just sync and async
|
|
362
|
-
result = resolve({ syncData, asyncData })
|
|
363
|
-
match(result, {
|
|
364
|
-
ok: values => {
|
|
365
|
-
successCount++
|
|
366
|
-
expect(values.syncData).toBe('available')
|
|
367
|
-
expect(values.asyncData).toBe('loaded')
|
|
368
|
-
},
|
|
369
|
-
err: () => errorCount++,
|
|
370
|
-
nil: () => pendingCount++,
|
|
371
|
-
})
|
|
372
|
-
|
|
373
|
-
// Should error when including error signal
|
|
374
|
-
result = resolve({ syncData, asyncData, errorData })
|
|
375
|
-
match(result, {
|
|
376
|
-
ok: () => successCount++,
|
|
377
|
-
err: errors => {
|
|
378
|
-
errorCount++
|
|
379
|
-
expect(errors[0].message).toBe('Failed to load')
|
|
380
|
-
},
|
|
381
|
-
nil: () => pendingCount++,
|
|
382
|
-
})
|
|
383
|
-
|
|
384
|
-
expect(successCount).toBe(1)
|
|
385
|
-
expect(errorCount).toBe(1)
|
|
386
|
-
expect(pendingCount).toBe(2)
|
|
387
|
-
})
|
|
388
|
-
})
|