@zeix/cause-effect 0.16.1 → 0.17.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 (66) hide show
  1. package/.ai-context.md +85 -21
  2. package/.cursorrules +11 -5
  3. package/.github/copilot-instructions.md +64 -13
  4. package/CLAUDE.md +143 -163
  5. package/LICENSE +1 -1
  6. package/README.md +248 -333
  7. package/archive/benchmark.ts +688 -0
  8. package/archive/collection.ts +312 -0
  9. package/{src → archive}/computed.ts +21 -21
  10. package/archive/list.ts +551 -0
  11. package/archive/memo.ts +139 -0
  12. package/{src → archive}/state.ts +13 -11
  13. package/archive/store.ts +368 -0
  14. package/archive/task.ts +194 -0
  15. package/eslint.config.js +1 -0
  16. package/index.dev.js +938 -509
  17. package/index.js +1 -1
  18. package/index.ts +50 -23
  19. package/package.json +1 -1
  20. package/src/classes/collection.ts +282 -0
  21. package/src/classes/composite.ts +176 -0
  22. package/src/classes/computed.ts +333 -0
  23. package/src/classes/list.ts +305 -0
  24. package/src/classes/ref.ts +68 -0
  25. package/src/classes/state.ts +98 -0
  26. package/src/classes/store.ts +210 -0
  27. package/src/diff.ts +26 -53
  28. package/src/effect.ts +9 -9
  29. package/src/errors.ts +71 -25
  30. package/src/match.ts +5 -12
  31. package/src/resolve.ts +3 -2
  32. package/src/signal.ts +58 -41
  33. package/src/system.ts +79 -42
  34. package/src/util.ts +16 -34
  35. package/test/batch.test.ts +15 -17
  36. package/test/benchmark.test.ts +4 -4
  37. package/test/collection.test.ts +853 -0
  38. package/test/computed.test.ts +138 -130
  39. package/test/diff.test.ts +2 -2
  40. package/test/effect.test.ts +36 -35
  41. package/test/list.test.ts +754 -0
  42. package/test/match.test.ts +25 -25
  43. package/test/ref.test.ts +227 -0
  44. package/test/resolve.test.ts +17 -19
  45. package/test/signal.test.ts +70 -119
  46. package/test/state.test.ts +44 -44
  47. package/test/store.test.ts +253 -929
  48. package/types/index.d.ts +12 -9
  49. package/types/src/classes/collection.d.ts +46 -0
  50. package/types/src/classes/composite.d.ts +15 -0
  51. package/types/src/classes/computed.d.ts +97 -0
  52. package/types/src/classes/list.d.ts +41 -0
  53. package/types/src/classes/ref.d.ts +39 -0
  54. package/types/src/classes/state.d.ts +52 -0
  55. package/types/src/classes/store.d.ts +51 -0
  56. package/types/src/diff.d.ts +8 -12
  57. package/types/src/errors.d.ts +17 -11
  58. package/types/src/signal.d.ts +27 -14
  59. package/types/src/system.d.ts +41 -20
  60. package/types/src/util.d.ts +6 -4
  61. package/src/store.ts +0 -474
  62. package/types/src/collection.d.ts +0 -26
  63. package/types/src/computed.d.ts +0 -33
  64. package/types/src/scheduler.d.ts +0 -55
  65. package/types/src/state.d.ts +0 -24
  66. package/types/src/store.d.ts +0 -65
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Cause & Effect
2
2
 
3
- Version 0.16.1
3
+ Version 0.17.1
4
4
 
5
- **Cause & Effect** is a lightweight, reactive state management library for JavaScript applications. It uses fine-grained reactivity with signals to create predictable and efficient data flow in your app.
5
+ **Cause & Effect** is a tiny (~5kB gzipped), dependency-free reactive state library for JavaScript. It uses fine-grained signals so derived values and side effects update automatically when their dependencies change.
6
6
 
7
7
  ## What is Cause & Effect?
8
8
 
@@ -10,31 +10,44 @@ Version 0.16.1
10
10
 
11
11
  ### Core Concepts
12
12
 
13
- - **State signals**: Hold values that can be directly modified: `createState()`
14
- - **Store signals**: Hold objects of nested reactive properties: `createStore()`
15
- - **Computed signals**: Derive memoized values from other signals: `createComputed()`
16
- - **Effects**: Run side effects when signals change: `createEffect()`
13
+ - **State**: mutable value (`new State()`)
14
+ - **Memo**: derived & memoized value (`new Memo()`)
15
+ - **Effect**: runs when dependencies change (`createEffect()`)
16
+ - **Task**: async derived value with cancellation (`new Task()`)
17
+ - **Store**: object with reactive nested props (`createStore()`)
18
+ - **List**: mutable array with stable keys & reactive items (`new List()`)
19
+ - **Collection**: read-only derived arrays from Lists (`new DerivedCollection()`)
20
+ - **Ref**: external mutable objects + manual .notify() (`new Ref()`)
17
21
 
18
22
  ## Key Features
19
23
 
20
- - ⚡ **Reactive States**: Automatic updates when dependencies change
21
- - 🧩 **Composable**: Create a complex signal graph with a minimal API
22
- - ⏱️ **Async Ready**: Built-in `Promise` and `AbortController` support
23
- - 🛡️ **Error Handling**: Built-in helper functions for declarative error handling
24
- - 🔧 **Helper Functions**: `resolve()` and `match()` for type-safe value extraction and pattern matching for suspense and error boundaries
25
- - 🚀 **Performance**: Batching and efficient dependency tracking
26
- - 📦 **Tiny**: Less than 3kB gzipped, tree-shakable, zero dependencies
24
+ - ⚡ **Fine-grained reactivity** with automatic dependency tracking
25
+ - 🧩 **Composable signal graph** with a small API
26
+ - ⏱️ **Async ready** (`Task`, `AbortController`, async `DerivedCollection`)
27
+ - 🛡️ **Declarative error handling** (`resolve()` + `match()`)
28
+ - 🚀 **Batching** and efficient dependency tracking
29
+ - 📦 **Tree-shakable**, zero dependencies
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ # with npm
35
+ npm install @zeix/cause-effect
36
+
37
+ # or with bun
38
+ bun add @zeix/cause-effect
39
+ ```
27
40
 
28
41
  ## Quick Start
29
42
 
30
43
  ```js
31
- import { createState, createComputed, createEffect } from '@zeix/cause-effect'
44
+ import { createEffect, Memo, State } from '@zeix/cause-effect'
32
45
 
33
46
  // 1. Create state
34
- const user = createState({ name: 'Alice', age: 30 })
47
+ const user = new State({ name: 'Alice', age: 30 })
35
48
 
36
49
  // 2. Create computed values
37
- const greeting = createComputed(() => `Hello ${user.get().name}!`)
50
+ const greeting = new Memo(() => `Hello ${user.get().name}!`)
38
51
 
39
52
  // 3. React to changes
40
53
  createEffect(() => {
@@ -45,422 +58,334 @@ createEffect(() => {
45
58
  user.update(u => ({ ...u, age: 31 })) // Logs: "Hello Alice! You are 31 years old"
46
59
  ```
47
60
 
48
- ## Installation
49
-
50
- ```bash
51
- # with npm
52
- npm install @zeix/cause-effect
53
-
54
- # or with bun
55
- bun add @zeix/cause-effect
56
- ```
57
-
58
61
  ## Usage of Signals
59
62
 
60
- ### State Signals
63
+ ### State
61
64
 
62
- `createState()` creates a mutable signal. Every signal has a `.get()` method to access its current value. State signals also provide `.set()` to directly assign a new value and `.update()` to modify the value with a function.
65
+ A `State` is a mutable signal. Every signal has a `.get()` method to access its current value. State signals also provide `.set()` to directly assign a new value and `.update()` to modify the value with a function.
63
66
 
64
67
  ```js
65
- import { createState, createEffect } from '@zeix/cause-effect'
68
+ import { createEffect, State } from '@zeix/cause-effect'
69
+
70
+ const count = new State(42)
71
+
72
+ createEffect(() => console.log(count.get()))
73
+ count.set(24)
66
74
 
67
- const count = createState(42)
68
- createEffect(() => {
69
- console.log(count.get()) // logs '42'
70
- })
71
- count.set(24) // logs '24'
72
75
  document.querySelector('.increment').addEventListener('click', () => {
73
76
  count.update(v => ++v)
74
77
  })
75
- // Click on button logs '25', '26', and so on
76
78
  ```
77
79
 
78
- ### Store Signals
80
+ Use `State` for primitives or for objects you typically replace entirely.
79
81
 
80
- `createStore()` creates a mutable signal that holds an object with nested reactive properties. Each property automatically becomes its own signal with `.get()`, `.set()`, and `.update()` methods. Nested objects recursively become nested stores.
82
+ ### Memo
81
83
 
82
- ```js
83
- import { createStore, createEffect } from '@zeix/cause-effect'
84
+ A `Memo` is a memoized read-only signal that automatically tracks dependencies and updates only when those dependencies change.
84
85
 
85
- const user = createStore({
86
- name: 'Alice',
87
- age: 30,
88
- preferences: {
89
- theme: 'dark',
90
- notifications: true
91
- }
92
- })
86
+ ```js
87
+ import { State, Memo, createEffect } from '@zeix/cause-effect'
93
88
 
94
- // Individual properties are reactive
95
- createEffect(() => {
96
- console.log(`${user.name.get()} is ${user.age.get()} years old`)
97
- })
89
+ const count = new State(42)
90
+ const isEven = new Memo(() => !(count.get() % 2))
98
91
 
99
- // Nested properties work the same way
100
- createEffect(() => {
101
- console.log(`Theme: ${user.preferences.theme.get()}`)
102
- })
92
+ createEffect(() => console.log(isEven.get()))
93
+ count.set(24) // no log; still even
94
+ ```
103
95
 
104
- // Update individual properties
105
- user.age.update(v => v + 1) // Logs: "Alice is 31 years old"
106
- user.preferences.theme.set('light') // Logs: "Theme: light"
96
+ **Tip**: For simple derivations, a plain function can be faster:
107
97
 
108
- // Watch the entire store
109
- createEffect(() => {
110
- console.log('User data:', user.get()) // Triggers on any nested change
111
- })
98
+ ```js
99
+ const isEven = () => !(count.get() % 2)
112
100
  ```
113
101
 
114
- #### When to Use
115
-
116
- **When to use stores vs states:**
102
+ **Advanced**: Reducer-style memos:
117
103
 
118
- - **Use `createStore()`** for objects with properties that you want to access and modify individually.
119
- - **Use `createState()`** for primitive values (numbers, strings, booleans) or objects you access and replace entirely.
104
+ ```js
105
+ import { State, Memo } from '@zeix/cause-effect'
106
+
107
+ const actions = new State('reset')
108
+ const counter = new Memo((prev) => {
109
+ switch (actions.get()) {
110
+ case 'increment': return prev + 1
111
+ case 'decrement': return prev - 1
112
+ case 'reset': return 0
113
+ default: return prev
114
+ }
115
+ }, 0)
116
+ ```
120
117
 
121
- #### Dynamic Properties
118
+ ### Task
122
119
 
123
- Stores support dynamic property addition and removal at runtime using the `add()` and `remove()` methods:
120
+ A `Task` handles asynchronous computations with cancellation support:
124
121
 
125
122
  ```js
126
- import { createStore, createEffect } from '@zeix/cause-effect'
123
+ import { State, Task } from '@zeix/cause-effect'
127
124
 
128
- const settings = store({ autoSave: true })
125
+ const id = new State(1)
129
126
 
130
- // Add new properties at runtime
131
- settings.add('timeout', 5000)
132
- console.log(settings.timeout.get()) // 5000
133
-
134
- // Adding an existing property has no effect
135
- settings.add('autoSave', false) // Ignored - autoSave remains true
136
-
137
- // Remove properties
138
- settings.remove('timeout')
139
- console.log(settings.timeout) // undefined
127
+ const data = new Task(async (oldValue, abort) => {
128
+ const response = await fetch(`/api/users/${id.get()}`, { signal: abort })
129
+ if (!response.ok) throw new Error('Failed to fetch')
130
+ return response.json()
131
+ })
140
132
 
141
- // Removing non-existent properties has no effect
142
- settings.remove('nonExistent') // Safe - no error thrown
133
+ id.set(2) // cancels previous fetch automatically
143
134
  ```
144
135
 
145
- The `add()` and `remove()` methods are optimized for performance:
146
- - They bypass the full reconciliation process used by `set()` and `update()`
147
- - They're perfect for frequent single-property additions/removals
148
- - They trigger the same events and reactivity as other store operations
136
+ **Note**: Use Task (not plain async functions) when you want memoization + cancellation + reactive pending/error states.
149
137
 
150
- #### Array-like Stores
138
+ ### Store
151
139
 
152
- Stores created from arrays behave like arrays with reactive properties. They support duck-typing with length property, single-parameter `add()`, and efficient sorting:
140
+ A `Store` is a reactive object. Each property automatically becomes its own signal with `.get()`, `.set()`, and `.update()` methods. Nested objects recursively become nested stores.
153
141
 
154
142
  ```js
155
143
  import { createStore, createEffect } from '@zeix/cause-effect'
156
144
 
157
- const items = createStore(['banana', 'apple', 'cherry'])
158
-
159
- // Duck-typing: behaves like an array
160
- console.log(items.length) // 3
161
- console.log(typeof items.length) // 'number'
145
+ const user = createStore({
146
+ name: 'Alice',
147
+ age: 30,
148
+ preferences: { theme: 'dark', notifications: true }
149
+ })
162
150
 
163
- // Individual items are reactive
164
151
  createEffect(() => {
165
- console.log(`First item: ${items[0].get()}`)
152
+ console.log(`${user.name.get()} is ${user.age.get()} years old`)
166
153
  })
167
154
 
168
- // Single-parameter add() appends to end
169
- items.add('date') // Adds at index 3
170
- console.log(items.get()) // ['banana', 'apple', 'cherry', 'date']
171
-
172
- // Efficient sorting preserves signal references
173
- items.sort() // Default: string comparison
174
- console.log(items.get()) // ['apple', 'banana', 'cherry', 'date']
155
+ user.age.update(v => v + 1)
156
+ user.preferences.theme.set('light')
175
157
 
176
- // Custom sorting
177
- items.sort((a, b) => b.localeCompare(a)) // Reverse alphabetical
178
- console.log(items.get()) // ['date', 'cherry', 'banana', 'apple']
158
+ // Watch the full object
159
+ createEffect(() => console.log('User:', user.get()))
179
160
  ```
180
161
 
181
- #### Store Change Notifications
182
-
183
- Stores emit notifications (sort of light-weight events) when properties are added, changed, or removed. You can listen to these notications using the `.on()` method:
162
+ Dynamic properties using the `add()` and `remove()` methods:
184
163
 
185
164
  ```js
186
- import { createStore } from '@zeix/cause-effect'
165
+ const settings = createStore({ autoSave: true })
187
166
 
188
- const user = createStore({ name: 'Alice', age: 30 })
167
+ settings.add('timeout', 5000)
168
+ settings.remove('timeout')
169
+ ```
189
170
 
190
- // Listen for property additions
191
- const offAdd = user.on('add', (added) => {
192
- console.log('Added properties:', added)
193
- })
171
+ Change Notifications using the `.on()` method:
194
172
 
195
- // Listen for property changes
196
- const offChange = user.on('change', (changed) => {
197
- console.log('Changed properties:', changed)
198
- })
173
+ ```js
174
+ const user = createStore({ name: 'Alice', age: 30 })
199
175
 
200
- // Listen for property removals
201
- const offRemove = user.on('remove', (removed) => {
202
- console.log('Removed properties:', removed)
203
- })
176
+ const offChange = user.on('change', changed => console.log(changed))
177
+ const offAdd = user.on('add', added => console.log(added))
178
+ const offRemove = user.on('remove', removed => console.log(removed))
204
179
 
205
180
  // These will trigger the respective notifications:
206
- user.add('email', 'alice@example.com') // Logs: "Added properties: { email: 'alice@example.com' }"
207
- user.age.set(31) // Logs: "Changed properties: { age: 31 }"
208
- user.remove('email') // Logs: "Removed properties: { email: UNSET }"
209
-
210
- // Listen for sort notifications (useful for UI animations)
211
- const items = createStore(['banana', 'apple', 'cherry'])
212
- items.sort((a, b) => b.localeCompare(a)) // Reverse alphabetical
213
- const offSort = items.on('sort', (newOrder) => {
214
- console.log('Items reordered:', newOrder) // ['2', '1', '0']
215
- })
216
- ```
181
+ user.add('email', 'alice@example.com') // Logs: "Added properties: ['email']"
182
+ user.age.set(31) // Logs: "Changed properties: ['age']"
183
+ user.remove('email') // Logs: "Removed properties: ['email']"
217
184
 
218
- Notifications are also fired when using `set()` or `update()` methods on the entire store:
185
+ To stop listening to notifications, call the returned cleanup functions:
219
186
 
220
187
  ```js
221
- // This will fire multiple notifications based on what changed
222
- user.update(u => ({ ...u, name: 'Bob', city: 'New York' }))
223
- // Logs: "Changed properties: { name: 'Bob' }"
224
- // Logs: "Added properties: { city: 'New York' }"
188
+ offAdd() // Stop listening to add notifications
189
+ offChange() // Stop listening to change notifications
190
+ offRemove() // Stop listening to remove notifications
225
191
  ```
226
192
 
227
- To stop listening to notifications, call the returned cleanup function:
193
+ ### List
228
194
 
229
- ```js
230
- offAdd() // Stops listening to add notifications
231
- offChange() // Stops listening to change notifications
232
- offRemove() // Stops listening to remove notifications
233
- offSort() // Stops listening to sort notifications
234
- ```
195
+ A `List` is a mutable signal for arrays with individually reactive items and stable keys. Each item becomes its own signal while maintaining persistent identity through sorting and reordering:
235
196
 
236
- ### Computed Signals
197
+ ```js
198
+ import { List, createEffect } from '@zeix/cause-effect'
237
199
 
238
- `createComputed()` creates a memoized read-only signal that automatically tracks dependencies and updates only when those dependencies change.
200
+ const items = new List(['banana', 'apple', 'cherry'])
239
201
 
240
- ```js
241
- import { createState, createComputed, createEffect } from '@zeix/cause-effect'
202
+ createEffect(() => console.log(`First: ${items[0].get()}`))
242
203
 
243
- const count = createState(42)
244
- const isEven = createComputed(() => !(count.get() % 2))
245
- createEffect(() => console.log(isEven.get())) // logs 'true'
246
- count.set(24) // logs nothing because 24 is also an even number
247
- document.querySelector('button.increment').addEventListener('click', () => {
248
- count.update(v => ++v)
249
- })
250
- // Click on button logs 'false', 'true', and so on
204
+ items.add('date')
205
+ items.splice(1, 1, 'orange')
206
+ items.sort()
251
207
  ```
252
208
 
253
- #### When to Use
254
-
255
- **Performance tip**: For simple derivations, plain functions often outperform computed signals:
209
+ Keys are stable across reordering:
256
210
 
257
211
  ```js
258
- // More performant for simple calculations
259
- const isEven = () => !(count.get() % 2)
212
+ const items = new List(['banana', 'apple'], 'item-')
213
+ const key = items.add('orange')
214
+
215
+ items.sort()
216
+ console.log(items.byKey(key)) // 'orange'
217
+ console.log(items.indexOfKey(key)) // current index
260
218
  ```
261
219
 
262
- **When to use which approach:**
220
+ Lists have `.add()`, `.remove()` and `.on()` methods like stores. In addition, they have `.sort()` and `.splice()` methods. But unlike stores, deeply nested properties in items are not converted to individual signals.
263
221
 
264
- - **Use functions when**: The calculation is simple, inexpensive, or called infrequently.
265
- - **Use createComputed() when**:
266
- - The calculation is expensive
267
- - You need to share the result between multiple consumers
268
- - You're working with asynchronous operations
269
- - You need to track specific error states
270
-
271
- #### Reducer-like Capabilities
222
+ ### Collection
272
223
 
273
- `createComputed()` supports reducer-like patterns by accepting an initial value and providing access to the previous value in the callback:
224
+ A `Collection` is a read-only derived reactive list from `List` or another `Collection`:
274
225
 
275
226
  ```js
276
- import { createState, createComputed, createEffect } from '@zeix/cause-effect'
277
-
278
- const actions = createState('increment')
279
- const counter = createComputed((prev, abort) => {
280
- const action = actions.get()
281
- switch (action) {
282
- case 'increment':
283
- return prev + 1
284
- case 'decrement':
285
- return prev - 1
286
- case 'reset':
287
- return 0
288
- default:
289
- return prev
290
- }
291
- }, 0) // Initial value of 0
292
-
293
- createEffect(() => console.log('Counter:', counter.get()))
294
-
295
- // Dispatch actions
296
- actions.set('increment') // Counter: 1
297
- actions.set('increment') // Counter: 2
298
- actions.set('decrement') // Counter: 1
299
- actions.set('reset') // Counter: 0
227
+ import { List, createEffect } from '@zeix/cause-effect'
228
+
229
+ const users = new List([
230
+ { id: 1, name: 'Alice', role: 'admin' },
231
+ { id: 2, name: 'Bob', role: 'user' }
232
+ ])
233
+ const profiles = users.deriveCollection(user => ({
234
+ ...user,
235
+ displayName: `${user.name} (${user.role})`
236
+ }))
237
+
238
+ createEffect(() => console.log('Profiles:', profiles.get()))
239
+ console.log(userProfiles.at(0).get().displayName)
300
240
  ```
301
241
 
302
- This pattern is particularly useful for:
303
- - State machines with transitions based on current state
304
- - Accumulating values over time
305
- - Complex state updates that depend on previous state
306
- - Building reactive reducers similar to Redux patterns
242
+ Async mapping is supported:
307
243
 
308
- #### Asynchronous Computations with Automatic Cancellation
244
+ ```js
245
+ const details = users.derivedCollection(async (user, abort) => {
246
+ const response = await fetch(`/users/${user.id}`, { signal: abort })
247
+ return { ...user, details: await response.json() }
248
+ })
249
+ ```
309
250
 
310
- `createComputed()` seamlessly handles asynchronous operations with built-in cancellation support. When used with an async function, it:
251
+ ### Ref
311
252
 
312
- 1. Provides an `abort` signal parameter you can pass to fetch or other cancelable APIs
313
- 2. Automatically cancels pending operations when dependencies change
314
- 3. Returns `UNSET` while the Promise is pending
315
- 4. Properly handles errors from failed requests
253
+ A `Ref` is a signal that holds a reference to an external object that can change outside the reactive system.
316
254
 
317
255
  ```js
318
- import { createState, createComputed, createEffect, resolve, match } from '@zeix/cause-effect'
256
+ import { createEffect, Ref } from '@zeix/cause-effect'
319
257
 
320
- const id = createState(42)
321
- const data = createComputed(async (_, abort) => {
322
- // The abort signal is automatically managed by the computed signal
323
- const response = await fetch(`/api/entries/${id.get()}`, { signal: abort })
324
- if (!response.ok) throw new Error(`Failed to fetch data: ${response.statusText}`)
325
- return response.json()
326
- })
258
+ const elementRef = new Ref(document.getElementById('status'))
327
259
 
328
- // Handle all possible states using resolve and match helpers
329
- createEffect(() => {
330
- match(resolve({ data }), {
331
- ok: ({ data: json }) => console.log('Data loaded:', json),
332
- nil: () => console.log('Loading...'),
333
- err: errors => console.error('Error:', errors[0])
334
- })
335
- })
260
+ createEffect(() => console.log(elementRef.get().className))
336
261
 
337
- // When id changes, the previous request is automatically canceled
338
- document.querySelector('button.next').addEventListener('click', () => {
339
- id.update(v => ++v)
340
- })
262
+ // external mutation happened
263
+ elementRef.notify()
341
264
  ```
342
265
 
343
- **Note**: Always use `createComputed()` (not plain functions) for async operations to benefit from automatic cancellation and memoization.
266
+ Use `Ref` for DOM nodes, Maps/Sets, sockets, third-party objects, etc.
344
267
 
345
- ## Effects and Error Handling
268
+ ## Effects
346
269
 
347
- The `createEffect()` function supports both synchronous and asynchronous callbacks:
348
-
349
- ### Synchronous Effects
270
+ The `createEffect()` callback runs whenever the signals it reads change. It supports sync or async callbacks and returns a cleanup function.
350
271
 
351
272
  ```js
352
- import { createState, createEffect } from '@zeix/cause-effect'
273
+ import { State, createEffect } from '@zeix/cause-effect'
353
274
 
354
- const count = createState(42)
355
- createEffect(() => {
356
- console.log('Count changed:', count.get())
275
+ const count = new State(42)
276
+
277
+ const cleanup = createEffect(() => {
278
+ console.log(count.get())
279
+ return () => console.log('Cleanup')
357
280
  })
358
- ```
359
281
 
360
- ### Asynchronous Effects with AbortSignal
282
+ cleanup()
283
+ ```
361
284
 
362
- Async effect callbacks receive an `AbortSignal` parameter that automatically cancels when the effect re-runs or is cleaned up:
285
+ Async effects receive an AbortSignal that cancels on rerun or cleanup:
363
286
 
364
287
  ```js
365
- import { createState, createEffect } from '@zeix/cause-effect'
366
-
367
- const userId = createState(1)
368
- createEffect(async (abort) => {
369
- try {
370
- const response = await fetch(`/api/users/${userId.get()}`, { signal: abort })
371
- const user = await response.json()
372
- console.log('User loaded:', user)
373
- } catch (error) {
374
- if (!abort.aborted) {
375
- console.error('Failed to load user:', error)
376
- }
377
- }
288
+ createEffect(async abort => {
289
+ const res = await fetch('/api', { signal: abort })
290
+ if (res.ok) console.log(await res.json())
378
291
  })
379
292
  ```
380
293
 
381
- ### Error Handling with Helper Functions
294
+ ### Error Handling: resolve() + match()
382
295
 
383
- For more sophisticated error handling, use the `resolve()` and `match()` helper functions:
296
+ Use `resolve()` to extract values from signals (including pending/err states) and `match()` to handle them declaratively:
384
297
 
385
298
  ```js
386
- import { createState, createEffect, resolve, match } from '@zeix/cause-effect'
299
+ import { State, Task, createEffect, resolve, match } from '@zeix/cause-effect'
387
300
 
388
- const userId = createState(1)
389
- const userData = createEffect(async (abort) => {
390
- const response = await fetch(`/api/users/${userId.get()}`, { signal: abort })
391
- if (!response.ok) throw new Error(`HTTP ${response.status}`)
392
- return response.json()
301
+ const userId = new State(1)
302
+ const userData = new Task(async (_, abort) => {
303
+ const res = await fetch(`/api/users/${userId.get()}`, { signal: abort })
304
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
305
+ return res.json()
393
306
  })
394
307
 
395
308
  createEffect(() => {
396
309
  match(resolve({ userData }), {
397
- ok: ({ userData: user }) => console.log('User loaded:', user),
398
- nil: () => console.log('Loading user...'),
399
- err: errors => console.error('Error loading user:', errors[0])
310
+ ok: ({ userData: user }) => console.log('User:', user),
311
+ nil: () => console.log('Loading...'),
312
+ err: errors => console.error(errors[0])
400
313
  })
401
314
  })
402
315
  ```
403
316
 
404
- The `resolve()` function extracts values from signals and returns a discriminated union result, while `match()` provides pattern matching for handling different states declaratively.
317
+ ## Signal Type Decision Tree
318
+
319
+ ```
320
+ Is the value managed *inside* the reactive system?
321
+
322
+ ├─ No → Use `Ref`
323
+ │ (DOM nodes, Map/Set, Date, sockets, 3rd-party objects)
324
+ │ Remember: call `.notify()` when it changes externally.
325
+
326
+ └─ Yes? What kind of data is it?
327
+
328
+ ├─ *Primitive* (number/string/boolean)
329
+ │ │
330
+ │ ├─ Do you want to mutate it directly?
331
+ │ │ └─ Yes → `State`
332
+ │ │
333
+ │ └─ Is it derived from other signals?
334
+ │ │
335
+ │ ├─ Sync derived
336
+ │ │ ├─ Simple/cheap → plain function (preferred)
337
+ │ │ └─ Expensive/shared/stateful → `Memo`
338
+ │ │
339
+ │ └─ Async derived → `Task`
340
+ │ (cancellation + memoization + pending/error state)
341
+
342
+ ├─ *Plain Object*
343
+ │ │
344
+ │ ├─ Do you want to mutate individual properties?
345
+ │ │ ├─ Yes → `Store`
346
+ │ │ └─ No, whole object mutations only → `State`
347
+ │ │
348
+ │ └─ Is it derived from other signals?
349
+ │ ├─ Sync derived → plain function or `Memo`
350
+ │ └─ Async derived → `Task`
351
+
352
+ └─ *Array*
353
+
354
+ ├─ Do you need to mutate it (add/remove/sort) with stable item identity?
355
+ │ ├─ Yes → `List`
356
+ │ └─ No, whole array mutations only → `State`
357
+
358
+ └─ Is it derived / read-only transformation of a `List` or `Collection`?
359
+ └─ Yes → `Collection`
360
+ (memoized + supports async mapping + chaining)
361
+ ```
405
362
 
406
363
  ## Advanced Usage
407
364
 
408
- ### Batching Updates
365
+ ### Batching
409
366
 
410
- Use `batch()` to group multiple signal updates, ensuring effects run only once after all changes are applied:
367
+ Group multiple signal updates, ensuring effects run only once after all changes are applied:
411
368
 
412
369
  ```js
413
- import {
414
- createState,
415
- createComputed,
416
- createEffect,
417
- batch,
418
- resolve,
419
- match
420
- } from '@zeix/cause-effect'
421
-
422
- // State: define an Array<State<number>>
423
- const signals = [createState(2), createState(3), createState(5)]
424
-
425
- // Compute the sum of all signals
426
- const sum = createComputed(() => {
427
- const v = signals.reduce((total, signal) => total + signal.get(), 0)
428
- // Validate the result
429
- if (!Number.isFinite(v)) throw new Error('Invalid value')
430
- return v
431
- })
370
+ import { batchSignalWrites, State } from '@zeix/cause-effect'
432
371
 
433
- // Effect: handle the result with error handling
434
- createEffect(() => {
435
- match(resolve({ sum }), {
436
- ok: ({ sum: v }) => console.log('Sum:', v),
437
- err: errors => console.error('Error:', errors[0])
438
- })
439
- })
372
+ const a = new State(2)
373
+ const b = new State(3)
440
374
 
441
- // Batch: apply changes to all signals in a single transaction
442
- document.querySelector('.double-all').addEventListener('click', () => {
443
- batch(() => {
444
- signals.forEach(signal => {
445
- signal.update(v => v * 2)
446
- })
447
- })
375
+ batchSignalWrites(() => {
376
+ a.set(4)
377
+ b.set(5)
448
378
  })
449
- // Click on button logs '20' only once
450
- // (instead of first '12', then '15' and then '20' without batch)
451
-
452
- // Provoke an error - but no worries: it will be handled fine
453
- signals[0].set(NaN)
454
379
  ```
455
380
 
456
- ### Cleanup Functions
381
+ ### Cleanup
457
382
 
458
383
  Effects return a cleanup function. When executed, it will unsubscribe from signals and run cleanup functions returned by effect callbacks, for example to remove event listeners.
459
384
 
460
385
  ```js
461
- import { createState, createComputed, createEffect } from '@zeix/cause-effect'
386
+ import { State, createEffect } from '@zeix/cause-effect'
462
387
 
463
- const user = createState({ name: 'Alice', age: 30 })
388
+ const user = new State({ name: 'Alice', age: 30 })
464
389
  const greeting = () => `Hello ${user.get().name}!`
465
390
  const cleanup = createEffect(() => {
466
391
  console.log(`${greeting()} You are ${user.get().age} years old`)
@@ -473,31 +398,25 @@ cleanup() // Logs: 'Cleanup' and unsubscribes from signal `user`
473
398
  user.set({ name: 'Bob', age: 28 }) // Won't trigger the effect anymore
474
399
  ```
475
400
 
476
- ## Helper Functions
401
+ ### resolve()
477
402
 
478
- ### `resolve()` - Extract Signal Values
479
-
480
- The `resolve()` function extracts values from multiple signals and returns a discriminated union result:
403
+ Extract signal values:
481
404
 
482
405
  ```js
483
- import { createState, createComputed, resolve } from '@zeix/cause-effect'
406
+ import { State, Memo, resolve } from '@zeix/cause-effect'
484
407
 
485
- const name = createState('Alice')
486
- const age = createComputed(() => 30)
408
+ const name = new State('Alice')
409
+ const age = new Memo(() => 30)
487
410
  const result = resolve({ name, age })
488
411
 
489
- if (result.ok) {
490
- console.log(result.values.name, result.values.age) // Type-safe access
491
- } else if (result.pending) {
492
- console.log('Loading...')
493
- } else {
494
- console.error('Errors:', result.errors)
495
- }
412
+ if (result.ok) console.log(result.values.name, result.values.age)
413
+ else if (result.pending) console.log('Loading...')
414
+ else console.error('Errors:', result.errors)
496
415
  ```
497
416
 
498
- ### `match()` - Pattern Matching for Side Effects
417
+ ### match()
499
418
 
500
- The `match()` function provides pattern matching on resolve results for side effects:
419
+ Pattern matching on resolved results for side effects:
501
420
 
502
421
  ```js
503
422
  import { resolve, match } from '@zeix/cause-effect'
@@ -509,9 +428,9 @@ match(resolve({ name, age }), {
509
428
  })
510
429
  ```
511
430
 
512
- ### `diff()` - Compare Object Changes
431
+ ### diff()
513
432
 
514
- The `diff()` function compares two objects and returns detailed information about what changed:
433
+ Compare object changes:
515
434
 
516
435
  ```js
517
436
  import { diff } from '@zeix/cause-effect'
@@ -526,11 +445,9 @@ console.log(changes.change) // { age: 31 }
526
445
  console.log(changes.remove) // { city: UNSET }
527
446
  ```
528
447
 
529
- This function is used internally by stores to efficiently determine what changed and emit appropriate events.
530
-
531
- ### `isEqual()` - Deep Equality Comparison
448
+ ### isEqual()
532
449
 
533
- The `isEqual()` function performs deep equality comparison with circular reference detection:
450
+ Deep equality comparison with circular reference detection:
534
451
 
535
452
  ```js
536
453
  import { isEqual } from '@zeix/cause-effect'
@@ -548,12 +465,10 @@ console.log(isEqual('hello', 'hello')) // true
548
465
  console.log(isEqual({ a: [1, 2] }, { a: [1, 2] })) // true
549
466
  ```
550
467
 
551
- Both `diff()` and `isEqual()` include built-in protection against circular references and will throw a `CircularDependencyError` if cycles are detected.
552
-
553
468
  ## Contributing & License
554
469
 
555
470
  Feel free to contribute, report issues, or suggest improvements.
556
471
 
557
472
  License: [MIT](LICENSE)
558
473
 
559
- (c) 2025 [Zeix AG](https://zeix.com)
474
+ (c) 2024 - 2026 [Zeix AG](https://zeix.com)