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