@zeix/cause-effect 0.10.1 → 0.12.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,14 +1,48 @@
1
1
  # Cause & Effect
2
2
 
3
- Version 0.10.1
3
+ Version 0.12.0
4
4
 
5
- **Cause & Effect** - efficient state management with signals that sync instantly and reactively across your application.
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.
6
+
7
+ ## What is Cause & Effect?
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.
10
+
11
+ ## Why Cause & Effect?
12
+
13
+ - **Simplicity**: Easy to learn and use, with a small API surface.
14
+ - **Performance**: Efficient updates that only recompute what's necessary.
15
+ - **Type Safety**: Full TypeScript support for robust applications.
16
+ - **Flexibility**: Works well with any UI framework or vanilla JavaScript.
17
+ - **Lightweight**: Dependency-free, only 1kB gzipped over the wire.
6
18
 
7
19
  ## Key Features
8
20
 
9
- * **Efficient State Management**: Use lightweight signals for state updates that automatically notify dependents when needed.
10
- * **Support for Asynchronous Operations**: Handle state updates smoothly, even when dealing with network requests or Promise-based libraries, without disrupting reactivity.
11
- * **Memoized Computed Signals**: Optionally create derived values that are cached and automatically recalculated when source data changes.
21
+ - 🚀 Efficient state management with automatic dependency tracking
22
+ - Built-in support for async operations
23
+ - 🧠 Memoized computed values
24
+ - 🛡️ Type-safe and non-nullable signals
25
+ - 🎭 Declarative error and pending state handling
26
+
27
+ ## Quick Example
28
+
29
+ ```js
30
+ import { state, effect } from '@zeix/cause-effect'
31
+
32
+ // Create a state signal
33
+ const count = state(0)
34
+
35
+ // Create a computed signal
36
+ const doubleCount = count.map(v => v * 2)
37
+
38
+ // Create an effect
39
+ effect((c, d) => {
40
+ console.log(`Count: ${c}, Double: ${d}`)
41
+ }, count, doubleCount)
42
+
43
+ // Update the state
44
+ count.set(5) // Logs: "Count: 5, Double: 10"
45
+ ```
12
46
 
13
47
  ## Installation
14
48
 
@@ -32,8 +66,9 @@ import { state, effect } from '@zeix/cause-effect'
32
66
  const count = state(42)
33
67
  effect(() => console.log(count.get())) // logs '42'
34
68
  count.set(24) // logs '24'
35
- document.querySelector('button.increment')
36
- .addEventListener('click', () => count.update(v => ++v))
69
+ document.querySelector('.increment').addEventListener('click', () => {
70
+ count.update(v => ++v)
71
+ })
37
72
  // Click on button logs '25', '26', and so on
38
73
  ```
39
74
 
@@ -48,8 +83,9 @@ const count = state(42)
48
83
  const isOdd = computed(() => count.get() % 2)
49
84
  effect(() => console.log(isOdd.get())) // logs 'false'
50
85
  count.set(24) // logs nothing because 24 is also an even number
51
- document.querySelector('button.increment')
52
- .addEventListener('click', () => count.update(v => ++v))
86
+ document.querySelector('button.increment').addEventListener('click', () => {
87
+ count.update(v => ++v)
88
+ })
53
89
  // Click on button logs 'true', 'false', and so on
54
90
  ```
55
91
 
@@ -62,8 +98,9 @@ const count = state(42)
62
98
  const isOdd = count.map(v => v % 2)
63
99
  effect(() => console.log(isOdd.get())) // logs 'false'
64
100
  count.set(24) // logs nothing because 24 is also an even number
65
- document.querySelector('button.increment')
66
- .addEventListener('click', () => count.set(v => ++v))
101
+ document.querySelector('.increment').addEventListener('click', () => {
102
+ count.update(v => ++v)
103
+ })
67
104
  // Click on button logs 'true', 'false', and so on
68
105
  ```
69
106
 
@@ -71,7 +108,7 @@ document.querySelector('button.increment')
71
108
 
72
109
  Async computed signals are as straight forward as their sync counterparts. Just create the computed signal with an async function.
73
110
 
74
- **Caution**: You can't use the `.map()` method to create an async computed signal. And async computed signals will return `undefined` until the Promise is resolved.
111
+ **Caution**: You can't use the `.map()` method to create an async computed signal. And async computed signals will return a Symbol `UNSET` until the Promise is resolved.
75
112
 
76
113
  ```js
77
114
  import { state, computed, effect } from '@zeix/cause-effect'
@@ -82,41 +119,98 @@ const entryData = computed(async () => {
82
119
  if (!response.ok) return new Error(`Failed to fetch data: ${response.statusText}`)
83
120
  return response.json()
84
121
  })
85
- effect(() => {
86
- let data
87
- try {
88
- data = entryData.get()
89
- } catch (error) {
90
- console.error(error.message) // logs the error message if an error ocurred
91
- return
92
- }
93
- if (null == data) return // doesn't do anything while we are still waiting for the data
94
- document.querySelector('.entry h2').textContent = data.title
95
- document.querySelector('.entry p').textContent = data.description
96
- })
97
122
  // Updates h1 and p of the entry as soon as fetched data for entry becomes available
98
123
  document.querySelector('button.next')
99
124
  .addEventListener('click', () => entryId.update(v => ++v))
100
125
  // Click on button updates h1 and p of the entry as soon as fetched data for the next entry is loaded
101
126
  ```
102
127
 
128
+ ### Handling Unset Values and Errors in Effects
129
+
130
+ Computations can fail and throw errors. Promises may not have resolved yet when you try to access their value. **Cause & Effect makes it easy to deal with errors and unresolved async functions.** Computed functions will catch errors and re-throw them when you access their values.
131
+
132
+ **Effects** are where you handle different cases:
133
+
134
+ ```js
135
+ const h2 = document.querySelector('.entry h2')
136
+ const p = document.querySelector('.entry p')
137
+ effect({
138
+
139
+ // Handle pending states while fetching data
140
+ nil: () => {
141
+ h2.textContent = 'Loading...'
142
+ },
143
+
144
+ // Handle errors
145
+ err: (error) => {
146
+ h2.textContent = 'Oops, Something Went Wrong'
147
+ p.textContent = error.message
148
+ },
149
+
150
+ // Happy path, data is entryData.get()
151
+ ok: (data) => {
152
+ h2.textContent = data.title
153
+ p.textContent = data.description
154
+ }
155
+ }, entryData) // assuming an `entryData` async computed signal as in the example above
156
+ ```
157
+
158
+ 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()`.
159
+
160
+ If you want an effect based on a single signal, there's a shorthand too: The `.match()` method on either `State` or `Computed`. You can use it for easy debugging, for example:
161
+
162
+ ```js
163
+ signal.match({
164
+ ok: v => console.log('Value:', v),
165
+ nil: () => console.warn('Not ready'),
166
+ err: e => console.error('Error:', e)
167
+ })
168
+ ```
169
+
103
170
  ### Effects and Batching
104
171
 
105
172
  Effects run synchronously as soon as source signals update. If you need to set multiple signals you can batch them together to ensure dependents are executed only once.
106
173
 
107
174
  ```js
108
- import { state, computed, effect, batch } from '@zeix/cause-effect'
109
-
110
- const a = state(3)
111
- const b = state(4)
112
- const sum = computed(() => a.get() + b.get())
113
- effect(() => console.log(sum.get())) // logs '7'
114
- document.querySelector('button.double-all')
115
- .addEventListener('click', () =>
116
- batch(() => {
117
- a.update(v => v * 2)
118
- b.update(v => v * 2)
119
- }
120
- ))
121
- // Click on button logs '14' only once (instead of first '10' and then '14' without batch)
122
- ```
175
+ import { state, computed, batch } from '@zeix/cause-effect'
176
+
177
+ // State: define an array of State<number>
178
+ const signals = [state(2), state(3), state(5)]
179
+
180
+ // Computed: derive a calculation ...
181
+ const sum = computed(
182
+ (...values) => values.reduce((total, v) => total + v, 0),
183
+ ...signals
184
+ ).map(v => { // ... perform validation and handle errors
185
+ if (!Number.isFinite(v)) throw new Error('Invalid value')
186
+ return v
187
+ })
188
+
189
+ // Effect: switch cases for the result
190
+ sum.match({
191
+ ok: v => console.log('Sum:', v),
192
+ err: error => console.error('Error:', error)
193
+ })
194
+
195
+ // Batch: apply changes to all signals in a single transaction
196
+ document.querySelector('.double-all').addEventListener('click', () => {
197
+ batch(() => {
198
+ signals.forEach(signal => signal.update(v => v * 2))
199
+ })
200
+ })
201
+ // Click on button logs '20' only once
202
+ // (instead of first '12', then '15' and then '20' without batch)
203
+
204
+ // Provoke an error - but no worries: it will be handled fine
205
+ signals[0].set(NaN)
206
+ ```
207
+
208
+ This example showcases several powerful features of Cause & Effect:
209
+
210
+ 1. **Composability and Declarative Computations**: Easily compose multiple signals into a single computed value, declaring how values should be calculated based on other signals.
211
+ 2. **Automatic Dependency Tracking and Efficient Updates**: The library tracks dependencies between signals and computed values, ensuring efficient propagation of changes.
212
+ 3. **Robust Error Handling**: Built-in error handling at computation level and reactive error management allow for graceful handling of unexpected situations.
213
+ 4. **Performance Optimization through Batching**: Group multiple state changes to ensure dependent computations and effects run only once after all changes are applied.
214
+ 5. **Flexibility and Integration**: Seamlessly integrates with DOM manipulation and event listeners, fitting into any JavaScript application or framework.
215
+
216
+ 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.
package/index.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * @name Cause & Effect
3
- * @version 0.10.1
3
+ * @version 0.12.0
4
4
  * @author Esther Brunner
5
5
  */
6
- export { UNSET, State, state, isState } from './lib/state';
6
+ export { type Signal, type MaybeSignal, UNSET, isSignal, toSignal } from './lib/signal';
7
+ export { type State, state, isState } from './lib/state';
7
8
  export { type Computed, computed, isComputed } from './lib/computed';
8
- export { type Signal, type MaybeSignal, isSignal, toSignal, batch } from './lib/signal';
9
- export { effect } from './lib/effect';
9
+ export { type EffectOkCallback, type EffectCallbacks, effect } from './lib/effect';
10
+ export { type EnqueueDedupe, batch, watch, enqueue } from './lib/scheduler';
package/index.js CHANGED
@@ -1 +1 @@
1
- var M=(T)=>typeof T==="function",C=(T)=>M(T)&&/^async\s+/.test(T.toString()),R=(T)=>M(T)&&T.length<2,Y=(T)=>(y)=>y instanceof T,K=Y(Error),D=Y(Promise);var O="Computed",I=(T,y)=>{y=y??C(T);let F=[],H,j=null,J=!0,U=()=>{if(J=!0,y)A(F)},Z={[Symbol.toStringTag]:O,get:()=>{if(y)z(F);if(!y||J)B(()=>{let q=(x)=>{H=x,J=!1,j=null},$=(x)=>{j=K(x)?x:new Error(`Computed function failed: ${x}`)};try{let x=T(H);D(x)?x.then(q).catch($):q(x)}catch(x){$(x)}},U);if(K(j))throw j;return H},map:(q)=>I(()=>q(Z.get()))};return Z},N=(T)=>!!T&&typeof T==="object"&&T[Symbol.toStringTag]===O;var L,Q=!1,V=[],P=(T)=>X(T)||N(T),k=(T,y=!1)=>P(T)?T:R(T)?I(T,y):W(T),z=(T)=>{if(L&&!T.includes(L))T.push(L)},A=(T)=>T.forEach((y)=>Q?V.push(y):y()),B=(T,y)=>{let F=L;L=y,T(),L=F},E=(T)=>{Q=!0,T(),Q=!1,V.forEach((y)=>y()),V.length=0};var S=Symbol();class G{T;watchers=[];constructor(T){this.value=T}get(){return z(this.watchers),this.value}set(T){if(Object.is(this.value,T))return;if(this.value=T,A(this.watchers),S===T)this.watchers=[]}update(T){this.set(T(this.value))}map(T){return I(()=>T(this.get()))}}var W=(T)=>new G(T),X=(T)=>T instanceof G;var g=(T)=>{let y=()=>B(()=>{try{T()}catch(F){console.error(F)}},y);y()};export{k as toSignal,W as state,X as isState,P as isSignal,N as isComputed,g as effect,I as computed,E as batch,S as UNSET,G as State};
1
+ var j=(y)=>typeof y==="function";var O=(y)=>j(y)&&y.length<2,D=(y,x)=>Object.prototype.toString.call(y)===`[object ${x}]`,g=(y)=>(x)=>x instanceof y,A=g(Error),o=g(Promise),C=(y)=>A(y)?y:new Error(String(y)),k=(y,x)=>{if(!x)return!1;return y.name===x.name&&y.message===x.message};var X,P=new Set,_=0,S=new Map,M,b=()=>{M=void 0;for(let y of S.values()){for(let x of y.values())x();y.clear()}},v=()=>{if(M)cancelAnimationFrame(M);M=requestAnimationFrame(b)};queueMicrotask(b);var T=(y)=>{if(X&&!y.includes(X))y.push(X)},I=(y)=>{for(let x of y)_?P.add(x):x()},U=()=>{while(P.size){let y=Array.from(P);P.clear();for(let x of y)x()}},t=(y)=>{_++,y(),U(),_--},q=(y,x)=>{let z=X;X=x,y(),X=z},u=(y,x)=>new Promise((z,L)=>{let B=()=>{try{z(y())}catch(G){L(G)}};if(x){let[G,J]=x;if(!S.has(G))S.set(G,new Map);S.get(G).set(J,B)}v()});function R(y,...x){let z=j(y)?{ok:y}:y,L=()=>q(()=>{let B=F(x,z);if(A(B))console.error("Unhandled error in effect:",B)},L);L()}var p="Computed",Z=(y,...x)=>{let z=j(y)?{ok:y}:y,L=[],B=Q,G,J=!0,K=!1,H=!1,W=($)=>{if(!Object.is($,B))B=$,J=!1,G=void 0,K=!1},V=()=>{K=Q===B,B=Q,G=void 0},m=($)=>{let N=C($);K=k(N,G),B=Q,G=N},n=()=>{if(J=!0,!K)I(L)},i=()=>q(()=>{if(H)throw new Error("Circular dependency detected");K=!0,H=!0;let $=F(x,z);if(o($))V(),$.then((N)=>{W(N),I(L)}).catch(m);else if($==null||Q===$)V();else if(A($))m($);else W($);H=!1},n),Y={[Symbol.toStringTag]:p,get:()=>{if(T(L),U(),J)i();if(G)throw G;return B},map:($)=>Z(()=>$(Y.get())),match:($)=>{return R($,Y),Y}};return Y},f=(y)=>D(y,p);var h="State",w=(y)=>{let x=[],z=y,L={[Symbol.toStringTag]:h,get:()=>{return T(x),z},set:(B)=>{if(Object.is(z,B))return;if(z=B,I(x),Q===z)x.length=0},update:(B)=>{L.set(B(z))},map:(B)=>Z(()=>B(L.get())),match:(B)=>{return R(B,L),L}};return L},E=(y)=>D(y,h);var Q=Symbol(),d=(y)=>E(y)||f(y),c=(y)=>d(y)?y:O(y)?Z(y):w(y),F=(y,x)=>{let{ok:z,nil:L,err:B}=x,G=[],J=[],K=!1;for(let W of y)try{let V=W.get();if(V===Q)K=!0;G.push(V)}catch(V){J.push(C(V))}let H=void 0;try{if(K&&L)H=L();else if(J.length)H=B?B(...J):J[0];else if(!K)H=z(...G)}catch(W){if(H=C(W),B)H=B(H)}finally{return H}};export{q as watch,c as toSignal,w as state,E as isState,d as isSignal,f as isComputed,u as enqueue,R as effect,Z as computed,t as batch,Q as UNSET};
package/index.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * @name Cause & Effect
3
- * @version 0.10.1
3
+ * @version 0.12.0
4
4
  * @author Esther Brunner
5
5
  */
6
- export { UNSET, State, state, isState } from './lib/state'
6
+ export { type Signal, type MaybeSignal, UNSET, isSignal, toSignal } from './lib/signal'
7
+ export { type State, state, isState } from './lib/state'
7
8
  export { type Computed, computed, isComputed } from './lib/computed'
8
- export { type Signal, type MaybeSignal, isSignal, toSignal, batch } from './lib/signal'
9
- export { effect } from './lib/effect'
9
+ export { type EffectOkCallback, type EffectCallbacks, effect } from './lib/effect'
10
+ export { type EnqueueDedupe, batch, watch, enqueue } from './lib/scheduler'
package/lib/computed.d.ts CHANGED
@@ -1,16 +1,29 @@
1
- export type Computed<T> = {
2
- [Symbol.toStringTag]: "Computed";
1
+ import { type SignalValue, type UnknownSignal } from './signal';
2
+ import { type EffectCallbacks } from './effect';
3
+ export type ComputedOkCallback<T extends {}, U extends UnknownSignal[]> = (...values: {
4
+ [K in keyof U]: SignalValue<U[K]>;
5
+ }) => T | Promise<T>;
6
+ export type ComputedCallbacks<T extends {}, U extends UnknownSignal[]> = {
7
+ ok: (...values: {
8
+ [K in keyof U]: SignalValue<U[K]>;
9
+ }) => T | Promise<T>;
10
+ nil?: () => T | Promise<T>;
11
+ err?: (...errors: Error[]) => T | Promise<T>;
12
+ };
13
+ export type Computed<T extends {}> = {
14
+ [Symbol.toStringTag]: 'Computed';
3
15
  get: () => T;
4
16
  map: <U extends {}>(fn: (value: T) => U) => Computed<U>;
17
+ match: (callbacks: EffectCallbacks<[Computed<T>]>) => void;
5
18
  };
6
19
  /**
7
20
  * Create a derived state from existing states
8
21
  *
9
22
  * @since 0.9.0
10
- * @param {() => T} fn - compute function to derive state
23
+ * @param {() => T} callbacksOrFn - compute function to derive state
11
24
  * @returns {Computed<T>} result of derived state
12
25
  */
13
- export declare const computed: <T extends {}>(fn: (v?: T) => T | Promise<T>, memo?: boolean) => Computed<T>;
26
+ export declare const computed: <T extends {}, U extends UnknownSignal[]>(callbacksOrFn: ComputedCallbacks<T, U> | ComputedOkCallback<T, U>, ...signals: U) => Computed<T>;
14
27
  /**
15
28
  * Check if a value is a computed state
16
29
  *
@@ -18,4 +31,4 @@ export declare const computed: <T extends {}>(fn: (v?: T) => T | Promise<T>, mem
18
31
  * @param {unknown} value - value to check
19
32
  * @returns {boolean} - true if value is a computed state, false otherwise
20
33
  */
21
- export declare const isComputed: <T>(value: unknown) => value is Computed<T>;
34
+ export declare const isComputed: <T extends {}>(value: unknown) => value is Computed<T>;
package/lib/computed.ts CHANGED
@@ -1,71 +1,140 @@
1
- import { type Watcher, subscribe, notify, watch } from "./signal"
2
- import { isAsyncFunction, isError, isPromise } from "./util"
1
+ import { resolveSignals, UNSET, type SignalValue, type UnknownSignal } from './signal'
2
+ import { isEquivalentError, isError, isFunction, isObjectOfType, isPromise, toError } from './util'
3
+ import { type Watcher, flush, notify, subscribe, watch } from './scheduler'
4
+ import { type EffectCallbacks, effect } from './effect'
3
5
 
4
6
  /* === Types === */
5
7
 
6
- export type Computed<T> = {
7
- [Symbol.toStringTag]: "Computed"
8
+ export type ComputedOkCallback<T extends {}, U extends UnknownSignal[]> = (
9
+ ...values: { [K in keyof U]: SignalValue<U[K]> }
10
+ ) => T | Promise<T>
11
+
12
+ export type ComputedCallbacks<T extends {}, U extends UnknownSignal[]> = {
13
+ ok: (...values: { [K in keyof U]: SignalValue<U[K]> }) => T | Promise<T>
14
+ nil?: () => T | Promise<T>
15
+ err?: (...errors: Error[]) => T | Promise<T>
16
+ }
17
+
18
+ export type Computed<T extends {}> = {
19
+ [Symbol.toStringTag]: 'Computed'
8
20
  get: () => T
9
21
  map: <U extends {}>(fn: (value: T) => U) => Computed<U>
22
+ match: (callbacks: EffectCallbacks<[Computed<T>]>) => void
10
23
  }
11
24
 
12
25
  /* === Constants === */
13
26
 
14
27
  const TYPE_COMPUTED = 'Computed'
15
28
 
16
- /* === Namespace Computed === */
29
+ /* === Computed Factory === */
17
30
 
18
31
  /**
19
32
  * Create a derived state from existing states
20
33
  *
21
34
  * @since 0.9.0
22
- * @param {() => T} fn - compute function to derive state
35
+ * @param {() => T} callbacksOrFn - compute function to derive state
23
36
  * @returns {Computed<T>} result of derived state
24
37
  */
25
- export const computed = /*#__PURE__*/ <T extends {}>(
26
- fn: (v?: T) => T | Promise<T>,
27
- memo?: boolean
38
+ export const computed = <T extends {}, U extends UnknownSignal[]>(
39
+ callbacksOrFn: ComputedCallbacks<T, U> | ComputedOkCallback<T, U>,
40
+ ...signals: U
28
41
  ): Computed<T> => {
29
- memo = memo ?? isAsyncFunction(fn)
42
+ const callbacks = isFunction(callbacksOrFn)
43
+ ? { ok: callbacksOrFn }
44
+ : callbacksOrFn
30
45
  const watchers: Watcher[] = []
31
- let value: T
32
- let error: Error | null = null
33
- let stale = true
46
+ let value: T = UNSET
47
+ let error: Error | undefined
48
+ let dirty = true
49
+ let unchanged = false
50
+ let computing = false
34
51
 
35
- const mark: Watcher = () => {
36
- stale = true
37
- if (memo) notify(watchers)
52
+ // Functions to update internal state
53
+ const ok = (v: T) => {
54
+ if (!Object.is(v, value)) {
55
+ value = v
56
+ dirty = false
57
+ error = undefined
58
+ unchanged = false
59
+ }
60
+ }
61
+ const nil = () => {
62
+ unchanged = (UNSET === value)
63
+ value = UNSET
64
+ error = undefined
65
+ }
66
+ const err = (e: unknown) => {
67
+ const newError = toError(e)
68
+ unchanged = isEquivalentError(newError, error)
69
+ value = UNSET
70
+ error = newError
38
71
  }
39
72
 
73
+ // Called when notified from sources (push)
74
+ const mark = () => {
75
+ dirty = true
76
+ if (!unchanged) notify(watchers)
77
+ }
78
+
79
+ // Called when requested by dependencies (pull)
80
+ const compute = () => watch(() => {
81
+ if (computing) throw new Error('Circular dependency detected')
82
+ unchanged = true
83
+ computing = true
84
+ const result = resolveSignals(signals, callbacks as ComputedCallbacks<T, U>)
85
+ if (isPromise(result)) {
86
+ nil() // sync
87
+ result.then(v => {
88
+ ok(v) // async
89
+ notify(watchers)
90
+ }).catch(err)
91
+ } else if (null == result || UNSET === result) nil()
92
+ else if (isError(result)) err(result)
93
+ else ok(result)
94
+ computing = false
95
+ }, mark)
96
+
40
97
  const c: Computed<T> = {
41
98
  [Symbol.toStringTag]: TYPE_COMPUTED,
42
- get: () => {
43
- if (memo) subscribe(watchers)
44
- if (!memo || stale) watch(() => {
45
- const handleOk = (v: T) => {
46
- value = v
47
- stale = false
48
- error = null
49
- }
50
- const handleErr = (e: unknown) => {
51
- error = isError(e)
52
- ? e
53
- : new Error(`Computed function failed: ${e}`)
54
- }
55
- try {
56
- const res = fn(value)
57
- isPromise(res)
58
- ? res.then(handleOk).catch(handleErr)
59
- : handleOk(res)
60
- } catch (e) {
61
- handleErr(e)
62
- }
63
- }, mark)
64
- if (isError(error)) throw error
99
+
100
+ /**
101
+ * Get the current value of the computed
102
+ *
103
+ * @since 0.9.0
104
+ * @method of Computed<T>
105
+ * @returns {T} - current value of the computed
106
+ */
107
+ get: (): T => {
108
+ subscribe(watchers)
109
+ flush()
110
+ if (dirty) compute()
111
+ if (error) throw error
65
112
  return value
66
113
  },
67
- map: <U extends {}>(fn: (value: T) => U): Computed<U> =>
114
+
115
+ /**
116
+ * Create a computed signal from the current computed signal
117
+ *
118
+ * @since 0.9.0
119
+ * @method of Computed<T>
120
+ * @param {(value: T) => R} fn
121
+ * @returns {Computed<R>} - computed signal
122
+ */
123
+ map: <R extends {}>(fn: (value: T) => R): Computed<R> =>
68
124
  computed(() => fn(c.get())),
125
+
126
+ /**
127
+ * Case matching for the computed signal with effect callbacks
128
+ *
129
+ * @since 0.12.0
130
+ * @method of Computed<T>
131
+ * @param {EffectCallbacks[<T>]} callbacks
132
+ * @returns {Computed<T>} - self, for chaining effect callbacks
133
+ */
134
+ match: (callbacks: EffectCallbacks<[Computed<T>]>): Computed<T> => {
135
+ effect(callbacks, c)
136
+ return c
137
+ }
69
138
  }
70
139
  return c
71
140
  }
@@ -79,6 +148,5 @@ export const computed = /*#__PURE__*/ <T extends {}>(
79
148
  * @param {unknown} value - value to check
80
149
  * @returns {boolean} - true if value is a computed state, false otherwise
81
150
  */
82
- export const isComputed = /*#__PURE__*/ <T>(value: unknown): value is Computed<T> =>
83
- !!value && typeof value === 'object'
84
- && (value as { [key in typeof Symbol.toStringTag]: string })[Symbol.toStringTag] === TYPE_COMPUTED
151
+ export const isComputed = /*#__PURE__*/ <T extends {}>(value: unknown): value is Computed<T> =>
152
+ isObjectOfType(value, TYPE_COMPUTED)
package/lib/effect.d.ts CHANGED
@@ -1,7 +1,18 @@
1
+ import { type SignalValue, type UnknownSignal } from './signal';
2
+ export type EffectOkCallback<T extends UnknownSignal[]> = (...values: {
3
+ [K in keyof T]: SignalValue<T[K]>;
4
+ }) => void;
5
+ export type EffectCallbacks<T extends UnknownSignal[]> = {
6
+ ok: (...values: {
7
+ [K in keyof T]: SignalValue<T[K]>;
8
+ }) => void;
9
+ nil?: () => void;
10
+ err?: (...errors: Error[]) => void;
11
+ };
1
12
  /**
2
13
  * Define what happens when a reactive state changes
3
14
  *
4
15
  * @since 0.1.0
5
- * @param {() => void} fn - callback function to be executed when a state changes
16
+ * @param {() => void} callbacksOrFn - callback function to be executed when a state changes
6
17
  */
7
- export declare const effect: (fn: () => void) => void;
18
+ export declare function effect<T extends UnknownSignal[]>(callbacksOrFn: EffectCallbacks<T> | EffectOkCallback<T>, ...signals: T): void;
package/lib/effect.ts CHANGED
@@ -1,5 +1,19 @@
1
1
 
2
- import { type Watcher, watch } from "./signal"
2
+ import { resolveSignals, type SignalValue, type UnknownSignal } from './signal'
3
+ import { isError, isFunction } from './util'
4
+ import { watch } from './scheduler'
5
+
6
+ /* === Types === */
7
+
8
+ export type EffectOkCallback<T extends UnknownSignal[]> = (
9
+ ...values: { [K in keyof T]: SignalValue<T[K]> }
10
+ ) => void
11
+
12
+ export type EffectCallbacks<T extends UnknownSignal[]> = {
13
+ ok: (...values: { [K in keyof T]: SignalValue<T[K]> }) => void
14
+ nil?: () => void
15
+ err?: (...errors: Error[]) => void
16
+ }
3
17
 
4
18
  /* === Exported Function === */
5
19
 
@@ -7,15 +21,20 @@ import { type Watcher, watch } from "./signal"
7
21
  * Define what happens when a reactive state changes
8
22
  *
9
23
  * @since 0.1.0
10
- * @param {() => void} fn - callback function to be executed when a state changes
24
+ * @param {() => void} callbacksOrFn - callback function to be executed when a state changes
11
25
  */
12
- export const effect = (fn: () => void) => {
13
- const run: Watcher = () => watch(() => {
14
- try {
15
- fn()
16
- } catch (error) {
17
- console.error(error)
18
- }
26
+ export function effect<T extends UnknownSignal[]>(
27
+ callbacksOrFn: EffectCallbacks<T> | EffectOkCallback<T>,
28
+ ...signals: T
29
+ ): void {
30
+ const callbacks = isFunction(callbacksOrFn)
31
+ ? { ok: callbacksOrFn }
32
+ : callbacksOrFn
33
+
34
+ const run = () => watch(() => {
35
+ const result = resolveSignals(signals, callbacks as EffectCallbacks<T>)
36
+ if (isError(result))
37
+ console.error('Unhandled error in effect:', result)
19
38
  }, run)
20
39
  run()
21
40
  }
@@ -0,0 +1,40 @@
1
+ export type EnqueueDedupe = [Element, string];
2
+ export type Watcher = () => void;
3
+ export type Updater = <T>() => T;
4
+ /**
5
+ * Add active watcher to the array of watchers
6
+ *
7
+ * @param {Watcher[]} watchers - watchers of the signal
8
+ */
9
+ export declare const subscribe: (watchers: Watcher[]) => void;
10
+ /**
11
+ * Add watchers to the pending set of change notifications
12
+ *
13
+ * @param {Watcher[]} watchers - watchers of the signal
14
+ */
15
+ export declare const notify: (watchers: Watcher[]) => void;
16
+ /**
17
+ * Flush all pending changes to notify watchers
18
+ */
19
+ export declare const flush: () => void;
20
+ /**
21
+ * Batch multiple changes in a single signal graph and DOM update cycle
22
+ *
23
+ * @param {() => void} fn - function with multiple signal writes to be batched
24
+ */
25
+ export declare const batch: (fn: () => void) => void;
26
+ /**
27
+ * Run a function in a reactive context
28
+ *
29
+ * @param {() => void} run - function to run the computation or effect
30
+ * @param {Watcher} mark - function to be called when the state changes
31
+ */
32
+ export declare const watch: (run: () => void, mark: Watcher) => void;
33
+ /**
34
+ * Enqueue a function to be executed on the next animation frame
35
+ *
36
+ * @param callback
37
+ * @param dedupe
38
+ * @returns
39
+ */
40
+ export declare const enqueue: <T>(update: Updater, dedupe?: EnqueueDedupe) => Promise<T>;