@zeix/cause-effect 0.9.7
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/.editorconfig +7 -0
- package/LICENSE +21 -0
- package/README.md +122 -0
- package/index.d.ts +9 -0
- package/index.js +1 -0
- package/index.ts +9 -0
- package/lib/computed.d.ts +14 -0
- package/lib/computed.ts +75 -0
- package/lib/effect.d.ts +7 -0
- package/lib/effect.ts +21 -0
- package/lib/signal.d.ts +48 -0
- package/lib/signal.ts +98 -0
- package/lib/state.d.ts +38 -0
- package/lib/state.ts +67 -0
- package/lib/util.d.ts +6 -0
- package/lib/util.ts +16 -0
- package/package.json +30 -0
- package/test/benchmark.test.ts +507 -0
- package/test/cause-effect.test.ts +467 -0
- package/test/util/dependency-graph.ts +131 -0
- package/test/util/pseudo-random.ts +45 -0
- package/tsconfig.json +34 -0
package/.editorconfig
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 - 2025 Zeix AG
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Cause & Effect
|
|
2
|
+
|
|
3
|
+
Version 0.9.7
|
|
4
|
+
|
|
5
|
+
**Cause & Effect** - efficient state management with signals that sync instantly and reactively across your application.
|
|
6
|
+
|
|
7
|
+
## Key Features
|
|
8
|
+
|
|
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.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# with npm
|
|
17
|
+
npm install @zeix/cause-effect
|
|
18
|
+
|
|
19
|
+
# or with bun
|
|
20
|
+
bun add @zeix/cause-effect
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Basic Usage
|
|
24
|
+
|
|
25
|
+
### Single State Signal
|
|
26
|
+
|
|
27
|
+
`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 an updater function of the form `(v: T) => T`.
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
import { state, effect } from '@zeix/cause-effect'
|
|
31
|
+
|
|
32
|
+
const count = state(42)
|
|
33
|
+
effect(() => console.log(count.get())) // logs '42'
|
|
34
|
+
count.set(24) // logs '24'
|
|
35
|
+
document.querySelector('button.increment')
|
|
36
|
+
.addEventListener('click', () => count.set(v => ++v))
|
|
37
|
+
// Click on button logs '25', '26', and so on
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Sync Computed Signal
|
|
41
|
+
|
|
42
|
+
`computed()` creates a new computed signal. Computed signals are read-only and you can access the current resulting value using the `.get()` method.
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
import { state, computed, effect } from '@zeix/cause-effect'
|
|
46
|
+
|
|
47
|
+
const count = state(42)
|
|
48
|
+
const isOdd = computed(() => count.get() % 2)
|
|
49
|
+
effect(() => console.log(isOdd.get())) // logs 'false'
|
|
50
|
+
count.set(24) // logs nothing because 24 is also an even number
|
|
51
|
+
document.querySelector('button.increment')
|
|
52
|
+
.addEventListener('click', () => count.set(v => ++v))
|
|
53
|
+
// Click on button logs 'true', 'false', and so on
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
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:
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
import { state, effect } from '@zeix/cause-effect'
|
|
60
|
+
|
|
61
|
+
const count = state(42)
|
|
62
|
+
const isOdd = count.map(v => v % 2)
|
|
63
|
+
effect(() => console.log(isOdd.get())) // logs 'false'
|
|
64
|
+
count.set(24) // logs nothing because 24 is also an even number
|
|
65
|
+
document.querySelector('button.increment')
|
|
66
|
+
.addEventListener('click', () => count.set(v => ++v))
|
|
67
|
+
// Click on button logs 'true', 'false', and so on
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Async Computed Signal
|
|
71
|
+
|
|
72
|
+
Async computed signals are as straight forward as their sync counterparts. Just create the computed signal with an async function.
|
|
73
|
+
|
|
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.
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
import { state, computed, effect } from '@zeix/cause-effect'
|
|
78
|
+
|
|
79
|
+
const entryId = state(42)
|
|
80
|
+
const entryData = computed(async () => {
|
|
81
|
+
const response = await fetch(`/api/entry/${entryId.get()}`)
|
|
82
|
+
if (!response.ok) return new Error(`Failed to fetch data: ${response.statusText}`)
|
|
83
|
+
return response.json()
|
|
84
|
+
})
|
|
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
|
+
// Updates h1 and p of the entry as soon as fetched data for entry becomes available
|
|
98
|
+
document.querySelector('button.next')
|
|
99
|
+
.addEventListener('click', () => entryId.set(v => ++v))
|
|
100
|
+
// Click on button updates h1 and p of the entry as soon as fetched data for the next entry is loaded
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Effects and Batching
|
|
104
|
+
|
|
105
|
+
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
|
+
|
|
107
|
+
```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.set(v => v * 2)
|
|
118
|
+
b.set(v => v * 2)
|
|
119
|
+
}
|
|
120
|
+
))
|
|
121
|
+
// Click on button logs '14' only once (instead of first '10' and then '14' without batch)
|
|
122
|
+
```
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @name Cause & Effect
|
|
3
|
+
* @version 0.9.7
|
|
4
|
+
* @author Esther Brunner
|
|
5
|
+
*/
|
|
6
|
+
export { UNSET, State, state, isState } from './lib/state';
|
|
7
|
+
export { type Computed, computed, isComputed } from './lib/computed';
|
|
8
|
+
export { type Signal, isSignal, toSignal, batch } from './lib/signal';
|
|
9
|
+
export { effect } from './lib/effect';
|
package/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var A=(y)=>typeof y==="function",D=(y)=>A(y)&&/^async\s+/.test(y.toString()),G=(y)=>(x)=>x instanceof y,X=G(Error),O=G(Promise);var R="Computed",q=(y,x)=>{x=x??D(y);let j=[],Q,B=null,W=!0,S=()=>{if(W=!0,x)I(j)},Y={[Symbol.toStringTag]:R,get:()=>{if(x)H(j);if(!x||W)J(()=>{let F=(L)=>{Q=L,W=!1,B=null},C=(L)=>{B=X(L)?L:new Error(`Computed function failed: ${L}`)};try{let L=y(Q);O(L)?L.then(F).catch(C):F(L)}catch(L){C(L)}},S);if(X(B))throw B;return Q},map:(F)=>q(()=>F(Y.get()))};return Y},Z=(y)=>!!y&&typeof y==="object"&&y[Symbol.toStringTag]===R;var z,$=!1,N=[],P=(y)=>M(y)||Z(y),U=(y,x=!1)=>P(y)?y:A(y)?q(y,x):k(y),H=(y)=>{if(z&&!y.includes(z))y.push(z)},I=(y)=>y.forEach((x)=>$?N.push(x):x()),J=(y,x)=>{let j=z;z=x,y(),z=j},T=(y)=>{$=!0,y(),$=!1,N.forEach((x)=>x()),N.length=0};var V=Symbol();class K{y;watchers=[];constructor(y){this.value=y}get(){return H(this.watchers),this.value}set(y){if(V!==y){let x=A(y)?y(this.value):y;if(Object.is(this.value,x))return;this.value=x}if(I(this.watchers),V===y)this.watchers=[]}map(y){return q(()=>y(this.get()))}}var k=(y)=>new K(y),M=G(K);var p=(y)=>{let x=()=>J(()=>{try{y()}catch(j){console.error(j)}},x);x()};export{U as toSignal,k as state,M as isState,P as isSignal,Z as isComputed,p as effect,q as computed,T as batch,V as UNSET,K as State};
|
package/index.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @name Cause & Effect
|
|
3
|
+
* @version 0.9.7
|
|
4
|
+
* @author Esther Brunner
|
|
5
|
+
*/
|
|
6
|
+
export { UNSET, State, state, isState } from './lib/state'
|
|
7
|
+
export { type Computed, computed, isComputed } from './lib/computed'
|
|
8
|
+
export { type Signal, isSignal, toSignal, batch } from './lib/signal'
|
|
9
|
+
export { effect } from './lib/effect'
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type Computed<T> = {
|
|
2
|
+
[Symbol.toStringTag]: "Computed";
|
|
3
|
+
get: () => T;
|
|
4
|
+
map: <U>(fn: (value: T) => U) => Computed<U>;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Create a derived state from existing states
|
|
8
|
+
*
|
|
9
|
+
* @since 0.9.0
|
|
10
|
+
* @param {() => T} fn - compute function to derive state
|
|
11
|
+
* @returns {Computed<T>} result of derived state
|
|
12
|
+
*/
|
|
13
|
+
export declare const computed: <T>(fn: (v?: T) => T | Promise<T>, memo?: boolean) => Computed<T>;
|
|
14
|
+
export declare const isComputed: <T>(value: unknown) => value is Computed<T>;
|
package/lib/computed.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { type Watcher, subscribe, notify, watch } from "./signal"
|
|
2
|
+
import { isAsyncFunction, isError, isPromise } from "./util"
|
|
3
|
+
|
|
4
|
+
/* === Types === */
|
|
5
|
+
|
|
6
|
+
export type Computed<T> = {
|
|
7
|
+
[Symbol.toStringTag]: "Computed"
|
|
8
|
+
get: () => T
|
|
9
|
+
map: <U>(fn: (value: T) => U) => Computed<U>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/* === Constants === */
|
|
13
|
+
|
|
14
|
+
const TYPE_COMPUTED = 'Computed'
|
|
15
|
+
|
|
16
|
+
/* === Namespace Computed === */
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Create a derived state from existing states
|
|
20
|
+
*
|
|
21
|
+
* @since 0.9.0
|
|
22
|
+
* @param {() => T} fn - compute function to derive state
|
|
23
|
+
* @returns {Computed<T>} result of derived state
|
|
24
|
+
*/
|
|
25
|
+
export const computed = /*#__PURE__*/ <T>(
|
|
26
|
+
fn: (v?: T) => T | Promise<T>,
|
|
27
|
+
memo?: boolean
|
|
28
|
+
): Computed<T> => {
|
|
29
|
+
memo = memo ?? isAsyncFunction(fn)
|
|
30
|
+
const watchers: Watcher[] = []
|
|
31
|
+
let value: T
|
|
32
|
+
let error: Error | null = null
|
|
33
|
+
let stale = true
|
|
34
|
+
|
|
35
|
+
const mark: Watcher = () => {
|
|
36
|
+
stale = true
|
|
37
|
+
if (memo) notify(watchers)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const c: Computed<T> = {
|
|
41
|
+
[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
|
|
65
|
+
return value
|
|
66
|
+
},
|
|
67
|
+
map: <U>(fn: (value: T) => U): Computed<U> =>
|
|
68
|
+
computed(() => fn(c.get())),
|
|
69
|
+
}
|
|
70
|
+
return c
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const isComputed = /*#__PURE__*/ <T>(value: unknown): value is Computed<T> =>
|
|
74
|
+
!!value && typeof value === 'object'
|
|
75
|
+
&& (value as { [key in typeof Symbol.toStringTag]: string })[Symbol.toStringTag] === TYPE_COMPUTED
|
package/lib/effect.d.ts
ADDED
package/lib/effect.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
|
|
2
|
+
import { type Watcher, watch } from "./signal"
|
|
3
|
+
|
|
4
|
+
/* === Exported Function === */
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Define what happens when a reactive state changes
|
|
8
|
+
*
|
|
9
|
+
* @since 0.1.0
|
|
10
|
+
* @param {() => void} fn - callback function to be executed when a state changes
|
|
11
|
+
*/
|
|
12
|
+
export const effect = (fn: () => void) => {
|
|
13
|
+
const run: Watcher = () => watch(() => {
|
|
14
|
+
try {
|
|
15
|
+
fn()
|
|
16
|
+
} catch (error) {
|
|
17
|
+
console.error(error)
|
|
18
|
+
}
|
|
19
|
+
}, run)
|
|
20
|
+
run()
|
|
21
|
+
}
|
package/lib/signal.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type State } from "./state";
|
|
2
|
+
import { type Computed } from "./computed";
|
|
3
|
+
type Signal<T> = State<T> | Computed<T>;
|
|
4
|
+
type MaybeSignal<T> = State<T> | Computed<T> | T;
|
|
5
|
+
type Watcher = () => void;
|
|
6
|
+
/**
|
|
7
|
+
* Check whether a value is a Signal or not
|
|
8
|
+
*
|
|
9
|
+
* @since 0.9.0
|
|
10
|
+
* @param {any} value - value to check
|
|
11
|
+
* @returns {boolean} - true if value is a Signal, false otherwise
|
|
12
|
+
*/
|
|
13
|
+
declare const isSignal: <T>(value: any) => value is Signal<T>;
|
|
14
|
+
/**
|
|
15
|
+
* Convert a value to a Signal if it's not already a Signal
|
|
16
|
+
*
|
|
17
|
+
* @since 0.9.6
|
|
18
|
+
* @param {MaybeSignal<T>} value - value to convert to a Signal
|
|
19
|
+
* @param memo
|
|
20
|
+
* @returns {Signal<T>} - converted Signal
|
|
21
|
+
*/
|
|
22
|
+
declare const toSignal: <T>(value: MaybeSignal<T>, memo?: boolean) => Signal<T>;
|
|
23
|
+
/**
|
|
24
|
+
* Add notify function of active watchers to the set of watchers
|
|
25
|
+
*
|
|
26
|
+
* @param {Watcher[]} watchers - set of current watchers
|
|
27
|
+
*/
|
|
28
|
+
declare const subscribe: (watchers: Watcher[]) => void;
|
|
29
|
+
/**
|
|
30
|
+
* Notify all subscribers of the state change or add to the pending set if batching is enabled
|
|
31
|
+
*
|
|
32
|
+
* @param {Watcher[]} watchers
|
|
33
|
+
*/
|
|
34
|
+
declare const notify: (watchers: Watcher[]) => void;
|
|
35
|
+
/**
|
|
36
|
+
* Run a function in a reactive context
|
|
37
|
+
*
|
|
38
|
+
* @param {() => void} run - function to run the computation or effect
|
|
39
|
+
* @param {Watcher} mark - function to be called when the state changes
|
|
40
|
+
*/
|
|
41
|
+
declare const watch: (run: () => void, mark: Watcher) => void;
|
|
42
|
+
/**
|
|
43
|
+
* Batch multiple state changes into a single update
|
|
44
|
+
*
|
|
45
|
+
* @param {() => void} run - function to run the batch of state changes
|
|
46
|
+
*/
|
|
47
|
+
declare const batch: (run: () => void) => void;
|
|
48
|
+
export { type Signal, type Watcher, isSignal, toSignal, subscribe, notify, watch, batch };
|
package/lib/signal.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { type State, isState, state } from "./state"
|
|
2
|
+
import { computed, type Computed, isComputed } from "./computed"
|
|
3
|
+
import { isFunction } from "./util"
|
|
4
|
+
|
|
5
|
+
/* === Types === */
|
|
6
|
+
|
|
7
|
+
type Signal<T> = State<T> | Computed<T>
|
|
8
|
+
|
|
9
|
+
type MaybeSignal<T> = State<T> | Computed<T> | T
|
|
10
|
+
|
|
11
|
+
type Watcher = () => void
|
|
12
|
+
|
|
13
|
+
/* === Internals === */
|
|
14
|
+
|
|
15
|
+
// Currently active watcher
|
|
16
|
+
let active: () => void | undefined
|
|
17
|
+
|
|
18
|
+
// Batching state
|
|
19
|
+
let batching = false
|
|
20
|
+
|
|
21
|
+
// Pending notifications
|
|
22
|
+
const pending: Watcher[] = []
|
|
23
|
+
|
|
24
|
+
/* === Exported Functions === */
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Check whether a value is a Signal or not
|
|
28
|
+
*
|
|
29
|
+
* @since 0.9.0
|
|
30
|
+
* @param {any} value - value to check
|
|
31
|
+
* @returns {boolean} - true if value is a Signal, false otherwise
|
|
32
|
+
*/
|
|
33
|
+
const isSignal = /*#__PURE__*/ <T>(value: any): value is Signal<T> =>
|
|
34
|
+
isState(value) || isComputed(value)
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Convert a value to a Signal if it's not already a Signal
|
|
38
|
+
*
|
|
39
|
+
* @since 0.9.6
|
|
40
|
+
* @param {MaybeSignal<T>} value - value to convert to a Signal
|
|
41
|
+
* @param memo
|
|
42
|
+
* @returns {Signal<T>} - converted Signal
|
|
43
|
+
*/
|
|
44
|
+
const toSignal = /*#__PURE__*/ <T>(
|
|
45
|
+
value: MaybeSignal<T>,
|
|
46
|
+
memo: boolean = false
|
|
47
|
+
): Signal<T> =>
|
|
48
|
+
isSignal<T>(value) ? value
|
|
49
|
+
: isFunction(value) ? computed(value, memo)
|
|
50
|
+
: state(value)
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Add notify function of active watchers to the set of watchers
|
|
54
|
+
*
|
|
55
|
+
* @param {Watcher[]} watchers - set of current watchers
|
|
56
|
+
*/
|
|
57
|
+
const subscribe = (watchers: Watcher[]) => {
|
|
58
|
+
if (active && !watchers.includes(active)) watchers.push(active)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Notify all subscribers of the state change or add to the pending set if batching is enabled
|
|
63
|
+
*
|
|
64
|
+
* @param {Watcher[]} watchers
|
|
65
|
+
*/
|
|
66
|
+
const notify = (watchers: Watcher[]) =>
|
|
67
|
+
watchers.forEach(n => batching ? pending.push(n) : n())
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Run a function in a reactive context
|
|
71
|
+
*
|
|
72
|
+
* @param {() => void} run - function to run the computation or effect
|
|
73
|
+
* @param {Watcher} mark - function to be called when the state changes
|
|
74
|
+
*/
|
|
75
|
+
const watch = (run: () => void, mark: Watcher): void => {
|
|
76
|
+
const prev = active
|
|
77
|
+
active = mark
|
|
78
|
+
run()
|
|
79
|
+
active = prev
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Batch multiple state changes into a single update
|
|
84
|
+
*
|
|
85
|
+
* @param {() => void} run - function to run the batch of state changes
|
|
86
|
+
*/
|
|
87
|
+
const batch = (run: () => void): void => {
|
|
88
|
+
batching = true
|
|
89
|
+
run()
|
|
90
|
+
batching = false
|
|
91
|
+
pending.forEach(n => n())
|
|
92
|
+
pending.length = 0
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export {
|
|
96
|
+
type Signal, type Watcher,
|
|
97
|
+
isSignal, toSignal, subscribe, notify, watch, batch
|
|
98
|
+
}
|
package/lib/state.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type Computed } from "./computed";
|
|
2
|
+
export declare const UNSET: any;
|
|
3
|
+
/**
|
|
4
|
+
* Define a reactive state
|
|
5
|
+
*
|
|
6
|
+
* @since 0.9.0
|
|
7
|
+
* @class State
|
|
8
|
+
*/
|
|
9
|
+
export declare class State<T> {
|
|
10
|
+
private value;
|
|
11
|
+
private watchers;
|
|
12
|
+
constructor(value: T);
|
|
13
|
+
/**
|
|
14
|
+
* Get the current value of the state
|
|
15
|
+
*
|
|
16
|
+
* @method of State<T>
|
|
17
|
+
* @returns {T} - current value of the state
|
|
18
|
+
*/
|
|
19
|
+
get(): T;
|
|
20
|
+
/**
|
|
21
|
+
* Set a new value of the state
|
|
22
|
+
*
|
|
23
|
+
* @method of State<T>
|
|
24
|
+
* @param {T | ((v: T) => T)} value
|
|
25
|
+
* @returns {void}
|
|
26
|
+
*/
|
|
27
|
+
set(value: T | ((v: T) => T)): void;
|
|
28
|
+
map<U>(fn: (value: T) => U): Computed<U>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Create a new state signal
|
|
32
|
+
*
|
|
33
|
+
* @static method of State<T>
|
|
34
|
+
* @param {T} value - initial value of the state
|
|
35
|
+
* @returns {State<T>} - new state signal
|
|
36
|
+
*/
|
|
37
|
+
export declare const state: <T>(value: T) => State<T>;
|
|
38
|
+
export declare const isState: (value: unknown) => value is State<any>;
|
package/lib/state.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { isFunction, isInstanceOf } from "./util"
|
|
2
|
+
import { type Watcher, subscribe, notify } from "./signal"
|
|
3
|
+
import { type Computed, computed } from "./computed"
|
|
4
|
+
|
|
5
|
+
/* === Constants === */
|
|
6
|
+
|
|
7
|
+
export const UNSET: any = Symbol()
|
|
8
|
+
|
|
9
|
+
/* === Class State === */
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Define a reactive state
|
|
13
|
+
*
|
|
14
|
+
* @since 0.9.0
|
|
15
|
+
* @class State
|
|
16
|
+
*/
|
|
17
|
+
export class State<T> {
|
|
18
|
+
private watchers: Watcher[] = []
|
|
19
|
+
|
|
20
|
+
constructor(private value: T) {}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get the current value of the state
|
|
24
|
+
*
|
|
25
|
+
* @method of State<T>
|
|
26
|
+
* @returns {T} - current value of the state
|
|
27
|
+
*/
|
|
28
|
+
get(): T {
|
|
29
|
+
subscribe(this.watchers)
|
|
30
|
+
return this.value
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Set a new value of the state
|
|
35
|
+
*
|
|
36
|
+
* @method of State<T>
|
|
37
|
+
* @param {T | ((v: T) => T)} value
|
|
38
|
+
* @returns {void}
|
|
39
|
+
*/
|
|
40
|
+
set(value: T | ((v: T) => T)): void {
|
|
41
|
+
if (UNSET !== value) {
|
|
42
|
+
const newValue = isFunction(value) ? value(this.value) : value
|
|
43
|
+
if (Object.is(this.value, newValue)) return
|
|
44
|
+
this.value = newValue
|
|
45
|
+
}
|
|
46
|
+
notify(this.watchers)
|
|
47
|
+
|
|
48
|
+
// Setting to null clears the watchers so the signal can be garbage collected
|
|
49
|
+
if (UNSET === value) this.watchers = []
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
map<U>(fn: (value: T) => U): Computed<U> {
|
|
53
|
+
return computed<U>(() => fn(this.get()))
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Create a new state signal
|
|
59
|
+
*
|
|
60
|
+
* @static method of State<T>
|
|
61
|
+
* @param {T} value - initial value of the state
|
|
62
|
+
* @returns {State<T>} - new state signal
|
|
63
|
+
*/
|
|
64
|
+
export const state = /*#__PURE__*/ <T>(value: T): State<T> =>
|
|
65
|
+
new State(value)
|
|
66
|
+
|
|
67
|
+
export const isState = /*#__PURE__*/ isInstanceOf(State)
|
package/lib/util.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
declare const isFunction: (value: unknown) => value is (...args: any[]) => any;
|
|
2
|
+
declare const isAsyncFunction: (value: unknown) => value is (...args: any[]) => Promise<any> | PromiseLike<any>;
|
|
3
|
+
declare const isInstanceOf: <T>(type: new (...args: any[]) => T) => (value: unknown) => value is T;
|
|
4
|
+
declare const isError: (value: unknown) => value is Error;
|
|
5
|
+
declare const isPromise: (value: unknown) => value is Promise<unknown>;
|
|
6
|
+
export { isFunction, isAsyncFunction, isInstanceOf, isError, isPromise };
|
package/lib/util.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/* === Utility Functions === */
|
|
2
|
+
|
|
3
|
+
const isFunction = /*#__PURE__*/ (value: unknown): value is (...args: any[]) => any =>
|
|
4
|
+
typeof value === 'function'
|
|
5
|
+
|
|
6
|
+
const isAsyncFunction = /*#__PURE__*/ (value: unknown): value is (...args: any[]) => Promise<any> | PromiseLike<any> =>
|
|
7
|
+
isFunction(value) && /^async\s+/.test(value.toString())
|
|
8
|
+
|
|
9
|
+
const isInstanceOf = /*#__PURE__*/ <T>(type: new (...args: any[]) => T) =>
|
|
10
|
+
(value: unknown): value is T =>
|
|
11
|
+
value instanceof type
|
|
12
|
+
|
|
13
|
+
const isError = /*#__PURE__*/ isInstanceOf(Error)
|
|
14
|
+
const isPromise = /*#__PURE__*/ isInstanceOf(Promise)
|
|
15
|
+
|
|
16
|
+
export { isFunction, isAsyncFunction, isInstanceOf, isError, isPromise }
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zeix/cause-effect",
|
|
3
|
+
"version": "0.9.7",
|
|
4
|
+
"author": "Esther Brunner",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"module": "index.ts",
|
|
7
|
+
"devDependencies": {
|
|
8
|
+
"@types/bun": "latest"
|
|
9
|
+
},
|
|
10
|
+
"peerDependencies": {
|
|
11
|
+
"typescript": "^5.6.3"
|
|
12
|
+
},
|
|
13
|
+
"description": "Cause & Effect - reactive state management with signals.",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"keywords": [
|
|
16
|
+
"Cause & Effect",
|
|
17
|
+
"Reactivity",
|
|
18
|
+
"Signals",
|
|
19
|
+
"Effects"
|
|
20
|
+
],
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "bun build index.ts --outdir ./ --minify && bunx tsc",
|
|
26
|
+
"test": "bun test"
|
|
27
|
+
},
|
|
28
|
+
"type": "module",
|
|
29
|
+
"types": "index.d.ts"
|
|
30
|
+
}
|