@zeix/cause-effect 0.14.0 → 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,6 +1,6 @@
1
1
  # Cause & Effect
2
2
 
3
- Version 0.14.0
3
+ Version 0.14.1
4
4
 
5
5
  **Cause & Effect** is a lightweight, reactive state management library for JavaScript applications. It uses fine-grained reactivity with signals to create predictable and efficient data flow in your app.
6
6
 
@@ -10,9 +10,9 @@ Version 0.14.0
10
10
 
11
11
  ### Core Concepts
12
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
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()`
16
16
 
17
17
  ## Key Features
18
18
 
@@ -26,20 +26,17 @@ Version 0.14.0
26
26
  ## Quick Start
27
27
 
28
28
  ```js
29
- import { state, memo, effect } from '@zeix/cause-effect'
29
+ import { state, computed, effect } from '@zeix/cause-effect'
30
30
 
31
31
  // 1. Create state
32
32
  const user = state({ name: 'Alice', age: 30 })
33
33
 
34
34
  // 2. Create computed values
35
- const greeting = memo(() => `Hello ${user.get().name}!`)
35
+ const greeting = computed(() => `Hello ${user.get().name}!`)
36
36
 
37
37
  // 3. React to changes
38
- effect({
39
- signals: [user, greeting],
40
- ok: ({ age }, greet) => {
41
- console.log(`${greet} You are ${age} years old`)
42
- }
38
+ effect(() => {
39
+ console.log(`${greeting.get()} You are ${user.get().age} years old`)
43
40
  })
44
41
 
45
42
  // 4. Update state
@@ -76,61 +73,85 @@ document.querySelector('.increment').addEventListener('click', () => {
76
73
  // Click on button logs '25', '26', and so on
77
74
  ```
78
75
 
79
- ### Computed Signals: memo() and task()
76
+ ### Computed Signals vs. Functions
80
77
 
81
- #### Synchronous Computations with memo()
78
+ #### When to Use Computed Signals
82
79
 
83
- `memo()` creates a read-only computed signal for synchronous calculations. It automatically tracks dependencies and updates when they change.
80
+ `computed()` creates a memoized read-only signal that automatically tracks dependencies and updates only when those dependencies change.
84
81
 
85
82
  ```js
86
- import { state, memo, effect } from '@zeix/cause-effect'
83
+ import { state, computed, effect } from '@zeix/cause-effect'
87
84
 
88
85
  const count = state(42)
89
- const isOdd = memo(() => count.get() % 2)
90
- effect(() => console.log(isOdd.get())) // logs 'false'
86
+ const isEven = computed(() => !(count.get() % 2))
87
+ effect(() => console.log(isEven.get())) // logs 'true'
91
88
  count.set(24) // logs nothing because 24 is also an even number
92
89
  document.querySelector('button.increment').addEventListener('click', () => {
93
90
  count.update(v => ++v)
94
91
  })
95
- // Click on button logs 'true', 'false', and so on
92
+ // Click on button logs 'false', 'true', and so on
93
+ ```
94
+
95
+ #### When to Use Functions
96
+
97
+ **Performance tip**: For simple derivations, plain functions often outperform computed signals:
98
+
99
+ ```js
100
+ // More performant for simple calculations
101
+ const isEven = () => !(count.get() % 2)
96
102
  ```
97
103
 
98
- #### Asynchronous Computations with task()
104
+ **When to use which approach:**
99
105
 
100
- `task()` creates computed signals for asynchronous operations. It automatically manages promises, tracks dependencies, and handles cancellation through `AbortController`.
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
101
112
 
102
- **Note**: Task signals return `UNSET` while pending, which you can handle with the `nil` case in effects.
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
103
121
 
104
122
  ```js
105
- import { state, task, effect } from '@zeix/cause-effect'
123
+ import { state, computed, effect } from '@zeix/cause-effect'
106
124
 
107
- const entryId = state(42)
108
- const entryData = task(async abort => {
109
- const response = await fetch(`/api/entry/${entryId.get()}`, { signal: abort })
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 })
110
129
  if (!response.ok) throw new Error(`Failed to fetch data: ${response.statusText}`)
111
130
  return response.json()
112
131
  })
113
132
 
114
- // Display data when available
133
+ // Handle all possible states
115
134
  effect({
116
- signals: [entryData],
117
- ok: data => console.log('Data loaded:', data),
135
+ signals: [data],
136
+ ok: json => console.log('Data loaded:', json),
118
137
  nil: () => console.log('Loading...'),
119
138
  err: error => console.error('Error:', error)
120
139
  })
121
140
 
122
- // Move to next entry, automatically triggers a new fetch
141
+ // When id changes, the previous request is automatically canceled
123
142
  document.querySelector('button.next').addEventListener('click', () => {
124
- entryId.update(v => ++v)
143
+ id.update(v => ++v)
125
144
  })
126
145
  ```
127
146
 
147
+ **Note**: Always use `computed()` (not plain functions) for async operations to benefit from automatic cancellation, memoization, and state management.
148
+
128
149
  ## Effects and Error Handling
129
150
 
130
151
  Cause & Effect provides a robust way to handle side effects and errors through the `effect()` function, with three distinct paths:
131
152
 
132
153
  1. **Ok**: When values are available
133
- 2. **Nil**: For loading/unset states (primarily with async tasks)
154
+ 2. **Nil**: For loading/unset states (with async tasks)
134
155
  3. **Err**: When errors occur during computation
135
156
 
136
157
  This allows for declarative handling of all possible states:
@@ -138,13 +159,13 @@ This allows for declarative handling of all possible states:
138
159
  ```js
139
160
  effect({
140
161
  signals: [data],
141
- ok: (value) => /* update UI */,
142
- nil: () => /* show loading */,
143
- err: (error) => /* show error */
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 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.
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`.
148
169
 
149
170
  ## DOM Updates
150
171
 
@@ -237,13 +258,13 @@ Using Symbols for deduplication provides:
237
258
  Use `batch()` to group multiple signal updates, ensuring effects run only once after all changes are applied:
238
259
 
239
260
  ```js
240
- import { state, memo, effect, batch } from '@zeix/cause-effect'
261
+ import { state, computed, effect, batch } from '@zeix/cause-effect'
241
262
 
242
263
  // State: define an array of State<number>
243
264
  const signals = [state(2), state(3), state(5)]
244
265
 
245
266
  // Compute the sum of all signals
246
- const sum = memo(() => {
267
+ const sum = computed(() => {
247
268
  const v = signals.reduce((total, signal) => total + signal.get(), 0)
248
269
  // Validate the result
249
270
  if (!Number.isFinite(v)) throw new Error('Invalid value')
@@ -260,7 +281,9 @@ effect({
260
281
  // Batch: apply changes to all signals in a single transaction
261
282
  document.querySelector('.double-all').addEventListener('click', () => {
262
283
  batch(() => {
263
- signals.forEach(signal => signal.update(v => v * 2))
284
+ signals.forEach(signal => {
285
+ signal.update(v => v * 2)
286
+ })
264
287
  })
265
288
  })
266
289
  // Click on button logs '20' only once
@@ -274,6 +297,7 @@ The Cause & Effect library is designed around these principles:
274
297
 
275
298
  - **Minimal API**: Core primitives with a small but powerful interface
276
299
  - **Automatic Dependency Tracking**: Fine-grained reactivity with minimal boilerplate
300
+ - **Performance-Focused**: Choose the right tool (functions vs computed) for optimal speed
277
301
  - **Tree-Shakable**: Import only what you need for optimal bundle size
278
302
  - **Flexible Integration**: Works with any JavaScript application or framework
279
303
 
@@ -282,49 +306,21 @@ The Cause & Effect library is designed around these principles:
282
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.
283
307
 
284
308
  ```js
285
- import { state, memo, effect } from '@zeix/cause-effect'
309
+ import { state, computed, effect } from '@zeix/cause-effect'
286
310
 
287
311
  const user = state({ name: 'Alice', age: 30 })
288
- const greeting = memo(() => `Hello ${user.get().name}!`)
289
- const cleanup = effect({
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
- }
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
295
316
  })
296
317
 
297
318
  // When you no longer need the effect, execute the cleanup function
298
- cleanup() // Logs: 'Cleanup' and unsubscribes from signals `user` and `greeting`
319
+ cleanup() // Logs: 'Cleanup' and unsubscribes from signal `user`
299
320
 
300
321
  user.set({ name: 'Bob', age: 28 }) // Won't trigger the effect anymore
301
322
  ```
302
323
 
303
- ### Automatic Abort Control
304
-
305
- For asynchronous operations, `task()` automatically manages cancellation when dependencies change, providing an `abort` signal parameter:
306
-
307
- ```js
308
- import { state, task, effect } from '@zeix/cause-effect'
309
-
310
- const id = state(42)
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()
316
- })
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)
322
- })
323
-
324
- // User switches to another entry
325
- id.set(24) // Cancels the previous fetch request and starts a new one
326
- ```
327
-
328
324
  ## Contributing & License
329
325
 
330
326
  Feel free to contribute, report issues, or suggest improvements.
package/index.d.ts CHANGED
@@ -1,13 +1,11 @@
1
1
  /**
2
2
  * @name Cause & Effect
3
- * @version 0.14.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, UNSET, isSignal, isComputedCallback, toSignal, } from './src/signal';
8
- export { type State, state, isState } from './src/state';
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';
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';
12
10
  export { type EffectMatcher, effect } from './src/effect';
13
- export { batch, watch, enqueue } from './src/scheduler';
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 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};
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.14.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 SignalValues,
10
11
  UNSET,
11
12
  isSignal,
12
13
  isComputedCallback,
13
14
  toSignal,
14
15
  } from './src/signal'
15
- export { type State, state, isState } from './src/state'
16
+ export { type State, TYPE_STATE, state, isState } from './src/state'
16
17
  export {
17
18
  type Computed,
18
19
  type ComputedCallback,
20
+ TYPE_COMPUTED,
19
21
  computed,
20
22
  isComputed,
21
23
  } from './src/computed'
22
- export { type MemoCallback, memo } from './src/memo'
23
- export { type TaskCallback, task } from './src/task'
24
24
  export { type EffectMatcher, effect } from './src/effect'
25
- export { batch, watch, enqueue } from './src/scheduler'
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.14.0",
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,20 +1,14 @@
1
- import { type MemoCallback } from './memo';
2
- import { type TaskCallback } from './task';
3
1
  type Computed<T extends {}> = {
4
2
  [Symbol.toStringTag]: 'Computed';
5
3
  get(): T;
6
4
  };
7
5
  type ComputedCallback<T extends {} & {
8
6
  then?: void;
9
- }> = TaskCallback<T> | MemoCallback<T>;
7
+ }> = ((abort: AbortSignal) => Promise<T>) | (() => T);
10
8
  declare const TYPE_COMPUTED = "Computed";
11
9
  /**
12
10
  * Create a derived signal from existing signals
13
11
  *
14
- * This function delegates to either memo() for synchronous computations
15
- * or task() for asynchronous computations, providing better performance
16
- * for each case.
17
- *
18
12
  * @since 0.9.0
19
13
  * @param {ComputedCallback<T>} fn - computation callback function
20
14
  * @returns {Computed<T>} - Computed signal
@@ -28,4 +22,12 @@ declare const computed: <T extends {}>(fn: ComputedCallback<T>) => Computed<T>;
28
22
  * @returns {boolean} - true if value is a computed state, false otherwise
29
23
  */
30
24
  declare const isComputed: <T extends {}>(value: unknown) => value is Computed<T>;
31
- export { type Computed, type ComputedCallback, TYPE_COMPUTED, computed, isComputed, };
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, };
package/src/computed.ts CHANGED
@@ -1,6 +1,18 @@
1
- import { isAsyncFunction, isObjectOfType } from './util'
2
- import { type MemoCallback, memo } from './memo'
3
- import { type TaskCallback, task } from './task'
1
+ import {
2
+ CircularDependencyError,
3
+ isFunction,
4
+ isObjectOfType,
5
+ toError,
6
+ } from './util'
7
+ import {
8
+ type Watcher,
9
+ watch,
10
+ subscribe,
11
+ notify,
12
+ flush,
13
+ observe,
14
+ } from './scheduler'
15
+ import { UNSET } from './signal'
4
16
 
5
17
  /* === Types === */
6
18
 
@@ -9,8 +21,8 @@ type Computed<T extends {}> = {
9
21
  get(): T
10
22
  }
11
23
  type ComputedCallback<T extends {} & { then?: void }> =
12
- | TaskCallback<T>
13
- | MemoCallback<T>
24
+ | ((abort: AbortSignal) => Promise<T>)
25
+ | (() => T)
14
26
 
15
27
  /* === Constants === */
16
28
 
@@ -21,16 +33,116 @@ const TYPE_COMPUTED = 'Computed'
21
33
  /**
22
34
  * Create a derived signal from existing signals
23
35
  *
24
- * This function delegates to either memo() for synchronous computations
25
- * or task() for asynchronous computations, providing better performance
26
- * for each case.
27
- *
28
36
  * @since 0.9.0
29
37
  * @param {ComputedCallback<T>} fn - computation callback function
30
38
  * @returns {Computed<T>} - Computed signal
31
39
  */
32
- const computed = <T extends {}>(fn: ComputedCallback<T>): Computed<T> =>
33
- isAsyncFunction<T>(fn) ? task<T>(fn) : memo<T>(fn as MemoCallback<T>)
40
+ const computed = <T extends {}>(fn: ComputedCallback<T>): Computed<T> => {
41
+ const watchers: Set<Watcher> = new Set()
42
+
43
+ // Internal state
44
+ let value: T = UNSET
45
+ let error: Error | undefined
46
+ let controller: AbortController | undefined
47
+ let dirty = true
48
+ let changed = false
49
+ let computing = false
50
+
51
+ // Functions to update internal state
52
+ const ok = (v: T) => {
53
+ if (!Object.is(v, value)) {
54
+ value = v
55
+ changed = true
56
+ }
57
+ error = undefined
58
+ dirty = false
59
+ }
60
+ const nil = () => {
61
+ changed = UNSET !== value
62
+ value = UNSET
63
+ error = undefined
64
+ }
65
+ const err = (e: unknown) => {
66
+ const newError = toError(e)
67
+ changed =
68
+ !error ||
69
+ newError.name !== error.name ||
70
+ newError.message !== error.message
71
+ value = UNSET
72
+ error = newError
73
+ }
74
+ const settle =
75
+ <T>(settleFn: (arg: T) => void) =>
76
+ (arg: T) => {
77
+ computing = false
78
+ controller = undefined
79
+ settleFn(arg)
80
+ if (changed) notify(watchers)
81
+ }
82
+
83
+ // Own watcher: called when notified from sources (push)
84
+ const mark = watch(() => {
85
+ dirty = true
86
+ controller?.abort('Aborted because source signal changed')
87
+ if (watchers.size) notify(watchers)
88
+ else mark.cleanup()
89
+ })
90
+
91
+ // Called when requested by dependencies (pull)
92
+ const compute = () =>
93
+ observe(() => {
94
+ if (computing) throw new CircularDependencyError('computed')
95
+ changed = false
96
+ if (isFunction(fn) && fn.constructor.name === 'AsyncFunction') {
97
+ if (controller) return value // return current value until promise resolves
98
+ controller = new AbortController()
99
+ controller.signal.addEventListener(
100
+ 'abort',
101
+ () => {
102
+ computing = false
103
+ controller = undefined
104
+ compute() // retry
105
+ },
106
+ {
107
+ once: true,
108
+ },
109
+ )
110
+ }
111
+ let result: T | Promise<T>
112
+ computing = true
113
+ try {
114
+ result = controller ? fn(controller.signal) : (fn as () => T)()
115
+ } catch (e) {
116
+ if (e instanceof DOMException && e.name === 'AbortError') nil()
117
+ else err(e)
118
+ computing = false
119
+ return
120
+ }
121
+ if (result instanceof Promise) result.then(settle(ok), settle(err))
122
+ else if (null == result || UNSET === result) nil()
123
+ else ok(result)
124
+ computing = false
125
+ }, mark)
126
+
127
+ const c: Computed<T> = {
128
+ [Symbol.toStringTag]: TYPE_COMPUTED,
129
+
130
+ /**
131
+ * Get the current value of the computed
132
+ *
133
+ * @since 0.9.0
134
+ * @returns {T} - current value of the computed
135
+ */
136
+ get: (): T => {
137
+ subscribe(watchers)
138
+ flush()
139
+ if (dirty) compute()
140
+ if (error) throw error
141
+ return value
142
+ },
143
+ }
144
+ return c
145
+ }
34
146
 
35
147
  /**
36
148
  * Check if a value is a computed state
@@ -43,6 +155,17 @@ const isComputed = /*#__PURE__*/ <T extends {}>(
43
155
  value: unknown,
44
156
  ): value is Computed<T> => isObjectOfType(value, TYPE_COMPUTED)
45
157
 
158
+ /**
159
+ * Check if the provided value is a callback that may be used as input for toSignal() to derive a computed state
160
+ *
161
+ * @since 0.12.0
162
+ * @param {unknown} value - value to check
163
+ * @returns {boolean} - true if value is a callback or callbacks object, false otherwise
164
+ */
165
+ const isComputedCallback = /*#__PURE__*/ <T extends {}>(
166
+ value: unknown,
167
+ ): value is ComputedCallback<T> => isFunction(value) && value.length < 2
168
+
46
169
  /* === Exports === */
47
170
 
48
171
  export {
@@ -51,4 +174,5 @@ export {
51
174
  TYPE_COMPUTED,
52
175
  computed,
53
176
  isComputed,
177
+ isComputedCallback,
54
178
  }
package/src/effect.d.ts CHANGED
@@ -1,10 +1,8 @@
1
- import { type Signal } from './signal';
1
+ import { type Signal, type SignalValues } from './signal';
2
2
  import { type Cleanup } from './scheduler';
3
3
  type EffectMatcher<S extends Signal<{}>[]> = {
4
4
  signals: S;
5
- ok: (...values: {
6
- [K in keyof S]: S[K] extends Signal<infer T> ? T : never;
7
- }) => void | Cleanup;
5
+ ok: (...values: SignalValues<S>) => void | Cleanup;
8
6
  err?: (...errors: Error[]) => void | Cleanup;
9
7
  nil?: () => void | Cleanup;
10
8
  };