@zeix/cause-effect 0.13.2 → 0.14.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/README.md +165 -139
- package/index.d.ts +7 -7
- package/index.js +1 -1
- package/index.ts +19 -8
- package/package.json +1 -1
- package/src/computed.d.ts +17 -17
- package/src/computed.ts +75 -125
- package/src/effect.d.ts +10 -15
- package/src/effect.ts +48 -39
- package/src/scheduler.d.ts +25 -12
- package/src/scheduler.ts +66 -26
- package/src/signal.d.ts +9 -28
- package/src/signal.ts +37 -76
- package/src/state.d.ts +5 -7
- package/src/state.ts +9 -39
- package/src/util.d.ts +1 -5
- package/src/util.ts +4 -22
- package/test/batch.test.ts +9 -13
- package/test/benchmark.test.ts +79 -67
- package/test/computed.test.ts +89 -106
- package/test/effect.test.ts +20 -12
- package/test/state.test.ts +2 -53
- package/test/util/dependency-graph.ts +2 -2
package/README.md
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
# Cause & Effect
|
|
2
2
|
|
|
3
|
-
Version 0.
|
|
3
|
+
Version 0.14.1
|
|
4
4
|
|
|
5
|
-
**Cause & Effect** is a lightweight, reactive state management library for JavaScript applications. It uses
|
|
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
|
|
|
7
7
|
## What is Cause & Effect?
|
|
8
8
|
|
|
9
|
-
**Cause & Effect** provides a simple way to manage application state using signals. Signals are containers for values that can change over time. When a signal's value changes, it automatically updates all
|
|
9
|
+
**Cause & Effect** provides a simple way to manage application state using signals. Signals are containers for values that can change over time. When a signal's value changes, it automatically updates all dependent computations and effects, ensuring your UI stays in sync with your data without manual intervention.
|
|
10
|
+
|
|
11
|
+
### Core Concepts
|
|
12
|
+
|
|
13
|
+
- **State signals**: Hold values that can be directly modified: `state()`
|
|
14
|
+
- **Computed signals**: Derive memoized values from other signals: `computed()`
|
|
15
|
+
- **Effects**: Run side effects when signals change: `effect()`
|
|
10
16
|
|
|
11
17
|
## Key Features
|
|
12
18
|
|
|
13
19
|
- ⚡ **Reactive States**: Automatic updates when dependencies change
|
|
14
|
-
- 🧩 **Composable**:
|
|
20
|
+
- 🧩 **Composable**: Create a complex signal graph with a minimal API
|
|
15
21
|
- ⏱️ **Async Ready**: Built-in `Promise` and `AbortController` support
|
|
16
22
|
- 🛡️ **Error Handling**: Declare handlers for errors and unset states in effects
|
|
17
23
|
- 🚀 **Performance**: Batching and efficient dependency tracking
|
|
18
|
-
- 📦 **Tiny**:
|
|
24
|
+
- 📦 **Tiny**: Around 1kB gzipped, zero dependencies
|
|
19
25
|
|
|
20
26
|
## Quick Start
|
|
21
27
|
|
|
@@ -29,11 +35,8 @@ const user = state({ name: 'Alice', age: 30 })
|
|
|
29
35
|
const greeting = computed(() => `Hello ${user.get().name}!`)
|
|
30
36
|
|
|
31
37
|
// 3. React to changes
|
|
32
|
-
effect({
|
|
33
|
-
|
|
34
|
-
ok: ({ age }, greet) => {
|
|
35
|
-
console.log(`${greet} You are ${age} years old`)
|
|
36
|
-
}
|
|
38
|
+
effect(() => {
|
|
39
|
+
console.log(`${greeting.get()} You are ${user.get().age} years old`)
|
|
37
40
|
})
|
|
38
41
|
|
|
39
42
|
// 4. Update state
|
|
@@ -52,113 +55,121 @@ bun add @zeix/cause-effect
|
|
|
52
55
|
|
|
53
56
|
## Usage of Signals
|
|
54
57
|
|
|
55
|
-
###
|
|
56
|
-
|
|
57
|
-
`state()` creates a new state signal. To access the current value of the signal use the `.get()` method. To update the value of the signal use the `.set()` method with a new value or `.update()` with an updater function of the form `(v: T) => T`.
|
|
58
|
+
### State Signals
|
|
58
59
|
|
|
59
|
-
|
|
60
|
+
`state()` 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.
|
|
60
61
|
|
|
61
62
|
```js
|
|
62
|
-
import { state } from '@zeix/cause-effect'
|
|
63
|
+
import { state, effect } from '@zeix/cause-effect'
|
|
63
64
|
|
|
64
65
|
const count = state(42)
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
effect(() => {
|
|
67
|
+
console.log(count.get()) // logs '42'
|
|
67
68
|
})
|
|
68
69
|
count.set(24) // logs '24'
|
|
69
70
|
document.querySelector('.increment').addEventListener('click', () => {
|
|
70
|
-
|
|
71
|
+
count.update(v => ++v)
|
|
71
72
|
})
|
|
72
73
|
// Click on button logs '25', '26', and so on
|
|
73
74
|
```
|
|
74
75
|
|
|
75
|
-
###
|
|
76
|
+
### Computed Signals vs. Functions
|
|
76
77
|
|
|
77
|
-
|
|
78
|
+
#### When to Use Computed Signals
|
|
79
|
+
|
|
80
|
+
`computed()` creates a memoized read-only signal that automatically tracks dependencies and updates only when those dependencies change.
|
|
78
81
|
|
|
79
82
|
```js
|
|
80
83
|
import { state, computed, effect } from '@zeix/cause-effect'
|
|
81
84
|
|
|
82
85
|
const count = state(42)
|
|
83
|
-
const
|
|
84
|
-
effect(() => console.log(
|
|
86
|
+
const isEven = computed(() => !(count.get() % 2))
|
|
87
|
+
effect(() => console.log(isEven.get())) // logs 'true'
|
|
85
88
|
count.set(24) // logs nothing because 24 is also an even number
|
|
86
89
|
document.querySelector('button.increment').addEventListener('click', () => {
|
|
87
|
-
|
|
90
|
+
count.update(v => ++v)
|
|
88
91
|
})
|
|
89
|
-
// Click on button logs '
|
|
92
|
+
// Click on button logs 'false', 'true', and so on
|
|
90
93
|
```
|
|
91
94
|
|
|
92
|
-
|
|
95
|
+
#### When to Use Functions
|
|
93
96
|
|
|
94
|
-
|
|
95
|
-
import { state } from '@zeix/cause-effect'
|
|
97
|
+
**Performance tip**: For simple derivations, plain functions often outperform computed signals:
|
|
96
98
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
document.querySelector('.increment').addEventListener('click', () => {
|
|
101
|
-
count.update(v => ++v)
|
|
102
|
-
})
|
|
103
|
-
// Click on button logs 'true', 'false', and so on
|
|
99
|
+
```js
|
|
100
|
+
// More performant for simple calculations
|
|
101
|
+
const isEven = () => !(count.get() % 2)
|
|
104
102
|
```
|
|
105
103
|
|
|
106
|
-
|
|
104
|
+
**When to use which approach:**
|
|
107
105
|
|
|
108
|
-
|
|
106
|
+
- **Use functions when**: The calculation is simple, inexpensive, or called infrequently
|
|
107
|
+
- **Use computed() when**:
|
|
108
|
+
- The calculation is expensive
|
|
109
|
+
- You need to share the result between multiple consumers
|
|
110
|
+
- You're working with asynchronous operations
|
|
111
|
+
- You need to track specific error states
|
|
109
112
|
|
|
110
|
-
|
|
113
|
+
#### Asynchronous Computations with Automatic Cancellation
|
|
114
|
+
|
|
115
|
+
`computed()` seamlessly handles asynchronous operations with built-in cancellation support. When used with an async function, it:
|
|
116
|
+
|
|
117
|
+
1. Provides an `abort` signal parameter you can pass to fetch or other cancelable APIs
|
|
118
|
+
2. Automatically cancels pending operations when dependencies change
|
|
119
|
+
3. Returns `UNSET` while the Promise is pending
|
|
120
|
+
4. Properly handles errors from failed requests
|
|
111
121
|
|
|
112
122
|
```js
|
|
113
|
-
import { state } from '@zeix/cause-effect'
|
|
123
|
+
import { state, computed, effect } from '@zeix/cause-effect'
|
|
114
124
|
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
125
|
+
const id = state(42)
|
|
126
|
+
const data = computed(async abort => {
|
|
127
|
+
// The abort signal is automatically managed by the computed signal
|
|
128
|
+
const response = await fetch(`/api/entries/${id.get()}`, { signal: abort })
|
|
129
|
+
if (!response.ok) throw new Error(`Failed to fetch data: ${response.statusText}`)
|
|
130
|
+
return response.json()
|
|
120
131
|
})
|
|
121
|
-
|
|
132
|
+
|
|
133
|
+
// Handle all possible states
|
|
134
|
+
effect({
|
|
135
|
+
signals: [data],
|
|
136
|
+
ok: json => console.log('Data loaded:', json),
|
|
137
|
+
nil: () => console.log('Loading...'),
|
|
138
|
+
err: error => console.error('Error:', error)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
// When id changes, the previous request is automatically canceled
|
|
122
142
|
document.querySelector('button.next').addEventListener('click', () => {
|
|
123
|
-
|
|
143
|
+
id.update(v => ++v)
|
|
124
144
|
})
|
|
125
|
-
// Click on button updates h1 and p of the entry as soon as fetched data for the next entry is loaded
|
|
126
145
|
```
|
|
127
146
|
|
|
128
|
-
|
|
147
|
+
**Note**: Always use `computed()` (not plain functions) for async operations to benefit from automatic cancellation, memoization, and state management.
|
|
148
|
+
|
|
149
|
+
## Effects and Error Handling
|
|
129
150
|
|
|
130
|
-
Cause & Effect provides
|
|
151
|
+
Cause & Effect provides a robust way to handle side effects and errors through the `effect()` function, with three distinct paths:
|
|
131
152
|
|
|
132
|
-
1. **Ok**:
|
|
133
|
-
2. **Nil**:
|
|
134
|
-
3. **Err**:
|
|
153
|
+
1. **Ok**: When values are available
|
|
154
|
+
2. **Nil**: For loading/unset states (with async tasks)
|
|
155
|
+
3. **Err**: When errors occur during computation
|
|
135
156
|
|
|
136
|
-
|
|
157
|
+
This allows for declarative handling of all possible states:
|
|
137
158
|
|
|
138
159
|
```js
|
|
139
160
|
effect({
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
161
|
+
signals: [data],
|
|
162
|
+
ok: (value) => /* update UI when data is available */,
|
|
163
|
+
nil: () => /* show loading state while pending */,
|
|
164
|
+
err: (error) => /* show error message when computation fails */
|
|
144
165
|
})
|
|
145
166
|
```
|
|
146
167
|
|
|
147
|
-
Instead of a single callback function, provide an object with `ok` (required), `err` and `nil`
|
|
148
|
-
|
|
149
|
-
If you want an effect based on a single signal, there's a shorthand too: The `.tap()` method on either `State` or `Computed`. You can use it for easy debugging, for example:
|
|
150
|
-
|
|
151
|
-
```js
|
|
152
|
-
signal.tap({
|
|
153
|
-
ok: v => console.log('Value:', v),
|
|
154
|
-
nil: () => console.warn('Not ready'),
|
|
155
|
-
err: e => console.error('Error:', e)
|
|
156
|
-
})
|
|
157
|
-
```
|
|
168
|
+
Instead of using a single callback function, you can provide an object with an `ok` handler (required), plus optional `err` and `nil` handlers. Cause & Effect will automatically route to the appropriate handler based on the state of the signals. If not provided, Cause & Effect will assume `console.error` for `err` and a no-op for `nil`.
|
|
158
169
|
|
|
159
170
|
## DOM Updates
|
|
160
171
|
|
|
161
|
-
The `enqueue()` function allows you to schedule DOM updates to be executed on the next animation frame. It returns a `Promise`, which makes it easy to
|
|
172
|
+
The `enqueue()` function allows you to schedule DOM updates to be executed on the next animation frame. It returns a `Promise`, which makes it easy to track when updates are applied or handle errors.
|
|
162
173
|
|
|
163
174
|
```js
|
|
164
175
|
import { enqueue } from '@zeix/cause-effect'
|
|
@@ -171,7 +182,11 @@ enqueue(() => {
|
|
|
171
182
|
.catch(error => console.error('Update failed:', error))
|
|
172
183
|
```
|
|
173
184
|
|
|
174
|
-
|
|
185
|
+
### Deduplication with Symbols
|
|
186
|
+
|
|
187
|
+
A powerful feature of `enqueue()` is deduplication, which ensures that only the most recent update for a specific operation is applied when multiple updates occur within a single animation frame. This is particularly useful for high-frequency events like typing, dragging, or scrolling.
|
|
188
|
+
|
|
189
|
+
Deduplication is controlled using JavaScript Symbols:
|
|
175
190
|
|
|
176
191
|
```js
|
|
177
192
|
import { state, effect, enqueue } from '@zeix/cause-effect'
|
|
@@ -179,57 +194,97 @@ import { state, effect, enqueue } from '@zeix/cause-effect'
|
|
|
179
194
|
// Define a signal and update it in an event handler
|
|
180
195
|
const name = state('')
|
|
181
196
|
document.querySelector('input[name="name"]').addEventListener('input', e => {
|
|
182
|
-
|
|
197
|
+
name.set(e.target.value) // Triggers an update on every keystroke
|
|
183
198
|
})
|
|
184
199
|
|
|
185
200
|
// Define an effect to react to signal changes
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
201
|
+
effect(text => {
|
|
202
|
+
// Create a Symbol for a specific update operation
|
|
203
|
+
const NAME_UPDATE = Symbol('name-update')
|
|
204
|
+
const text = name.get()
|
|
205
|
+
const nameSpan = document.querySelector('.greeting .name')
|
|
206
|
+
enqueue(() => {
|
|
207
|
+
nameSpan.textContent = text
|
|
208
|
+
return text
|
|
209
|
+
}, NAME_UPDATE) // Using the Symbol for deduplication
|
|
210
|
+
.then(result => console.log(`Name was updated to ${result}`))
|
|
211
|
+
.catch(error => console.error('Failed to update name:', error))
|
|
194
212
|
})
|
|
195
213
|
```
|
|
196
214
|
|
|
197
|
-
In this example, as the user types
|
|
215
|
+
In this example, as the user types "Jane" quickly, the intermediate values ('J', 'Ja', 'Jan') are deduplicated, and only the final value 'Jane' is applied to the DOM. Only the Promise for the final update is resolved.
|
|
198
216
|
|
|
199
|
-
|
|
217
|
+
### How Deduplication Works
|
|
218
|
+
|
|
219
|
+
When multiple `enqueue` calls use the same Symbol before the next animation frame:
|
|
220
|
+
|
|
221
|
+
1. Only the last call will be executed
|
|
222
|
+
2. Previous calls are superseded
|
|
223
|
+
3. Only the Promise of the last call will be resolved
|
|
224
|
+
|
|
225
|
+
This "last-write-wins" behavior optimizes DOM updates and prevents unnecessary work when many updates happen rapidly.
|
|
226
|
+
|
|
227
|
+
### Optional Deduplication
|
|
228
|
+
|
|
229
|
+
The deduplication Symbol is optional. When not provided, a unique Symbol is created automatically, ensuring the update is always executed:
|
|
230
|
+
|
|
231
|
+
```js
|
|
232
|
+
// No deduplication - always executed
|
|
233
|
+
enqueue(() => document.title = 'New Page Title')
|
|
234
|
+
|
|
235
|
+
// Create symbols for different types of updates
|
|
236
|
+
const COLOR_UPDATE = Symbol('color-update')
|
|
237
|
+
const SIZE_UPDATE = Symbol('size-update')
|
|
238
|
+
|
|
239
|
+
// These won't interfere with each other (different symbols)
|
|
240
|
+
enqueue(() => element.style.color = 'red', COLOR_UPDATE)
|
|
241
|
+
enqueue(() => element.style.fontSize = '16px', SIZE_UPDATE)
|
|
242
|
+
|
|
243
|
+
// This will replace the previous color update (same symbol)
|
|
244
|
+
enqueue(() => element.style.color = 'blue', COLOR_UPDATE)
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Using Symbols for deduplication provides:
|
|
248
|
+
|
|
249
|
+
- Clear semantic meaning for update operations
|
|
250
|
+
- Type safety in TypeScript
|
|
251
|
+
- Simple mechanism to control which updates should overwrite each other
|
|
252
|
+
- Flexibility to run every update when needed
|
|
200
253
|
|
|
201
254
|
## Advanced Usage
|
|
202
255
|
|
|
203
|
-
### Batching
|
|
256
|
+
### Batching Updates
|
|
204
257
|
|
|
205
|
-
|
|
258
|
+
Use `batch()` to group multiple signal updates, ensuring effects run only once after all changes are applied:
|
|
206
259
|
|
|
207
260
|
```js
|
|
208
|
-
import { state, computed, batch } from '@zeix/cause-effect'
|
|
261
|
+
import { state, computed, effect, batch } from '@zeix/cause-effect'
|
|
209
262
|
|
|
210
263
|
// State: define an array of State<number>
|
|
211
264
|
const signals = [state(2), state(3), state(5)]
|
|
212
265
|
|
|
213
|
-
//
|
|
214
|
-
const sum = computed({
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
return v
|
|
266
|
+
// Compute the sum of all signals
|
|
267
|
+
const sum = computed(() => {
|
|
268
|
+
const v = signals.reduce((total, signal) => total + signal.get(), 0)
|
|
269
|
+
// Validate the result
|
|
270
|
+
if (!Number.isFinite(v)) throw new Error('Invalid value')
|
|
271
|
+
return v
|
|
220
272
|
})
|
|
221
273
|
|
|
222
|
-
// Effect:
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
274
|
+
// Effect: handle the result
|
|
275
|
+
effect({
|
|
276
|
+
signals: [sum],
|
|
277
|
+
ok: v => console.log('Sum:', v),
|
|
278
|
+
err: error => console.error('Error:', error)
|
|
226
279
|
})
|
|
227
280
|
|
|
228
281
|
// Batch: apply changes to all signals in a single transaction
|
|
229
282
|
document.querySelector('.double-all').addEventListener('click', () => {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
283
|
+
batch(() => {
|
|
284
|
+
signals.forEach(signal => {
|
|
285
|
+
signal.update(v => v * 2)
|
|
286
|
+
})
|
|
287
|
+
})
|
|
233
288
|
})
|
|
234
289
|
// Click on button logs '20' only once
|
|
235
290
|
// (instead of first '12', then '15' and then '20' without batch)
|
|
@@ -238,17 +293,15 @@ document.querySelector('.double-all').addEventListener('click', () => {
|
|
|
238
293
|
signals[0].set(NaN)
|
|
239
294
|
```
|
|
240
295
|
|
|
241
|
-
|
|
296
|
+
The Cause & Effect library is designed around these principles:
|
|
242
297
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
298
|
+
- **Minimal API**: Core primitives with a small but powerful interface
|
|
299
|
+
- **Automatic Dependency Tracking**: Fine-grained reactivity with minimal boilerplate
|
|
300
|
+
- **Performance-Focused**: Choose the right tool (functions vs computed) for optimal speed
|
|
301
|
+
- **Tree-Shakable**: Import only what you need for optimal bundle size
|
|
302
|
+
- **Flexible Integration**: Works with any JavaScript application or framework
|
|
248
303
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
### Cleanup
|
|
304
|
+
### Cleanup Functions
|
|
252
305
|
|
|
253
306
|
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.
|
|
254
307
|
|
|
@@ -256,49 +309,22 @@ Effects return a cleanup function. When executed, it will unsubscribe from signa
|
|
|
256
309
|
import { state, computed, effect } from '@zeix/cause-effect'
|
|
257
310
|
|
|
258
311
|
const user = state({ name: 'Alice', age: 30 })
|
|
259
|
-
const greeting =
|
|
260
|
-
const cleanup = effect({
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
console.log(`${greet} You are ${age} years old`)
|
|
264
|
-
return () => console.log('Cleanup') // Cleanup function
|
|
265
|
-
}
|
|
312
|
+
const greeting = () => `Hello ${user.get().name}!`
|
|
313
|
+
const cleanup = effect(() => {
|
|
314
|
+
console.log(`${greeting()} You are ${user.get().age} years old`)
|
|
315
|
+
return () => console.log('Cleanup') // Cleanup function
|
|
266
316
|
})
|
|
267
317
|
|
|
268
318
|
// When you no longer need the effect, execute the cleanup function
|
|
269
|
-
cleanup() // Logs: 'Cleanup' and unsubscribes from
|
|
319
|
+
cleanup() // Logs: 'Cleanup' and unsubscribes from signal `user`
|
|
270
320
|
|
|
271
321
|
user.set({ name: 'Bob', age: 28 }) // Won't trigger the effect anymore
|
|
272
322
|
```
|
|
273
323
|
|
|
274
|
-
### Abort Controller
|
|
275
|
-
|
|
276
|
-
For asynchronous computed signals, Cause & Effect uses an `AbortController` to cancel pending promises when source signals update. You can use the `abort` parameter in `computed()` callbacks and pass it on to other AbortController aware APIs like `fetch()`:
|
|
277
|
-
|
|
278
|
-
```js
|
|
279
|
-
import { state, computed } from '@zeix/cause-effect'
|
|
280
|
-
|
|
281
|
-
const id = state(42)
|
|
282
|
-
const url = id.map(v => `https://example.com/api/entries/${v}`)
|
|
283
|
-
const data = computed(async abort => {
|
|
284
|
-
const response = await fetch(url.get(), { signal: abort })
|
|
285
|
-
if (!response.ok) throw new Error(`Failed to fetch data: ${response.statusText}`)
|
|
286
|
-
return response.json()
|
|
287
|
-
})
|
|
288
|
-
data.tap({
|
|
289
|
-
ok: v => console.log('Value:', v),
|
|
290
|
-
nil: () => console.warn('Not ready'),
|
|
291
|
-
err: e => console.error('Error:', e)
|
|
292
|
-
})
|
|
293
|
-
|
|
294
|
-
// User switches to another entry
|
|
295
|
-
id.set(24) // Cancels or ignores the previous fetch request and starts a new one
|
|
296
|
-
```
|
|
297
|
-
|
|
298
324
|
## Contributing & License
|
|
299
325
|
|
|
300
326
|
Feel free to contribute, report issues, or suggest improvements.
|
|
301
327
|
|
|
302
|
-
|
|
328
|
+
License: [MIT](LICENSE)
|
|
303
329
|
|
|
304
330
|
(c) 2025 [Zeix AG](https://zeix.com)
|
package/index.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @name Cause & Effect
|
|
3
|
-
* @version 0.
|
|
3
|
+
* @version 0.14.1
|
|
4
4
|
* @author Esther Brunner
|
|
5
5
|
*/
|
|
6
|
-
export { CircularDependencyError } from './src/util';
|
|
7
|
-
export { type Signal, type MaybeSignal, type
|
|
8
|
-
export { type State, state, isState } from './src/state';
|
|
9
|
-
export { type Computed, type
|
|
10
|
-
export { type EffectMatcher,
|
|
11
|
-
export { type
|
|
6
|
+
export { isFunction, CircularDependencyError } from './src/util';
|
|
7
|
+
export { type Signal, type MaybeSignal, type SignalValues, UNSET, isSignal, isComputedCallback, toSignal, } from './src/signal';
|
|
8
|
+
export { type State, TYPE_STATE, state, isState } from './src/state';
|
|
9
|
+
export { type Computed, type ComputedCallback, TYPE_COMPUTED, computed, isComputed, } from './src/computed';
|
|
10
|
+
export { type EffectMatcher, effect } from './src/effect';
|
|
11
|
+
export { type Watcher, type Cleanup, type Updater, watch, subscribe, notify, flush, batch, observe, enqueue, } from './src/scheduler';
|
package/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var j=($)=>typeof $==="function",M=($,B)=>Object.prototype.toString.call($)===`[object ${B}]`,Y=($)=>$ instanceof Error?$:Error(String($));class F extends Error{constructor($){super(`Circular dependency in ${$} detected`);return this}}var y,N=new Set,U=0,k=new Map,D,f=()=>{D=void 0;let $=Array.from(k.values());k.clear();for(let B of $)B()},d=()=>{if(D)cancelAnimationFrame(D);D=requestAnimationFrame(f)};queueMicrotask(f);var I=($)=>{let B=new Set,W=$;return W.off=(z)=>{B.add(z)},W.cleanup=()=>{for(let z of B)z();B.clear()},W},O=($)=>{if(y&&!$.has(y)){let B=y;$.add(B),y.off(()=>{$.delete(B)})}},R=($)=>{for(let B of $)if(U)N.add(B);else B()},P=()=>{while(N.size){let $=Array.from(N);N.clear();for(let B of $)B()}},h=($)=>{U++;try{$()}finally{P(),U--}},q=($,B)=>{let W=y;y=B;try{$()}finally{y=W}},v=($,B)=>new Promise((W,z)=>{k.set(B||Symbol(),()=>{try{W($())}catch(G){z(G)}}),d()});var _="State",S=($)=>{let B=new Set,W=$,z={[Symbol.toStringTag]:_,get:()=>{return O(B),W},set:(G)=>{if(Object.is(W,G))return;if(W=G,R(B),J===W)B.clear()},update:(G)=>{z.set(G(W))}};return z},T=($)=>M($,_);var b="Computed",E=($)=>{let B=new Set,W=J,z,G,X=!0,K=!1,L=!1,x=(H)=>{if(!Object.is(H,W))W=H,K=!0;z=void 0,X=!1},C=()=>{K=J!==W,W=J,z=void 0},Z=(H)=>{let Q=Y(H);K=!z||Q.name!==z.name||Q.message!==z.message,W=J,z=Q},A=(H)=>(Q)=>{if(L=!1,G=void 0,H(Q),K)R(B)},V=I(()=>{if(X=!0,G?.abort("Aborted because source signal changed"),B.size)R(B);else V.cleanup()}),w=()=>q(()=>{if(L)throw new F("computed");if(K=!1,j($)&&$.constructor.name==="AsyncFunction"){if(G)return W;G=new AbortController,G.signal.addEventListener("abort",()=>{L=!1,G=void 0,w()},{once:!0})}let H;L=!0;try{H=G?$(G.signal):$()}catch(Q){if(Q instanceof DOMException&&Q.name==="AbortError")C();else Z(Q);L=!1;return}if(H instanceof Promise)H.then(A(x),A(Z));else if(H==null||J===H)C();else x(H);L=!1},V);return{[Symbol.toStringTag]:b,get:()=>{if(O(B),P(),X)w();if(z)throw z;return W}}},g=($)=>M($,b),m=($)=>j($)&&$.length<2;var J=Symbol(),p=($)=>T($)||g($),o=($)=>p($)?$:m($)?E($):S($);function s($){let{signals:B,ok:W,err:z=console.error,nil:G=()=>{}}=j($)?{signals:[],ok:$}:$,X=!1,K=I(()=>q(()=>{if(X)throw new F("effect");X=!0;let L=[],x=!1,C=B.map((A)=>{try{let V=A.get();if(V===J)x=!0;return V}catch(V){return L.push(Y(V)),J}}),Z=void 0;try{Z=x?G():L.length?z(...L):W(...C)}catch(A){Z=z(Y(A))}finally{if(j(Z))K.off(Z)}X=!1},K));return K(),()=>K.cleanup()}export{I as watch,o as toSignal,O as subscribe,S as state,q as observe,R as notify,T as isState,p as isSignal,j as isFunction,m as isComputedCallback,g as isComputed,P as flush,v as enqueue,s as effect,E as computed,h as batch,J as UNSET,_ as TYPE_STATE,b as TYPE_COMPUTED,F as CircularDependencyError};
|
package/index.ts
CHANGED
|
@@ -1,25 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @name Cause & Effect
|
|
3
|
-
* @version 0.
|
|
3
|
+
* @version 0.14.1
|
|
4
4
|
* @author Esther Brunner
|
|
5
5
|
*/
|
|
6
|
-
export { CircularDependencyError } from './src/util'
|
|
6
|
+
export { isFunction, CircularDependencyError } from './src/util'
|
|
7
7
|
export {
|
|
8
8
|
type Signal,
|
|
9
9
|
type MaybeSignal,
|
|
10
|
-
type
|
|
10
|
+
type SignalValues,
|
|
11
11
|
UNSET,
|
|
12
12
|
isSignal,
|
|
13
13
|
isComputedCallback,
|
|
14
14
|
toSignal,
|
|
15
15
|
} from './src/signal'
|
|
16
|
-
|
|
17
|
-
export { type State, state, isState } from './src/state'
|
|
16
|
+
export { type State, TYPE_STATE, state, isState } from './src/state'
|
|
18
17
|
export {
|
|
19
18
|
type Computed,
|
|
20
|
-
type
|
|
19
|
+
type ComputedCallback,
|
|
20
|
+
TYPE_COMPUTED,
|
|
21
21
|
computed,
|
|
22
22
|
isComputed,
|
|
23
23
|
} from './src/computed'
|
|
24
|
-
export { type EffectMatcher,
|
|
25
|
-
export {
|
|
24
|
+
export { type EffectMatcher, effect } from './src/effect'
|
|
25
|
+
export {
|
|
26
|
+
type Watcher,
|
|
27
|
+
type Cleanup,
|
|
28
|
+
type Updater,
|
|
29
|
+
watch,
|
|
30
|
+
subscribe,
|
|
31
|
+
notify,
|
|
32
|
+
flush,
|
|
33
|
+
batch,
|
|
34
|
+
observe,
|
|
35
|
+
enqueue,
|
|
36
|
+
} from './src/scheduler'
|
package/package.json
CHANGED
package/src/computed.d.ts
CHANGED
|
@@ -1,28 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
import { type TapMatcher } from './effect';
|
|
3
|
-
export type ComputedMatcher<S extends Signal<{}>[], R extends {}> = {
|
|
4
|
-
signals: S;
|
|
5
|
-
abort?: AbortSignal;
|
|
6
|
-
ok: (...values: {
|
|
7
|
-
[K in keyof S]: S[K] extends Signal<infer T> ? T : never;
|
|
8
|
-
}) => R | Promise<R>;
|
|
9
|
-
err?: (...errors: Error[]) => R | Promise<R>;
|
|
10
|
-
nil?: () => R | Promise<R>;
|
|
11
|
-
};
|
|
12
|
-
export type Computed<T extends {}> = {
|
|
1
|
+
type Computed<T extends {}> = {
|
|
13
2
|
[Symbol.toStringTag]: 'Computed';
|
|
14
3
|
get(): T;
|
|
15
|
-
map<U extends {}>(fn: (v: T) => U | Promise<U>): Computed<U>;
|
|
16
|
-
tap(matcher: TapMatcher<T> | ((v: T) => void | (() => void))): () => void;
|
|
17
4
|
};
|
|
5
|
+
type ComputedCallback<T extends {} & {
|
|
6
|
+
then?: void;
|
|
7
|
+
}> = ((abort: AbortSignal) => Promise<T>) | (() => T);
|
|
8
|
+
declare const TYPE_COMPUTED = "Computed";
|
|
18
9
|
/**
|
|
19
10
|
* Create a derived signal from existing signals
|
|
20
11
|
*
|
|
21
12
|
* @since 0.9.0
|
|
22
|
-
* @param {
|
|
13
|
+
* @param {ComputedCallback<T>} fn - computation callback function
|
|
23
14
|
* @returns {Computed<T>} - Computed signal
|
|
24
15
|
*/
|
|
25
|
-
|
|
16
|
+
declare const computed: <T extends {}>(fn: ComputedCallback<T>) => Computed<T>;
|
|
26
17
|
/**
|
|
27
18
|
* Check if a value is a computed state
|
|
28
19
|
*
|
|
@@ -30,4 +21,13 @@ export declare const computed: <T extends {}, S extends Signal<{}>[] = []>(match
|
|
|
30
21
|
* @param {unknown} value - value to check
|
|
31
22
|
* @returns {boolean} - true if value is a computed state, false otherwise
|
|
32
23
|
*/
|
|
33
|
-
|
|
24
|
+
declare const isComputed: <T extends {}>(value: unknown) => value is Computed<T>;
|
|
25
|
+
/**
|
|
26
|
+
* Check if the provided value is a callback that may be used as input for toSignal() to derive a computed state
|
|
27
|
+
*
|
|
28
|
+
* @since 0.12.0
|
|
29
|
+
* @param {unknown} value - value to check
|
|
30
|
+
* @returns {boolean} - true if value is a callback or callbacks object, false otherwise
|
|
31
|
+
*/
|
|
32
|
+
declare const isComputedCallback: <T extends {}>(value: unknown) => value is ComputedCallback<T>;
|
|
33
|
+
export { type Computed, type ComputedCallback, TYPE_COMPUTED, computed, isComputed, isComputedCallback, };
|