@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 +133 -39
- package/index.d.ts +5 -4
- package/index.js +1 -1
- package/index.ts +5 -4
- package/lib/computed.d.ts +18 -5
- package/lib/computed.ts +111 -43
- package/lib/effect.d.ts +13 -2
- package/lib/effect.ts +28 -9
- package/lib/scheduler.d.ts +40 -0
- package/lib/scheduler.ts +127 -0
- package/lib/signal.d.ts +16 -26
- package/lib/signal.ts +49 -55
- package/lib/state.d.ts +8 -46
- package/lib/state.ts +89 -69
- package/lib/util.d.ts +4 -1
- package/lib/util.ts +18 -1
- package/package.json +5 -2
- package/test/batch.test.ts +99 -0
- package/test/benchmark.test.ts +127 -52
- package/test/computed.test.ts +329 -0
- package/test/effect.test.ts +157 -0
- package/test/state.test.ts +199 -0
- package/test/util/dependency-graph.ts +95 -37
- package/test/util/framework-types.ts +53 -0
- package/test/util/perf-tests.ts +42 -0
- package/test/util/reactive-framework.ts +22 -0
- package/test/cause-effect.test.ts +0 -458
- package/test/util/pseudo-random.ts +0 -45
package/README.md
CHANGED
|
@@ -1,14 +1,48 @@
|
|
|
1
1
|
# Cause & Effect
|
|
2
2
|
|
|
3
|
-
Version 0.
|
|
3
|
+
Version 0.12.0
|
|
4
4
|
|
|
5
|
-
**Cause & Effect**
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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('
|
|
36
|
-
|
|
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
|
-
|
|
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('
|
|
66
|
-
|
|
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 `
|
|
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,
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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.
|
|
3
|
+
* @version 0.12.0
|
|
4
4
|
* @author Esther Brunner
|
|
5
5
|
*/
|
|
6
|
-
export {
|
|
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
|
|
9
|
-
export {
|
|
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
|
|
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.
|
|
3
|
+
* @version 0.12.0
|
|
4
4
|
* @author Esther Brunner
|
|
5
5
|
*/
|
|
6
|
-
export {
|
|
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
|
|
9
|
-
export {
|
|
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
|
-
|
|
2
|
-
|
|
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}
|
|
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 {}>(
|
|
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 {
|
|
2
|
-
import {
|
|
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
|
|
7
|
-
|
|
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
|
-
/* ===
|
|
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}
|
|
35
|
+
* @param {() => T} callbacksOrFn - compute function to derive state
|
|
23
36
|
* @returns {Computed<T>} result of derived state
|
|
24
37
|
*/
|
|
25
|
-
export const computed =
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
42
|
+
const callbacks = isFunction(callbacksOrFn)
|
|
43
|
+
? { ok: callbacksOrFn }
|
|
44
|
+
: callbacksOrFn
|
|
30
45
|
const watchers: Watcher[] = []
|
|
31
|
-
let value: T
|
|
32
|
-
let error: Error |
|
|
33
|
-
let
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
if (
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}
|
|
16
|
+
* @param {() => void} callbacksOrFn - callback function to be executed when a state changes
|
|
6
17
|
*/
|
|
7
|
-
export declare
|
|
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
|
|
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}
|
|
24
|
+
* @param {() => void} callbacksOrFn - callback function to be executed when a state changes
|
|
11
25
|
*/
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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>;
|