@zeix/cause-effect 0.13.2 → 0.14.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 CHANGED
@@ -1,39 +1,45 @@
1
1
  # Cause & Effect
2
2
 
3
- Version 0.13.2
3
+ Version 0.14.0
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
14
+ - **Computed signals**: Derive values from other signals (either `memo()` for sync or `task()` for async)
15
+ - **Effects**: Run side effects when signals change
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
 
22
28
  ```js
23
- import { state, computed, effect } from '@zeix/cause-effect'
29
+ import { state, memo, effect } from '@zeix/cause-effect'
24
30
 
25
31
  // 1. Create state
26
32
  const user = state({ name: 'Alice', age: 30 })
27
33
 
28
34
  // 2. Create computed values
29
- const greeting = computed(() => `Hello ${user.get().name}!`)
35
+ const greeting = memo(() => `Hello ${user.get().name}!`)
30
36
 
31
37
  // 3. React to changes
32
38
  effect({
33
- signals: [user, greeting],
34
- ok: ({ age }, greet) => {
35
- console.log(`${greet} You are ${age} years old`)
36
- }
39
+ signals: [user, greeting],
40
+ ok: ({ age }, greet) => {
41
+ console.log(`${greet} You are ${age} years old`)
42
+ }
37
43
  })
38
44
 
39
45
  // 4. Update state
@@ -52,113 +58,97 @@ bun add @zeix/cause-effect
52
58
 
53
59
  ## Usage of Signals
54
60
 
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`.
61
+ ### State Signals
58
62
 
59
- The `.tap()` method on either `State` or `Computed` is a shorthand for creating an effect on the signal.
63
+ `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
64
 
61
65
  ```js
62
- import { state } from '@zeix/cause-effect'
66
+ import { state, effect } from '@zeix/cause-effect'
63
67
 
64
68
  const count = state(42)
65
- count.tap(v => {
66
- console.log(v) // logs '42'
69
+ effect(() => {
70
+ console.log(count.get()) // logs '42'
67
71
  })
68
72
  count.set(24) // logs '24'
69
73
  document.querySelector('.increment').addEventListener('click', () => {
70
- count.update(v => ++v)
74
+ count.update(v => ++v)
71
75
  })
72
76
  // Click on button logs '25', '26', and so on
73
77
  ```
74
78
 
75
- ### Sync Computed Signal
79
+ ### Computed Signals: memo() and task()
76
80
 
77
- `computed()` creates a new computed signal. Computed signals are read-only and you can access the current resulting value using the `.get()` method.
81
+ #### Synchronous Computations with memo()
82
+
83
+ `memo()` creates a read-only computed signal for synchronous calculations. It automatically tracks dependencies and updates when they change.
78
84
 
79
85
  ```js
80
- import { state, computed, effect } from '@zeix/cause-effect'
86
+ import { state, memo, effect } from '@zeix/cause-effect'
81
87
 
82
88
  const count = state(42)
83
- const isOdd = computed(() => count.get() % 2)
89
+ const isOdd = memo(() => count.get() % 2)
84
90
  effect(() => console.log(isOdd.get())) // logs 'false'
85
91
  count.set(24) // logs nothing because 24 is also an even number
86
92
  document.querySelector('button.increment').addEventListener('click', () => {
87
- count.update(v => ++v)
93
+ count.update(v => ++v)
88
94
  })
89
95
  // Click on button logs 'true', 'false', and so on
90
96
  ```
91
97
 
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:
98
+ #### Asynchronous Computations with task()
93
99
 
94
- ```js
95
- import { state } from '@zeix/cause-effect'
100
+ `task()` creates computed signals for asynchronous operations. It automatically manages promises, tracks dependencies, and handles cancellation through `AbortController`.
96
101
 
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
104
- ```
105
-
106
- ### Async Computed Signal
107
-
108
- Async computed signals are as straight forward as their sync counterparts. Just create the computed signal with an async function.
109
-
110
- **Caution**: Async computed signals will return a Symbol `UNSET` until the Promise is resolved.
102
+ **Note**: Task signals return `UNSET` while pending, which you can handle with the `nil` case in effects.
111
103
 
112
104
  ```js
113
- import { state } from '@zeix/cause-effect'
105
+ import { state, task, effect } from '@zeix/cause-effect'
114
106
 
115
107
  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()
108
+ const entryData = task(async abort => {
109
+ const response = await fetch(`/api/entry/${entryId.get()}`, { signal: abort })
110
+ if (!response.ok) throw new Error(`Failed to fetch data: ${response.statusText}`)
111
+ return response.json()
112
+ })
113
+
114
+ // Display data when available
115
+ effect({
116
+ signals: [entryData],
117
+ ok: data => console.log('Data loaded:', data),
118
+ nil: () => console.log('Loading...'),
119
+ err: error => console.error('Error:', error)
120
120
  })
121
- // Updates h1 and p of the entry as soon as fetched data for entry becomes available
121
+
122
+ // Move to next entry, automatically triggers a new fetch
122
123
  document.querySelector('button.next').addEventListener('click', () => {
123
- entryId.update(v => ++v)
124
+ entryId.update(v => ++v)
124
125
  })
125
- // Click on button updates h1 and p of the entry as soon as fetched data for the next entry is loaded
126
126
  ```
127
127
 
128
- ## Error Handling
128
+ ## Effects and Error Handling
129
129
 
130
- Cause & Effect provides three paths for robust error handling:
130
+ Cause & Effect provides a robust way to handle side effects and errors through the `effect()` function, with three distinct paths:
131
131
 
132
- 1. **Ok**: Value is available
133
- 2. **Nil**: Loading/Unset state (especially for async)
134
- 3. **Err**: Error occurred
132
+ 1. **Ok**: When values are available
133
+ 2. **Nil**: For loading/unset states (primarily with async tasks)
134
+ 3. **Err**: When errors occur during computation
135
135
 
136
- Handle all cases declaratively:
136
+ This allows for declarative handling of all possible states:
137
137
 
138
138
  ```js
139
139
  effect({
140
- signals: [data],
141
- ok: (value) => /* update UI */,
142
- nil: () => /* show loading */,
143
- err: (error) => /* show error */
140
+ signals: [data],
141
+ ok: (value) => /* update UI */,
142
+ nil: () => /* show loading */,
143
+ err: (error) => /* show error */
144
144
  })
145
145
  ```
146
146
 
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
- ```
147
+ 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.
158
148
 
159
149
  ## DOM Updates
160
150
 
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.
151
+ 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
152
 
163
153
  ```js
164
154
  import { enqueue } from '@zeix/cause-effect'
@@ -171,7 +161,11 @@ enqueue(() => {
171
161
  .catch(error => console.error('Update failed:', error))
172
162
  ```
173
163
 
174
- You can also use the deduplication feature to ensure that only the latest update for a specific element and operation is applied:
164
+ ### Deduplication with Symbols
165
+
166
+ 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.
167
+
168
+ Deduplication is controlled using JavaScript Symbols:
175
169
 
176
170
  ```js
177
171
  import { state, effect, enqueue } from '@zeix/cause-effect'
@@ -179,57 +173,95 @@ import { state, effect, enqueue } from '@zeix/cause-effect'
179
173
  // Define a signal and update it in an event handler
180
174
  const name = state('')
181
175
  document.querySelector('input[name="name"]').addEventListener('input', e => {
182
- name.set(e.target.value) // Triggers an update on every keystroke
176
+ name.set(e.target.value) // Triggers an update on every keystroke
183
177
  })
184
178
 
185
179
  // 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))
180
+ effect(text => {
181
+ // Create a Symbol for a specific update operation
182
+ const NAME_UPDATE = Symbol('name-update')
183
+ const text = name.get()
184
+ const nameSpan = document.querySelector('.greeting .name')
185
+ enqueue(() => {
186
+ nameSpan.textContent = text
187
+ return text
188
+ }, NAME_UPDATE) // Using the Symbol for deduplication
189
+ .then(result => console.log(`Name was updated to ${result}`))
190
+ .catch(error => console.error('Failed to update name:', error))
194
191
  })
195
192
  ```
196
193
 
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).
194
+ 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.
195
+
196
+ ### How Deduplication Works
197
+
198
+ When multiple `enqueue` calls use the same Symbol before the next animation frame:
199
+
200
+ 1. Only the last call will be executed
201
+ 2. Previous calls are superseded
202
+ 3. Only the Promise of the last call will be resolved
203
+
204
+ This "last-write-wins" behavior optimizes DOM updates and prevents unnecessary work when many updates happen rapidly.
205
+
206
+ ### Optional Deduplication
207
+
208
+ The deduplication Symbol is optional. When not provided, a unique Symbol is created automatically, ensuring the update is always executed:
209
+
210
+ ```js
211
+ // No deduplication - always executed
212
+ enqueue(() => document.title = 'New Page Title')
213
+
214
+ // Create symbols for different types of updates
215
+ const COLOR_UPDATE = Symbol('color-update')
216
+ const SIZE_UPDATE = Symbol('size-update')
217
+
218
+ // These won't interfere with each other (different symbols)
219
+ enqueue(() => element.style.color = 'red', COLOR_UPDATE)
220
+ enqueue(() => element.style.fontSize = '16px', SIZE_UPDATE)
221
+
222
+ // This will replace the previous color update (same symbol)
223
+ enqueue(() => element.style.color = 'blue', COLOR_UPDATE)
224
+ ```
225
+
226
+ Using Symbols for deduplication provides:
198
227
 
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.
228
+ - Clear semantic meaning for update operations
229
+ - Type safety in TypeScript
230
+ - Simple mechanism to control which updates should overwrite each other
231
+ - Flexibility to run every update when needed
200
232
 
201
233
  ## Advanced Usage
202
234
 
203
- ### Batching
235
+ ### Batching Updates
204
236
 
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.
237
+ Use `batch()` to group multiple signal updates, ensuring effects run only once after all changes are applied:
206
238
 
207
239
  ```js
208
- import { state, computed, batch } from '@zeix/cause-effect'
240
+ import { state, memo, effect, batch } from '@zeix/cause-effect'
209
241
 
210
242
  // State: define an array of State<number>
211
243
  const signals = [state(2), state(3), state(5)]
212
244
 
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
245
+ // Compute the sum of all signals
246
+ const sum = memo(() => {
247
+ const v = signals.reduce((total, signal) => total + signal.get(), 0)
248
+ // Validate the result
249
+ if (!Number.isFinite(v)) throw new Error('Invalid value')
250
+ return v
220
251
  })
221
252
 
222
- // Effect: switch cases for the result
223
- sum.tap({
224
- ok: v => console.log('Sum:', v),
225
- err: error => console.error('Error:', error)
253
+ // Effect: handle the result
254
+ effect({
255
+ signals: [sum],
256
+ ok: v => console.log('Sum:', v),
257
+ err: error => console.error('Error:', error)
226
258
  })
227
259
 
228
260
  // Batch: apply changes to all signals in a single transaction
229
261
  document.querySelector('.double-all').addEventListener('click', () => {
230
- batch(() => {
231
- signals.forEach(signal => signal.update(v => v * 2))
232
- })
262
+ batch(() => {
263
+ signals.forEach(signal => signal.update(v => v * 2))
264
+ })
233
265
  })
234
266
  // Click on button logs '20' only once
235
267
  // (instead of first '12', then '15' and then '20' without batch)
@@ -238,31 +270,28 @@ document.querySelector('.double-all').addEventListener('click', () => {
238
270
  signals[0].set(NaN)
239
271
  ```
240
272
 
241
- This example showcases several powerful features of Cause & Effect:
273
+ The Cause & Effect library is designed around these principles:
242
274
 
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.
275
+ - **Minimal API**: Core primitives with a small but powerful interface
276
+ - **Automatic Dependency Tracking**: Fine-grained reactivity with minimal boilerplate
277
+ - **Tree-Shakable**: Import only what you need for optimal bundle size
278
+ - **Flexible Integration**: Works with any JavaScript application or framework
248
279
 
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
280
+ ### Cleanup Functions
252
281
 
253
282
  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
283
 
255
284
  ```js
256
- import { state, computed, effect } from '@zeix/cause-effect'
285
+ import { state, memo, effect } from '@zeix/cause-effect'
257
286
 
258
287
  const user = state({ name: 'Alice', age: 30 })
259
- const greeting = computed(() => `Hello ${user.get().name}!`)
288
+ const greeting = memo(() => `Hello ${user.get().name}!`)
260
289
  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
- }
290
+ signals: [user, greeting],
291
+ ok: ({ age }, greet) => {
292
+ console.log(`${greet} You are ${age} years old`)
293
+ return () => console.log('Cleanup') // Cleanup function
294
+ }
266
295
  })
267
296
 
268
297
  // When you no longer need the effect, execute the cleanup function
@@ -271,34 +300,35 @@ cleanup() // Logs: 'Cleanup' and unsubscribes from signals `user` and `greeting`
271
300
  user.set({ name: 'Bob', age: 28 }) // Won't trigger the effect anymore
272
301
  ```
273
302
 
274
- ### Abort Controller
303
+ ### Automatic Abort Control
275
304
 
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()`:
305
+ For asynchronous operations, `task()` automatically manages cancellation when dependencies change, providing an `abort` signal parameter:
277
306
 
278
307
  ```js
279
- import { state, computed } from '@zeix/cause-effect'
308
+ import { state, task, effect } from '@zeix/cause-effect'
280
309
 
281
310
  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()
311
+ const url = memo(v => `https://example.com/api/entries/${id.get()}`)
312
+ const data = task(async abort => {
313
+ const response = await fetch(url.get(), { signal: abort })
314
+ if (!response.ok) throw new Error(`Failed to fetch data: ${response.statusText}`)
315
+ return response.json()
287
316
  })
288
- data.tap({
289
- ok: v => console.log('Value:', v),
290
- nil: () => console.warn('Not ready'),
291
- err: e => console.error('Error:', e)
317
+ effect({
318
+ signals: [data],
319
+ ok: v => console.log('Value:', v),
320
+ nil: () => console.warn('Not ready'),
321
+ err: e => console.error('Error:', e)
292
322
  })
293
323
 
294
324
  // User switches to another entry
295
- id.set(24) // Cancels or ignores the previous fetch request and starts a new one
325
+ id.set(24) // Cancels the previous fetch request and starts a new one
296
326
  ```
297
327
 
298
328
  ## Contributing & License
299
329
 
300
330
  Feel free to contribute, report issues, or suggest improvements.
301
331
 
302
- Licence: [MIT](LICENCE.md)
332
+ License: [MIT](LICENSE)
303
333
 
304
334
  (c) 2025 [Zeix AG](https://zeix.com)
package/index.d.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  /**
2
2
  * @name Cause & Effect
3
- * @version 0.13.2
3
+ * @version 0.14.0
4
4
  * @author Esther Brunner
5
5
  */
6
6
  export { CircularDependencyError } from './src/util';
7
- export { type Signal, type MaybeSignal, type ComputedCallback, UNSET, isSignal, isComputedCallback, toSignal, } from './src/signal';
7
+ export { type Signal, type MaybeSignal, UNSET, isSignal, isComputedCallback, toSignal, } from './src/signal';
8
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';
9
+ export { type Computed, type ComputedCallback, computed, isComputed, } from './src/computed';
10
+ export { type MemoCallback, memo } from './src/memo';
11
+ export { type TaskCallback, task } from './src/task';
12
+ export { type EffectMatcher, effect } from './src/effect';
13
+ export { batch, watch, 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 O=($)=>typeof $==="function",p=($)=>O($)&&$.constructor.name==="AsyncFunction",N=($,B)=>Object.prototype.toString.call($)===`[object ${B}]`,c=($)=>$ instanceof Error,q=($)=>$ instanceof DOMException&&$.name==="AbortError",d=($)=>$ instanceof Promise,M=($)=>c($)?$:Error(String($));class x extends Error{constructor($){super(`Circular dependency in ${$} detected`);return this}}var V,k=new Set,U=0,b=new Map,_,h=()=>{_=void 0;let $=Array.from(b.values());b.clear();for(let B of $)B()},n=()=>{if(_)cancelAnimationFrame(_);_=requestAnimationFrame(h)};queueMicrotask(h);var Y=($)=>{if(V&&!$.has(V)){let B=V;$.add(B),V.cleanups.add(()=>{$.delete(B)})}},A=($)=>{for(let B of $)if(U)k.add(B);else B()},C=()=>{while(k.size){let $=Array.from(k);k.clear();for(let B of $)B()}},l=($)=>{U++;try{$()}finally{C(),U--}},R=($,B)=>{let z=V;V=B;try{$()}finally{V=z}},u=($,B)=>new Promise((z,F)=>{b.set(B||Symbol(),()=>{try{z($())}catch(L){F(L)}}),n()});var v="State",S=($)=>{let B=new Set,z=$,F={[Symbol.toStringTag]:v,get:()=>{return Y(B),z},set:(L)=>{if(Object.is(z,L))return;if(z=L,A(B),K===z)B.clear()},update:(L)=>{F.set(L(z))}};return F},T=($)=>N($,v);var w=($)=>{let B=new Set,z=K,F,L=!0,X=!1,H=()=>{if(L=!0,B.size)A(B);else H.cleanups.forEach((W)=>W()),H.cleanups.clear()};H.cleanups=new Set;let Q=()=>R(()=>{if(X)throw new x("memo");X=!0;try{let W=$();if(W==null||K===W)z=K,F=void 0;else z=W,L=!1,F=void 0}catch(W){z=K,F=W instanceof Error?W:new Error(String(W))}finally{X=!1}},H);return{[Symbol.toStringTag]:y,get:()=>{if(Y(B),C(),L)Q();if(F)throw F;return z}}};var g=($)=>{let B=new Set,z=K,F,L=!0,X=!1,H=!1,Q,j=(J)=>{if(!Object.is(J,z))z=J,L=!1,F=void 0,X=!0},W=()=>{X=K!==z,z=K,F=void 0},D=(J)=>{let I=M(J);X=!(F&&I.name===F.name&&I.message===F.message),z=K,F=I},G=(J)=>{if(H=!1,Q=void 0,j(J),X)A(B)},Z=(J)=>{if(H=!1,Q=void 0,D(J),X)A(B)},i=()=>{H=!1,Q=void 0,m()},P=()=>{if(L=!0,Q?.abort("Aborted because source signal changed"),B.size)A(B);else P.cleanups.forEach((J)=>J()),P.cleanups.clear()};P.cleanups=new Set;let m=()=>R(()=>{if(H)throw new x("task");X=!1,Q=new AbortController,Q.signal.addEventListener("abort",i,{once:!0});let J;H=!0;try{J=$(Q.signal)}catch(I){if(q(I))W();else D(I);H=!1;return}if(d(J))J.then(G,Z);else if(J==null||K===J)W();else j(J);H=!1},P);return{[Symbol.toStringTag]:y,get:()=>{if(Y(B),C(),L)m();if(F)throw F;return z}}};var y="Computed",E=($)=>p($)?g($):w($),f=($)=>N($,y);var K=Symbol(),o=($)=>T($)||f($),s=($)=>O($)&&$.length<2,t=($)=>o($)?$:s($)?E($):S($);function r($){let{signals:B,ok:z,err:F=console.error,nil:L=()=>{}}=O($)?{signals:[],ok:$}:$,X=!1,H=()=>R(()=>{if(X)throw new x("effect");X=!0;let Q=void 0;try{let j=[],W=!1,D=B.map((G)=>{try{let Z=G.get();if(Z===K)W=!0;return Z}catch(Z){if(q(Z))throw Z;return j.push(M(Z)),K}});try{Q=W?L():j.length?F(...j):z(...D)}catch(G){if(q(G))throw G;let Z=M(G);Q=F(Z)}}catch(j){F(M(j))}if(O(Q))H.cleanups.add(Q);X=!1},H);return H.cleanups=new Set,H(),()=>{H.cleanups.forEach((Q)=>Q()),H.cleanups.clear()}}export{R as watch,t as toSignal,g as task,S as state,w as memo,T as isState,o as isSignal,s as isComputedCallback,f as isComputed,u as enqueue,r as effect,E as computed,l as batch,K as UNSET,x as CircularDependencyError};
package/index.ts CHANGED
@@ -1,25 +1,25 @@
1
1
  /**
2
2
  * @name Cause & Effect
3
- * @version 0.13.2
3
+ * @version 0.14.0
4
4
  * @author Esther Brunner
5
5
  */
6
6
  export { CircularDependencyError } from './src/util'
7
7
  export {
8
8
  type Signal,
9
9
  type MaybeSignal,
10
- type ComputedCallback,
11
10
  UNSET,
12
11
  isSignal,
13
12
  isComputedCallback,
14
13
  toSignal,
15
14
  } from './src/signal'
16
-
17
15
  export { type State, state, isState } from './src/state'
18
16
  export {
19
17
  type Computed,
20
- type ComputedMatcher,
18
+ type ComputedCallback,
21
19
  computed,
22
20
  isComputed,
23
21
  } from './src/computed'
24
- export { type EffectMatcher, type TapMatcher, effect } from './src/effect'
25
- export { type EnqueueDedupe, batch, watch, enqueue } from './src/scheduler'
22
+ export { type MemoCallback, memo } from './src/memo'
23
+ export { type TaskCallback, task } from './src/task'
24
+ export { type EffectMatcher, effect } from './src/effect'
25
+ export { batch, watch, enqueue } 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.0",
4
4
  "author": "Esther Brunner",
5
5
  "main": "index.js",
6
6
  "module": "index.ts",
package/src/computed.d.ts CHANGED
@@ -1,28 +1,25 @@
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
+ import { type MemoCallback } from './memo';
2
+ import { type TaskCallback } from './task';
3
+ type Computed<T extends {}> = {
13
4
  [Symbol.toStringTag]: 'Computed';
14
5
  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
6
  };
7
+ type ComputedCallback<T extends {} & {
8
+ then?: void;
9
+ }> = TaskCallback<T> | MemoCallback<T>;
10
+ declare const TYPE_COMPUTED = "Computed";
18
11
  /**
19
12
  * Create a derived signal from existing signals
20
13
  *
14
+ * This function delegates to either memo() for synchronous computations
15
+ * or task() for asynchronous computations, providing better performance
16
+ * for each case.
17
+ *
21
18
  * @since 0.9.0
22
- * @param {ComputedMatcher<S, T> | ComputedCallback<T>} matcher - computed matcher or callback
19
+ * @param {ComputedCallback<T>} fn - computation callback function
23
20
  * @returns {Computed<T>} - Computed signal
24
21
  */
25
- export declare const computed: <T extends {}, S extends Signal<{}>[] = []>(matcher: ComputedMatcher<S, T> | ComputedCallback<T>) => Computed<T>;
22
+ declare const computed: <T extends {}>(fn: ComputedCallback<T>) => Computed<T>;
26
23
  /**
27
24
  * Check if a value is a computed state
28
25
  *
@@ -30,4 +27,5 @@ export declare const computed: <T extends {}, S extends Signal<{}>[] = []>(match
30
27
  * @param {unknown} value - value to check
31
28
  * @returns {boolean} - true if value is a computed state, false otherwise
32
29
  */
33
- export declare const isComputed: <T extends {}>(value: unknown) => value is Computed<T>;
30
+ declare const isComputed: <T extends {}>(value: unknown) => value is Computed<T>;
31
+ export { type Computed, type ComputedCallback, TYPE_COMPUTED, computed, isComputed, };