@zeix/cause-effect 0.14.1 → 0.15.0
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 +256 -27
- package/biome.json +35 -0
- package/index.d.ts +32 -7
- package/index.dev.js +629 -0
- package/index.js +1 -1
- package/index.ts +41 -21
- package/package.json +6 -7
- package/src/computed.ts +30 -21
- package/src/diff.ts +136 -0
- package/src/effect.ts +59 -49
- package/src/match.ts +57 -0
- package/src/resolve.ts +58 -0
- package/src/scheduler.ts +3 -3
- package/src/signal.ts +48 -15
- package/src/state.ts +4 -3
- package/src/store.ts +325 -0
- package/src/util.ts +57 -5
- package/test/batch.test.ts +29 -25
- package/test/benchmark.test.ts +81 -45
- package/test/computed.test.ts +43 -39
- package/test/diff.test.ts +638 -0
- package/test/effect.test.ts +657 -49
- package/test/match.test.ts +378 -0
- package/test/resolve.test.ts +156 -0
- package/test/state.test.ts +33 -33
- package/test/store.test.ts +719 -0
- package/test/util/framework-types.ts +2 -2
- package/test/util/perf-tests.ts +2 -2
- package/test/util/reactive-framework.ts +1 -1
- package/tsconfig.json +9 -10
- package/types/index.d.ts +15 -0
- package/{src → types/src}/computed.d.ts +2 -2
- package/types/src/diff.d.ts +27 -0
- package/types/src/effect.d.ts +16 -0
- package/types/src/match.d.ts +21 -0
- package/types/src/resolve.d.ts +29 -0
- package/{src → types/src}/scheduler.d.ts +2 -2
- package/types/src/signal.d.ts +40 -0
- package/{src → types/src}/state.d.ts +1 -1
- package/types/src/store.d.ts +57 -0
- package/types/src/util.d.ts +15 -0
- package/types/test-new-effect.d.ts +1 -0
- package/src/effect.d.ts +0 -17
- package/src/signal.d.ts +0 -26
- package/src/util.d.ts +0 -7
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Cause & Effect
|
|
2
2
|
|
|
3
|
-
Version 0.
|
|
3
|
+
Version 0.15.0
|
|
4
4
|
|
|
5
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.
|
|
6
6
|
|
|
@@ -11,6 +11,7 @@ Version 0.14.1
|
|
|
11
11
|
### Core Concepts
|
|
12
12
|
|
|
13
13
|
- **State signals**: Hold values that can be directly modified: `state()`
|
|
14
|
+
- **Store signals**: Hold objects of nested reactive properties: `store()`
|
|
14
15
|
- **Computed signals**: Derive memoized values from other signals: `computed()`
|
|
15
16
|
- **Effects**: Run side effects when signals change: `effect()`
|
|
16
17
|
|
|
@@ -19,9 +20,10 @@ Version 0.14.1
|
|
|
19
20
|
- ⚡ **Reactive States**: Automatic updates when dependencies change
|
|
20
21
|
- 🧩 **Composable**: Create a complex signal graph with a minimal API
|
|
21
22
|
- ⏱️ **Async Ready**: Built-in `Promise` and `AbortController` support
|
|
22
|
-
- 🛡️ **Error Handling**:
|
|
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
|
|
23
25
|
- 🚀 **Performance**: Batching and efficient dependency tracking
|
|
24
|
-
- 📦 **Tiny**:
|
|
26
|
+
- 📦 **Tiny**: Less than 3kB gzipped, zero dependencies
|
|
25
27
|
|
|
26
28
|
## Quick Start
|
|
27
29
|
|
|
@@ -73,6 +75,114 @@ document.querySelector('.increment').addEventListener('click', () => {
|
|
|
73
75
|
// Click on button logs '25', '26', and so on
|
|
74
76
|
```
|
|
75
77
|
|
|
78
|
+
### Store Signals
|
|
79
|
+
|
|
80
|
+
`store()` 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.
|
|
81
|
+
|
|
82
|
+
```js
|
|
83
|
+
import { store, effect } from '@zeix/cause-effect'
|
|
84
|
+
|
|
85
|
+
const user = store({
|
|
86
|
+
name: 'Alice',
|
|
87
|
+
age: 30,
|
|
88
|
+
preferences: {
|
|
89
|
+
theme: 'dark',
|
|
90
|
+
notifications: true
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// Individual properties are reactive
|
|
95
|
+
effect(() => {
|
|
96
|
+
console.log(`${user.name.get()} is ${user.age.get()} years old`)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
// Nested properties work the same way
|
|
100
|
+
effect(() => {
|
|
101
|
+
console.log(`Theme: ${user.preferences.theme.get()}`)
|
|
102
|
+
})
|
|
103
|
+
|
|
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"
|
|
107
|
+
|
|
108
|
+
// Watch the entire store
|
|
109
|
+
effect(() => {
|
|
110
|
+
console.log('User data:', user.get()) // Triggers on any nested change
|
|
111
|
+
})
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### Dynamic Properties
|
|
115
|
+
|
|
116
|
+
Stores support dynamic property addition and removal at runtime using the `add()` and `remove()` methods:
|
|
117
|
+
|
|
118
|
+
```js
|
|
119
|
+
import { store, effect } from '@zeix/cause-effect'
|
|
120
|
+
|
|
121
|
+
const settings = store({ autoSave: true })
|
|
122
|
+
|
|
123
|
+
// Add new properties at runtime
|
|
124
|
+
settings.add('timeout', 5000)
|
|
125
|
+
console.log(settings.timeout.get()) // 5000
|
|
126
|
+
|
|
127
|
+
// Adding an existing property has no effect
|
|
128
|
+
settings.add('autoSave', false) // Ignored - autoSave remains true
|
|
129
|
+
|
|
130
|
+
// Remove properties
|
|
131
|
+
settings.remove('timeout')
|
|
132
|
+
console.log(settings.timeout) // undefined
|
|
133
|
+
|
|
134
|
+
// Removing non-existent properties has no effect
|
|
135
|
+
settings.remove('nonExistent') // Safe - no error thrown
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The `add()` and `remove()` methods are optimized for performance:
|
|
139
|
+
- They bypass the full reconciliation process used by `set()` and `update()`
|
|
140
|
+
- They're perfect for frequent single-property additions/removals
|
|
141
|
+
- They trigger the same events and reactivity as other store operations
|
|
142
|
+
|
|
143
|
+
#### Store Events
|
|
144
|
+
|
|
145
|
+
Stores emit events when properties are added, changed, or removed. You can listen to these events using standard `addEventListener()`:
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
import { store } from '@zeix/cause-effect'
|
|
149
|
+
|
|
150
|
+
const user = store({ name: 'Alice', age: 30 })
|
|
151
|
+
|
|
152
|
+
// Listen for property additions
|
|
153
|
+
user.addEventListener('store-add', (event) => {
|
|
154
|
+
console.log('Added properties:', event.detail)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
// Listen for property changes
|
|
158
|
+
user.addEventListener('store-change', (event) => {
|
|
159
|
+
console.log('Changed properties:', event.detail)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// Listen for property removals
|
|
163
|
+
user.addEventListener('store-remove', (event) => {
|
|
164
|
+
console.log('Removed properties:', event.detail)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
// These will trigger the respective events:
|
|
168
|
+
user.add('email', 'alice@example.com') // Logs: "Added properties: { email: 'alice@example.com' }"
|
|
169
|
+
user.age.set(31) // Logs: "Changed properties: { age: 31 }"
|
|
170
|
+
user.remove('email') // Logs: "Removed properties: { email: UNSET }"
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Events are also fired when using `set()` or `update()` methods on the entire store:
|
|
174
|
+
|
|
175
|
+
```js
|
|
176
|
+
// This will fire multiple events based on what changed
|
|
177
|
+
user.update(u => ({ ...u, name: 'Bob', city: 'New York' }))
|
|
178
|
+
// Logs: "Changed properties: { name: 'Bob' }"
|
|
179
|
+
// Logs: "Added properties: { city: 'New York' }"
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**When to use stores vs state:**
|
|
183
|
+
- **Use `store()`** for objects with reactive properties that you want to access individually
|
|
184
|
+
- **Use `state()`** for primitive values or objects you replace entirely
|
|
185
|
+
|
|
76
186
|
### Computed Signals vs. Functions
|
|
77
187
|
|
|
78
188
|
#### When to Use Computed Signals
|
|
@@ -120,7 +230,7 @@ const isEven = () => !(count.get() % 2)
|
|
|
120
230
|
4. Properly handles errors from failed requests
|
|
121
231
|
|
|
122
232
|
```js
|
|
123
|
-
import { state, computed, effect } from '@zeix/cause-effect'
|
|
233
|
+
import { state, computed, effect, resolve, match } from '@zeix/cause-effect'
|
|
124
234
|
|
|
125
235
|
const id = state(42)
|
|
126
236
|
const data = computed(async abort => {
|
|
@@ -130,12 +240,13 @@ const data = computed(async abort => {
|
|
|
130
240
|
return response.json()
|
|
131
241
|
})
|
|
132
242
|
|
|
133
|
-
// Handle all possible states
|
|
134
|
-
effect({
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
243
|
+
// Handle all possible states using resolve and match helpers
|
|
244
|
+
effect(() => {
|
|
245
|
+
match(resolve({ data }), {
|
|
246
|
+
ok: ({ data: json }) => console.log('Data loaded:', json),
|
|
247
|
+
nil: () => console.log('Loading...'),
|
|
248
|
+
err: errors => console.error('Error:', errors[0])
|
|
249
|
+
})
|
|
139
250
|
})
|
|
140
251
|
|
|
141
252
|
// When id changes, the previous request is automatically canceled
|
|
@@ -148,24 +259,64 @@ document.querySelector('button.next').addEventListener('click', () => {
|
|
|
148
259
|
|
|
149
260
|
## Effects and Error Handling
|
|
150
261
|
|
|
151
|
-
|
|
262
|
+
The `effect()` function supports both synchronous and asynchronous callbacks:
|
|
152
263
|
|
|
153
|
-
|
|
154
|
-
2. **Nil**: For loading/unset states (with async tasks)
|
|
155
|
-
3. **Err**: When errors occur during computation
|
|
264
|
+
### Synchronous Effects
|
|
156
265
|
|
|
157
|
-
|
|
266
|
+
```js
|
|
267
|
+
import { state, effect } from '@zeix/cause-effect'
|
|
268
|
+
|
|
269
|
+
const count = state(42)
|
|
270
|
+
effect(() => {
|
|
271
|
+
console.log('Count changed:', count.get())
|
|
272
|
+
})
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Asynchronous Effects with AbortSignal
|
|
276
|
+
|
|
277
|
+
Async effect callbacks receive an `AbortSignal` parameter that automatically cancels when the effect re-runs or is cleaned up:
|
|
278
|
+
|
|
279
|
+
```js
|
|
280
|
+
import { state, effect } from '@zeix/cause-effect'
|
|
281
|
+
|
|
282
|
+
const userId = state(1)
|
|
283
|
+
effect(async (abort) => {
|
|
284
|
+
try {
|
|
285
|
+
const response = await fetch(`/api/users/${userId.get()}`, { signal: abort })
|
|
286
|
+
const user = await response.json()
|
|
287
|
+
console.log('User loaded:', user)
|
|
288
|
+
} catch (error) {
|
|
289
|
+
if (!abort.aborted) {
|
|
290
|
+
console.error('Failed to load user:', error)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
})
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Error Handling with Helper Functions
|
|
297
|
+
|
|
298
|
+
For more sophisticated error handling, use the `resolve()` and `match()` helper functions:
|
|
158
299
|
|
|
159
300
|
```js
|
|
160
|
-
effect
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
301
|
+
import { state, computed, effect, resolve, match } from '@zeix/cause-effect'
|
|
302
|
+
|
|
303
|
+
const userId = state(1)
|
|
304
|
+
const userData = computed(async (abort) => {
|
|
305
|
+
const response = await fetch(`/api/users/${userId.get()}`, { signal: abort })
|
|
306
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
307
|
+
return response.json()
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
effect(() => {
|
|
311
|
+
match(resolve({ userData }), {
|
|
312
|
+
ok: ({ userData: user }) => console.log('User loaded:', user),
|
|
313
|
+
nil: () => console.log('Loading user...'),
|
|
314
|
+
err: errors => console.error('Error loading user:', errors[0])
|
|
315
|
+
})
|
|
165
316
|
})
|
|
166
317
|
```
|
|
167
318
|
|
|
168
|
-
|
|
319
|
+
The `resolve()` function extracts values from signals and returns a discriminated union result, while `match()` provides pattern matching for handling different states declaratively.
|
|
169
320
|
|
|
170
321
|
## DOM Updates
|
|
171
322
|
|
|
@@ -258,7 +409,7 @@ Using Symbols for deduplication provides:
|
|
|
258
409
|
Use `batch()` to group multiple signal updates, ensuring effects run only once after all changes are applied:
|
|
259
410
|
|
|
260
411
|
```js
|
|
261
|
-
import { state, computed, effect, batch } from '@zeix/cause-effect'
|
|
412
|
+
import { state, computed, effect, batch, resolve, match } from '@zeix/cause-effect'
|
|
262
413
|
|
|
263
414
|
// State: define an array of State<number>
|
|
264
415
|
const signals = [state(2), state(3), state(5)]
|
|
@@ -271,11 +422,12 @@ const sum = computed(() => {
|
|
|
271
422
|
return v
|
|
272
423
|
})
|
|
273
424
|
|
|
274
|
-
// Effect: handle the result
|
|
275
|
-
effect({
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
425
|
+
// Effect: handle the result with error handling
|
|
426
|
+
effect(() => {
|
|
427
|
+
match(resolve({ sum }), {
|
|
428
|
+
ok: ({ sum: v }) => console.log('Sum:', v),
|
|
429
|
+
err: errors => console.error('Error:', errors[0])
|
|
430
|
+
})
|
|
279
431
|
})
|
|
280
432
|
|
|
281
433
|
// Batch: apply changes to all signals in a single transaction
|
|
@@ -321,6 +473,83 @@ cleanup() // Logs: 'Cleanup' and unsubscribes from signal `user`
|
|
|
321
473
|
user.set({ name: 'Bob', age: 28 }) // Won't trigger the effect anymore
|
|
322
474
|
```
|
|
323
475
|
|
|
476
|
+
## Helper Functions
|
|
477
|
+
|
|
478
|
+
### `resolve()` - Extract Signal Values
|
|
479
|
+
|
|
480
|
+
The `resolve()` function extracts values from multiple signals and returns a discriminated union result:
|
|
481
|
+
|
|
482
|
+
```js
|
|
483
|
+
import { state, computed, resolve } from '@zeix/cause-effect'
|
|
484
|
+
|
|
485
|
+
const name = state('Alice')
|
|
486
|
+
const age = computed(() => 30)
|
|
487
|
+
const result = resolve({ name, age })
|
|
488
|
+
|
|
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
|
+
}
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
### `match()` - Pattern Matching for Side Effects
|
|
499
|
+
|
|
500
|
+
The `match()` function provides pattern matching on resolve results for side effects:
|
|
501
|
+
|
|
502
|
+
```js
|
|
503
|
+
import { resolve, match } from '@zeix/cause-effect'
|
|
504
|
+
|
|
505
|
+
match(resolve({ name, age }), {
|
|
506
|
+
ok: ({ name, age }) => document.title = `${name} (${age})`,
|
|
507
|
+
nil: () => document.title = 'Loading...',
|
|
508
|
+
err: errors => document.title = `Error: ${errors[0].message}`
|
|
509
|
+
})
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
### `diff()` - Compare Object Changes
|
|
513
|
+
|
|
514
|
+
The `diff()` function compares two objects and returns detailed information about what changed:
|
|
515
|
+
|
|
516
|
+
```js
|
|
517
|
+
import { diff } from '@zeix/cause-effect'
|
|
518
|
+
|
|
519
|
+
const oldUser = { name: 'Alice', age: 30, city: 'Boston' }
|
|
520
|
+
const newUser = { name: 'Alice', age: 31, email: 'alice@example.com' }
|
|
521
|
+
|
|
522
|
+
const changes = diff(oldUser, newUser)
|
|
523
|
+
console.log(changes.changed) // true - something changed
|
|
524
|
+
console.log(changes.add) // { email: 'alice@example.com' }
|
|
525
|
+
console.log(changes.change) // { age: 31 }
|
|
526
|
+
console.log(changes.remove) // { city: UNSET }
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
This function is used internally by stores to efficiently determine what changed and emit appropriate events.
|
|
530
|
+
|
|
531
|
+
### `isEqual()` - Deep Equality Comparison
|
|
532
|
+
|
|
533
|
+
The `isEqual()` function performs deep equality comparison with circular reference detection:
|
|
534
|
+
|
|
535
|
+
```js
|
|
536
|
+
import { isEqual } from '@zeix/cause-effect'
|
|
537
|
+
|
|
538
|
+
const obj1 = { name: 'Alice', preferences: { theme: 'dark' } }
|
|
539
|
+
const obj2 = { name: 'Alice', preferences: { theme: 'dark' } }
|
|
540
|
+
const obj3 = { name: 'Bob', preferences: { theme: 'dark' } }
|
|
541
|
+
|
|
542
|
+
console.log(isEqual(obj1, obj2)) // true - deep equality
|
|
543
|
+
console.log(isEqual(obj1, obj3)) // false - names differ
|
|
544
|
+
|
|
545
|
+
// Handles arrays, primitives, and complex nested structures
|
|
546
|
+
console.log(isEqual([1, 2, 3], [1, 2, 3])) // true
|
|
547
|
+
console.log(isEqual('hello', 'hello')) // true
|
|
548
|
+
console.log(isEqual({ a: [1, 2] }, { a: [1, 2] })) // true
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
Both `diff()` and `isEqual()` include built-in protection against circular references and will throw a `CircularDependencyError` if cycles are detected.
|
|
552
|
+
|
|
324
553
|
## Contributing & License
|
|
325
554
|
|
|
326
555
|
Feel free to contribute, report issues, or suggest improvements.
|
package/biome.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/2.1.4/schema.json",
|
|
3
|
+
"vcs": {
|
|
4
|
+
"enabled": false,
|
|
5
|
+
"clientKind": "git",
|
|
6
|
+
"useIgnoreFile": false
|
|
7
|
+
},
|
|
8
|
+
"files": {
|
|
9
|
+
"ignoreUnknown": false,
|
|
10
|
+
"includes": ["**", "!index.js", "!index.dev.js", "!**/*.d.ts"]
|
|
11
|
+
},
|
|
12
|
+
"formatter": {
|
|
13
|
+
"enabled": true,
|
|
14
|
+
"indentStyle": "tab"
|
|
15
|
+
},
|
|
16
|
+
"linter": {
|
|
17
|
+
"enabled": true,
|
|
18
|
+
"rules": {
|
|
19
|
+
"recommended": true
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"javascript": {
|
|
23
|
+
"formatter": {
|
|
24
|
+
"quoteStyle": "double"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"assist": {
|
|
28
|
+
"enabled": true,
|
|
29
|
+
"actions": {
|
|
30
|
+
"source": {
|
|
31
|
+
"organizeImports": "on"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
package/index.d.ts
CHANGED
|
@@ -1,11 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @name Cause & Effect
|
|
3
|
-
* @version 0.14.
|
|
3
|
+
* @version 0.14.2
|
|
4
4
|
* @author Esther Brunner
|
|
5
5
|
*/
|
|
6
|
-
export {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
export {
|
|
7
|
+
type Computed,
|
|
8
|
+
type ComputedCallback,
|
|
9
|
+
computed,
|
|
10
|
+
isComputed,
|
|
11
|
+
TYPE_COMPUTED,
|
|
12
|
+
} from './src/computed'
|
|
13
|
+
export { type EffectMatcher, type MaybeCleanup, effect } from './src/effect'
|
|
14
|
+
export {
|
|
15
|
+
batch,
|
|
16
|
+
type Cleanup,
|
|
17
|
+
enqueue,
|
|
18
|
+
flush,
|
|
19
|
+
notify,
|
|
20
|
+
observe,
|
|
21
|
+
subscribe,
|
|
22
|
+
type Updater,
|
|
23
|
+
type Watcher,
|
|
24
|
+
watch,
|
|
25
|
+
} from './src/scheduler'
|
|
26
|
+
export {
|
|
27
|
+
isComputedCallback,
|
|
28
|
+
isSignal,
|
|
29
|
+
type MaybeSignal,
|
|
30
|
+
type Signal,
|
|
31
|
+
type SignalValues,
|
|
32
|
+
toSignal,
|
|
33
|
+
UNSET,
|
|
34
|
+
} from './src/signal'
|
|
35
|
+
export { isState, type State, state, TYPE_STATE } from './src/state'
|
|
36
|
+
export { CircularDependencyError, isFunction } from './src/util'
|