@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,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
- })