@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 CHANGED
@@ -1,21 +1,27 @@
1
1
  # Cause & Effect
2
2
 
3
- Version 0.13.2
3
+ Version 0.14.1
4
4
 
5
- **Cause & Effect** is a lightweight, reactive state management library for JavaScript applications. It uses the concept of signals to create a predictable and efficient data flow in your app.
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 parts of your app that depend on it, ensuring your UI stays in sync with your data.
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**: Chain signals with `.map()` and `.tap()`
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**: ~1kB gzipped, zero dependencies
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
- signals: [user, greeting],
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
- ### Single State Signal
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
- The `.tap()` method on either `State` or `Computed` is a shorthand for creating an effect on the signal.
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
- count.tap(v => {
66
- console.log(v) // logs '42'
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
- count.update(v => ++v)
71
+ count.update(v => ++v)
71
72
  })
72
73
  // Click on button logs '25', '26', and so on
73
74
  ```
74
75
 
75
- ### Sync Computed Signal
76
+ ### Computed Signals vs. Functions
76
77
 
77
- `computed()` creates a new computed signal. Computed signals are read-only and you can access the current resulting value using the `.get()` method.
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 isOdd = computed(() => count.get() % 2)
84
- effect(() => console.log(isOdd.get())) // logs 'false'
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
- count.update(v => ++v)
90
+ count.update(v => ++v)
88
91
  })
89
- // Click on button logs 'true', 'false', and so on
92
+ // Click on button logs 'false', 'true', and so on
90
93
  ```
91
94
 
92
- If you want to derive a computed signal from a single other signal you can use the `.map()` method on either `State` or `Computed`. This does the same as the snippet above:
95
+ #### When to Use Functions
93
96
 
94
- ```js
95
- import { state } from '@zeix/cause-effect'
97
+ **Performance tip**: For simple derivations, plain functions often outperform computed signals:
96
98
 
97
- const count = state(42)
98
- count.map(v => v % 2).tap(v => console.log(v)) // logs 'false'
99
- count.set(24) // logs nothing because 24 is also an even number
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
- ### Async Computed Signal
104
+ **When to use which approach:**
107
105
 
108
- Async computed signals are as straight forward as their sync counterparts. Just create the computed signal with an async function.
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
- **Caution**: Async computed signals will return a Symbol `UNSET` until the Promise is resolved.
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 entryId = state(42)
116
- const entryData = entryId.map(async id => {
117
- const response = await fetch(`/api/entry/${id}`)
118
- if (!response.ok) return new Error(`Failed to fetch data: ${response.statusText}`)
119
- return response.json()
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
- // Updates h1 and p of the entry as soon as fetched data for entry becomes available
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
- entryId.update(v => ++v)
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
- ## Error Handling
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 three paths for robust error handling:
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**: Value is available
133
- 2. **Nil**: Loading/Unset state (especially for async)
134
- 3. **Err**: Error occurred
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
- Handle all cases declaratively:
157
+ This allows for declarative handling of all possible states:
137
158
 
138
159
  ```js
139
160
  effect({
140
- signals: [data],
141
- ok: (value) => /* update UI */,
142
- nil: () => /* show loading */,
143
- err: (error) => /* show error */
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` keys (both optional) and Cause & Effect will take care of anything that might go wrong with the listed signals in the rest parameters of `effect()`.
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 detect when updates are applied or if they fail.
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
- You can also use the deduplication feature to ensure that only the latest update for a specific element and operation is applied:
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
- name.set(e.target.value) // Triggers an update on every keystroke
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
- name.tap(text => {
187
- const nameSpan = document.querySelector('.greeting .name')
188
- enqueue(() => {
189
- nameSpan.textContent = text
190
- return text
191
- }, [nameSpan, 'setName']) // For deduplication
192
- .then(result => console.log(`Name was updated to ${result}`))
193
- .catch(error => console.error('Failed to update name:', error))
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 in the input field only 'Jane' will be applied to the DOM. 'J', 'Ja', 'Jan' were superseded by more recent updates and deduplicated (if typing was fast enough).
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
- When multiple `enqueue` calls are made with the same deduplication key before the next animation frame, only the last call will be executed. Previous calls are superseded and their Promises will not be resolved or rejected. This "last-write-wins" behavior ensures that only the most recent update is applied, which is typically desirable for UI updates and state changes.
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
- Effects run synchronously as soon as source signals update. If you need to set multiple signals you can batch them together to ensure dependent effects are executed simultanously and only once.
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
- // Computed: derive a calculation ...
214
- const sum = computed({
215
- signals,
216
- ok: (...values) => values.reduce((total, v) => total + v, 0),
217
- }).map(v => { // ... perform validation and handle errors
218
- if (!Number.isFinite(v)) throw new Error('Invalid value')
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: switch cases for the result
223
- sum.tap({
224
- ok: v => console.log('Sum:', v),
225
- err: error => console.error('Error:', error)
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
- batch(() => {
231
- signals.forEach(signal => signal.update(v => v * 2))
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
- This example showcases several powerful features of Cause & Effect:
296
+ The Cause & Effect library is designed around these principles:
242
297
 
243
- 1. **Composability and Declarative Computations**: Easily compose multiple signals into a single computed value, declaring how values should be calculated based on other signals.
244
- 2. **Automatic Dependency Tracking and Efficient Updates**: The library tracks dependencies between signals and computed values, ensuring efficient propagation of changes.
245
- 3. **Robust Error Handling**: Built-in error handling at computation level and reactive error management allow for graceful handling of unexpected situations.
246
- 4. **Performance Optimization through Batching**: Group multiple state changes to ensure dependent computations and effects run only once after all changes are applied.
247
- 5. **Flexibility and Integration**: Seamlessly integrates with DOM manipulation and event listeners, fitting into any JavaScript application or framework.
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
- These principles enable developers to create complex, reactive applications with clear data flow, efficient updates, and robust error handling, while promoting code reuse and modularity.
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 = computed(() => `Hello ${user.get().name}!`)
260
- const cleanup = effect({
261
- signals: [user, greeting],
262
- ok: ({ age }, greet) => {
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 signals `user` and `greeting`
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
- Licence: [MIT](LICENCE.md)
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.13.2
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 ComputedCallback, UNSET, isSignal, isComputedCallback, toSignal, } from './src/signal';
8
- export { type State, state, isState } from './src/state';
9
- export { type Computed, type ComputedMatcher, computed, isComputed, } from './src/computed';
10
- export { type EffectMatcher, type TapMatcher, effect } from './src/effect';
11
- export { type EnqueueDedupe, batch, watch, enqueue } from './src/scheduler';
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 X=(x)=>typeof x==="function",d=(x)=>X(x)&&x.constructor.name==="AsyncFunction",P=(x,y)=>Object.prototype.toString.call(x)===`[object ${y}]`,c=(x)=>x instanceof Error,I=(x)=>x instanceof DOMException&&x.name==="AbortError",m=(x)=>x instanceof Promise,C=(x)=>c(x)?x:Error(String(x));class A extends Error{constructor(x){super(`Circular dependency in ${x} detected`);return this}}var j,R=new Set,U=0,_=new Map,D,v=()=>{D=void 0;let x=Array.from(_.values());_.clear();for(let y of x)y()},t=()=>{if(D)cancelAnimationFrame(D);D=requestAnimationFrame(v)};queueMicrotask(v);var N=(x)=>{if(j&&!x.has(j)){let y=j;x.add(y),j.cleanups.add(()=>{x.delete(y)})}},V=(x)=>{for(let y of x)if(U)R.add(y);else y()},k=()=>{while(R.size){let x=Array.from(R);R.clear();for(let y of x)y()}},a=(x)=>{U++;try{x()}finally{k(),U--}},M=(x,y)=>{let z=j;j=y;try{x()}finally{j=z}},r=(x,y)=>new Promise((z,J)=>{_.set(y,()=>{try{z(x())}catch(B){J(B)}}),t()});function Y(x){let{signals:y,ok:z,err:J=console.error,nil:B=()=>{}}=X(x)?{signals:[],ok:x}:x,L=!1,K=()=>M(()=>{if(L)throw new A("effect");L=!0;let H=void 0;try{H=S({signals:y,ok:z,err:J,nil:B})}catch(Q){J(C(Q))}if(X(H))K.cleanups.add(H);L=!1},K);return K.cleanups=new Set,K(),()=>{K.cleanups.forEach((H)=>H()),K.cleanups.clear()}}var o="Computed",e=(x,y)=>{if(!y)return!1;return x.name===y.name&&x.message===y.message},F=(x)=>{let y=new Set,z=X(x)?void 0:{nil:()=>Z,err:(...$)=>{if($.length>1)throw new AggregateError($);else throw $[0]},...x},J=z?z.ok:x,B=Z,L,K=!0,H=!1,Q=!1,G,W=($)=>{if(!Object.is($,B))B=$,K=!1,L=void 0,H=!0},g=()=>{H=Z!==B,B=Z,L=void 0},p=($)=>{let O=C($);H=!e(O,L),B=Z,L=O},n=($)=>{if(Q=!1,G=void 0,W($),H)V(y)},u=($)=>{if(Q=!1,G=void 0,p($),H)V(y)},l=()=>{Q=!1,G=void 0,f()},q=()=>{if(K=!0,G?.abort("Aborted because source signal changed"),y.size)V(y);else q.cleanups.forEach(($)=>$()),q.cleanups.clear()};q.cleanups=new Set;let f=()=>M(()=>{if(Q)throw new A("computed");if(H=!1,d(J)){if(G)return B;if(G=new AbortController,z)z.abort=z.abort instanceof AbortSignal?AbortSignal.any([z.abort,G.signal]):G.signal;G.signal.addEventListener("abort",l,{once:!0})}let $;Q=!0;try{$=z&&z.signals.length?S(z):J(G?.signal)}catch(O){if(I(O))g();else p(O);Q=!1;return}if(m($))$.then(n,u);else if($==null||Z===$)g();else W($);Q=!1},q),T={[Symbol.toStringTag]:o,get:()=>{if(N(y),k(),K)f();if(L)throw L;return B},map:($)=>F({signals:[T],ok:$}),tap:($)=>Y({signals:[T],...X($)?{ok:$}:$})};return T},b=(x)=>P(x,o);var i="State",E=(x)=>{let y=new Set,z=x,J={[Symbol.toStringTag]:i,get:()=>{return N(y),z},set:(B)=>{if(Object.is(z,B))return;if(z=B,V(y),Z===z)y.clear()},update:(B)=>{J.set(B(z))},map:(B)=>F({signals:[J],ok:B}),tap:(B)=>Y({signals:[J],...X(B)?{ok:B}:B})};return J},w=(x)=>P(x,i);var Z=Symbol(),h=(x)=>w(x)||b(x),s=(x)=>X(x)&&x.length<2,xx=(x)=>h(x)?x:s(x)?F(x):E(x),S=(x)=>{let{signals:y,abort:z,ok:J,err:B,nil:L}=x,K=[],H=!1,Q=y.map((G)=>{try{let W=G.get();if(W===Z)H=!0;return W}catch(W){if(I(W))throw W;K.push(C(W))}});try{return H?L(z):K.length?B(...K):J(...Q)}catch(G){if(I(G))throw G;let W=C(G);return B(W)}};export{M as watch,xx as toSignal,E as state,w as isState,h as isSignal,s as isComputedCallback,b as isComputed,r as enqueue,Y as effect,F as computed,a as batch,Z as UNSET,A as CircularDependencyError};
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.13.2
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 ComputedCallback,
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 ComputedMatcher,
19
+ type ComputedCallback,
20
+ TYPE_COMPUTED,
21
21
  computed,
22
22
  isComputed,
23
23
  } from './src/computed'
24
- export { type EffectMatcher, type TapMatcher, effect } from './src/effect'
25
- export { type EnqueueDedupe, batch, watch, enqueue } from './src/scheduler'
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeix/cause-effect",
3
- "version": "0.13.2",
3
+ "version": "0.14.1",
4
4
  "author": "Esther Brunner",
5
5
  "main": "index.js",
6
6
  "module": "index.ts",
package/src/computed.d.ts CHANGED
@@ -1,28 +1,19 @@
1
- import { type Signal, type ComputedCallback } from './signal';
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 {ComputedMatcher<S, T> | ComputedCallback<T>} matcher - computed matcher or callback
13
+ * @param {ComputedCallback<T>} fn - computation callback function
23
14
  * @returns {Computed<T>} - Computed signal
24
15
  */
25
- export declare const computed: <T extends {}, S extends Signal<{}>[] = []>(matcher: ComputedMatcher<S, T> | ComputedCallback<T>) => Computed<T>;
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
- export declare const isComputed: <T extends {}>(value: unknown) => value is Computed<T>;
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, };