@zeix/cause-effect 0.14.0 → 0.14.2
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 +68 -72
- package/biome.json +35 -0
- package/index.d.ts +6 -8
- package/index.dev.js +294 -0
- package/index.js +1 -1
- package/index.ts +26 -14
- package/package.json +5 -6
- package/src/computed.d.ts +11 -9
- package/src/computed.ts +138 -14
- package/src/effect.d.ts +7 -9
- package/src/effect.ts +43 -58
- package/src/scheduler.d.ts +15 -6
- package/src/scheduler.ts +39 -16
- package/src/signal.d.ts +5 -10
- package/src/signal.ts +10 -15
- package/src/state.d.ts +2 -1
- package/src/state.ts +1 -1
- package/src/util.d.ts +1 -5
- package/src/util.ts +3 -23
- package/test/batch.test.ts +13 -13
- package/test/benchmark.test.ts +79 -43
- package/test/computed.test.ts +76 -78
- package/test/effect.test.ts +24 -24
- package/test/state.test.ts +33 -33
- package/test/util/framework-types.ts +2 -2
- package/test/util/perf-tests.ts +2 -2
- package/test/util/reactive-framework.ts +1 -1
- package/src/memo.d.ts +0 -13
- package/src/memo.ts +0 -91
- package/src/task.d.ts +0 -17
- package/src/task.ts +0 -153
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Cause & Effect
|
|
2
2
|
|
|
3
|
-
Version 0.14.
|
|
3
|
+
Version 0.14.2
|
|
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
|
|
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,
|
|
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 =
|
|
35
|
+
const greeting = computed(() => `Hello ${user.get().name}!`)
|
|
36
36
|
|
|
37
37
|
// 3. React to changes
|
|
38
|
-
effect({
|
|
39
|
-
|
|
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
|
|
76
|
+
### Computed Signals vs. Functions
|
|
80
77
|
|
|
81
|
-
####
|
|
78
|
+
#### When to Use Computed Signals
|
|
82
79
|
|
|
83
|
-
`
|
|
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,
|
|
83
|
+
import { state, computed, effect } from '@zeix/cause-effect'
|
|
87
84
|
|
|
88
85
|
const count = state(42)
|
|
89
|
-
const
|
|
90
|
-
effect(() => console.log(
|
|
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 '
|
|
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
|
-
|
|
104
|
+
**When to use which approach:**
|
|
99
105
|
|
|
100
|
-
|
|
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
|
-
|
|
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,
|
|
123
|
+
import { state, computed, effect } from '@zeix/cause-effect'
|
|
106
124
|
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
|
|
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
|
-
//
|
|
133
|
+
// Handle all possible states
|
|
115
134
|
effect({
|
|
116
|
-
signals: [
|
|
117
|
-
ok:
|
|
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
|
-
//
|
|
141
|
+
// When id changes, the previous request is automatically canceled
|
|
123
142
|
document.querySelector('button.next').addEventListener('click', () => {
|
|
124
|
-
|
|
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 (
|
|
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,
|
|
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 =
|
|
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 =>
|
|
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,
|
|
309
|
+
import { state, computed, effect } from '@zeix/cause-effect'
|
|
286
310
|
|
|
287
311
|
const user = state({ name: 'Alice', age: 30 })
|
|
288
|
-
const greeting =
|
|
289
|
-
const cleanup = effect({
|
|
290
|
-
|
|
291
|
-
|
|
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
|
|
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/biome.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/2.1.4/schema.json",
|
|
3
|
+
"vcs": {
|
|
4
|
+
"enabled": false,
|
|
5
|
+
"clientKind": "git",
|
|
6
|
+
"useIgnoreFile": false
|
|
7
|
+
},
|
|
8
|
+
"files": {
|
|
9
|
+
"ignoreUnknown": false,
|
|
10
|
+
"includes": ["**", "!index.js", "!index.dev.js", "!**/*.d.ts"]
|
|
11
|
+
},
|
|
12
|
+
"formatter": {
|
|
13
|
+
"enabled": true,
|
|
14
|
+
"indentStyle": "tab"
|
|
15
|
+
},
|
|
16
|
+
"linter": {
|
|
17
|
+
"enabled": true,
|
|
18
|
+
"rules": {
|
|
19
|
+
"recommended": true
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"javascript": {
|
|
23
|
+
"formatter": {
|
|
24
|
+
"quoteStyle": "double"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"assist": {
|
|
28
|
+
"enabled": true,
|
|
29
|
+
"actions": {
|
|
30
|
+
"source": {
|
|
31
|
+
"organizeImports": "on"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
package/index.d.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @name Cause & Effect
|
|
3
|
-
* @version 0.14.
|
|
3
|
+
* @version 0.14.2
|
|
4
4
|
* @author Esther Brunner
|
|
5
5
|
*/
|
|
6
|
-
export {
|
|
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 { type Computed, type ComputedCallback, computed, isComputed, TYPE_COMPUTED, } from './src/computed';
|
|
12
7
|
export { type EffectMatcher, effect } from './src/effect';
|
|
13
|
-
export { batch,
|
|
8
|
+
export { batch, type Cleanup, enqueue, flush, notify, observe, subscribe, type Updater, type Watcher, watch, } from './src/scheduler';
|
|
9
|
+
export { isComputedCallback, isSignal, type MaybeSignal, type Signal, type SignalValues, toSignal, UNSET, } from './src/signal';
|
|
10
|
+
export { isState, type State, state, TYPE_STATE } from './src/state';
|
|
11
|
+
export { CircularDependencyError, isFunction } from './src/util';
|
package/index.dev.js
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// src/scheduler.ts
|
|
2
|
+
var active;
|
|
3
|
+
var pending = new Set;
|
|
4
|
+
var batchDepth = 0;
|
|
5
|
+
var updateMap = new Map;
|
|
6
|
+
var requestId;
|
|
7
|
+
var updateDOM = () => {
|
|
8
|
+
requestId = undefined;
|
|
9
|
+
const updates = Array.from(updateMap.values());
|
|
10
|
+
updateMap.clear();
|
|
11
|
+
for (const update of updates) {
|
|
12
|
+
update();
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var requestTick = () => {
|
|
16
|
+
if (requestId)
|
|
17
|
+
cancelAnimationFrame(requestId);
|
|
18
|
+
requestId = requestAnimationFrame(updateDOM);
|
|
19
|
+
};
|
|
20
|
+
queueMicrotask(updateDOM);
|
|
21
|
+
var watch = (notice) => {
|
|
22
|
+
const cleanups = new Set;
|
|
23
|
+
const w = notice;
|
|
24
|
+
w.off = (on) => {
|
|
25
|
+
cleanups.add(on);
|
|
26
|
+
};
|
|
27
|
+
w.cleanup = () => {
|
|
28
|
+
for (const cleanup of cleanups) {
|
|
29
|
+
cleanup();
|
|
30
|
+
}
|
|
31
|
+
cleanups.clear();
|
|
32
|
+
};
|
|
33
|
+
return w;
|
|
34
|
+
};
|
|
35
|
+
var subscribe = (watchers) => {
|
|
36
|
+
if (active && !watchers.has(active)) {
|
|
37
|
+
const watcher = active;
|
|
38
|
+
watchers.add(watcher);
|
|
39
|
+
active.off(() => {
|
|
40
|
+
watchers.delete(watcher);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
var notify = (watchers) => {
|
|
45
|
+
for (const watcher of watchers) {
|
|
46
|
+
if (batchDepth)
|
|
47
|
+
pending.add(watcher);
|
|
48
|
+
else
|
|
49
|
+
watcher();
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var flush = () => {
|
|
53
|
+
while (pending.size) {
|
|
54
|
+
const watchers = Array.from(pending);
|
|
55
|
+
pending.clear();
|
|
56
|
+
for (const watcher of watchers) {
|
|
57
|
+
watcher();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
var batch = (fn) => {
|
|
62
|
+
batchDepth++;
|
|
63
|
+
try {
|
|
64
|
+
fn();
|
|
65
|
+
} finally {
|
|
66
|
+
flush();
|
|
67
|
+
batchDepth--;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
var observe = (run, watcher) => {
|
|
71
|
+
const prev = active;
|
|
72
|
+
active = watcher;
|
|
73
|
+
try {
|
|
74
|
+
run();
|
|
75
|
+
} finally {
|
|
76
|
+
active = prev;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
var enqueue = (fn, dedupe) => new Promise((resolve, reject) => {
|
|
80
|
+
updateMap.set(dedupe || Symbol(), () => {
|
|
81
|
+
try {
|
|
82
|
+
resolve(fn());
|
|
83
|
+
} catch (error) {
|
|
84
|
+
reject(error);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
requestTick();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// src/util.ts
|
|
91
|
+
var isFunction = (value) => typeof value === "function";
|
|
92
|
+
var isObjectOfType = (value, type) => Object.prototype.toString.call(value) === `[object ${type}]`;
|
|
93
|
+
var toError = (reason) => reason instanceof Error ? reason : Error(String(reason));
|
|
94
|
+
|
|
95
|
+
class CircularDependencyError extends Error {
|
|
96
|
+
constructor(where) {
|
|
97
|
+
super(`Circular dependency in ${where} detected`);
|
|
98
|
+
this.name = "CircularDependencyError";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/state.ts
|
|
103
|
+
var TYPE_STATE = "State";
|
|
104
|
+
var state = (initialValue) => {
|
|
105
|
+
const watchers = new Set;
|
|
106
|
+
let value = initialValue;
|
|
107
|
+
const s = {
|
|
108
|
+
[Symbol.toStringTag]: TYPE_STATE,
|
|
109
|
+
get: () => {
|
|
110
|
+
subscribe(watchers);
|
|
111
|
+
return value;
|
|
112
|
+
},
|
|
113
|
+
set: (v) => {
|
|
114
|
+
if (Object.is(value, v))
|
|
115
|
+
return;
|
|
116
|
+
value = v;
|
|
117
|
+
notify(watchers);
|
|
118
|
+
if (UNSET === value)
|
|
119
|
+
watchers.clear();
|
|
120
|
+
},
|
|
121
|
+
update: (fn) => {
|
|
122
|
+
s.set(fn(value));
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
return s;
|
|
126
|
+
};
|
|
127
|
+
var isState = (value) => isObjectOfType(value, TYPE_STATE);
|
|
128
|
+
|
|
129
|
+
// src/signal.ts
|
|
130
|
+
var UNSET = Symbol();
|
|
131
|
+
var isSignal = (value) => isState(value) || isComputed(value);
|
|
132
|
+
var toSignal = (value) => isSignal(value) ? value : isComputedCallback(value) ? computed(value) : state(value);
|
|
133
|
+
|
|
134
|
+
// src/computed.ts
|
|
135
|
+
var TYPE_COMPUTED = "Computed";
|
|
136
|
+
var computed = (fn) => {
|
|
137
|
+
const watchers = new Set;
|
|
138
|
+
let value = UNSET;
|
|
139
|
+
let error;
|
|
140
|
+
let controller;
|
|
141
|
+
let dirty = true;
|
|
142
|
+
let changed = false;
|
|
143
|
+
let computing = false;
|
|
144
|
+
const ok = (v) => {
|
|
145
|
+
if (!Object.is(v, value)) {
|
|
146
|
+
value = v;
|
|
147
|
+
changed = true;
|
|
148
|
+
}
|
|
149
|
+
error = undefined;
|
|
150
|
+
dirty = false;
|
|
151
|
+
};
|
|
152
|
+
const nil = () => {
|
|
153
|
+
changed = UNSET !== value;
|
|
154
|
+
value = UNSET;
|
|
155
|
+
error = undefined;
|
|
156
|
+
};
|
|
157
|
+
const err = (e) => {
|
|
158
|
+
const newError = toError(e);
|
|
159
|
+
changed = !error || newError.name !== error.name || newError.message !== error.message;
|
|
160
|
+
value = UNSET;
|
|
161
|
+
error = newError;
|
|
162
|
+
};
|
|
163
|
+
const settle = (settleFn) => (arg) => {
|
|
164
|
+
computing = false;
|
|
165
|
+
controller = undefined;
|
|
166
|
+
settleFn(arg);
|
|
167
|
+
if (changed)
|
|
168
|
+
notify(watchers);
|
|
169
|
+
};
|
|
170
|
+
const mark = watch(() => {
|
|
171
|
+
dirty = true;
|
|
172
|
+
controller?.abort("Aborted because source signal changed");
|
|
173
|
+
if (watchers.size)
|
|
174
|
+
notify(watchers);
|
|
175
|
+
else
|
|
176
|
+
mark.cleanup();
|
|
177
|
+
});
|
|
178
|
+
const compute = () => observe(() => {
|
|
179
|
+
if (computing)
|
|
180
|
+
throw new CircularDependencyError("computed");
|
|
181
|
+
changed = false;
|
|
182
|
+
if (isFunction(fn) && fn.constructor.name === "AsyncFunction") {
|
|
183
|
+
if (controller)
|
|
184
|
+
return value;
|
|
185
|
+
controller = new AbortController;
|
|
186
|
+
controller.signal.addEventListener("abort", () => {
|
|
187
|
+
computing = false;
|
|
188
|
+
controller = undefined;
|
|
189
|
+
compute();
|
|
190
|
+
}, {
|
|
191
|
+
once: true
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
let result;
|
|
195
|
+
computing = true;
|
|
196
|
+
try {
|
|
197
|
+
result = controller ? fn(controller.signal) : fn();
|
|
198
|
+
} catch (e) {
|
|
199
|
+
if (e instanceof DOMException && e.name === "AbortError")
|
|
200
|
+
nil();
|
|
201
|
+
else
|
|
202
|
+
err(e);
|
|
203
|
+
computing = false;
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (result instanceof Promise)
|
|
207
|
+
result.then(settle(ok), settle(err));
|
|
208
|
+
else if (result == null || UNSET === result)
|
|
209
|
+
nil();
|
|
210
|
+
else
|
|
211
|
+
ok(result);
|
|
212
|
+
computing = false;
|
|
213
|
+
}, mark);
|
|
214
|
+
const c = {
|
|
215
|
+
[Symbol.toStringTag]: TYPE_COMPUTED,
|
|
216
|
+
get: () => {
|
|
217
|
+
subscribe(watchers);
|
|
218
|
+
flush();
|
|
219
|
+
if (dirty)
|
|
220
|
+
compute();
|
|
221
|
+
if (error)
|
|
222
|
+
throw error;
|
|
223
|
+
return value;
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
return c;
|
|
227
|
+
};
|
|
228
|
+
var isComputed = (value) => isObjectOfType(value, TYPE_COMPUTED);
|
|
229
|
+
var isComputedCallback = (value) => isFunction(value) && value.length < 2;
|
|
230
|
+
// src/effect.ts
|
|
231
|
+
function effect(matcher) {
|
|
232
|
+
const {
|
|
233
|
+
signals,
|
|
234
|
+
ok,
|
|
235
|
+
err = (error) => {
|
|
236
|
+
console.error(error);
|
|
237
|
+
},
|
|
238
|
+
nil = () => {
|
|
239
|
+
}
|
|
240
|
+
} = isFunction(matcher) ? { signals: [], ok: matcher } : matcher;
|
|
241
|
+
let running = false;
|
|
242
|
+
const run = watch(() => observe(() => {
|
|
243
|
+
if (running)
|
|
244
|
+
throw new CircularDependencyError("effect");
|
|
245
|
+
running = true;
|
|
246
|
+
const errors = [];
|
|
247
|
+
let pending2 = false;
|
|
248
|
+
const values = signals.map((signal) => {
|
|
249
|
+
try {
|
|
250
|
+
const value = signal.get();
|
|
251
|
+
if (value === UNSET)
|
|
252
|
+
pending2 = true;
|
|
253
|
+
return value;
|
|
254
|
+
} catch (e) {
|
|
255
|
+
errors.push(toError(e));
|
|
256
|
+
return UNSET;
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
let cleanup;
|
|
260
|
+
try {
|
|
261
|
+
cleanup = pending2 ? nil() : errors.length ? err(...errors) : ok(...values);
|
|
262
|
+
} catch (e) {
|
|
263
|
+
cleanup = err(toError(e));
|
|
264
|
+
} finally {
|
|
265
|
+
if (isFunction(cleanup))
|
|
266
|
+
run.off(cleanup);
|
|
267
|
+
}
|
|
268
|
+
running = false;
|
|
269
|
+
}, run));
|
|
270
|
+
run();
|
|
271
|
+
return () => run.cleanup();
|
|
272
|
+
}
|
|
273
|
+
export {
|
|
274
|
+
watch,
|
|
275
|
+
toSignal,
|
|
276
|
+
subscribe,
|
|
277
|
+
state,
|
|
278
|
+
observe,
|
|
279
|
+
notify,
|
|
280
|
+
isState,
|
|
281
|
+
isSignal,
|
|
282
|
+
isFunction,
|
|
283
|
+
isComputedCallback,
|
|
284
|
+
isComputed,
|
|
285
|
+
flush,
|
|
286
|
+
enqueue,
|
|
287
|
+
effect,
|
|
288
|
+
computed,
|
|
289
|
+
batch,
|
|
290
|
+
UNSET,
|
|
291
|
+
TYPE_STATE,
|
|
292
|
+
TYPE_COMPUTED,
|
|
293
|
+
CircularDependencyError
|
|
294
|
+
};
|
package/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var V,M=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 Y=($)=>{let B=new Set,W=$;return W.off=(z)=>{B.add(z)},W.cleanup=()=>{for(let z of B)z();B.clear()},W},I=($)=>{if(V&&!$.has(V)){let B=V;$.add(B),V.off(()=>{$.delete(B)})}},F=($)=>{for(let B of $)if(U)M.add(B);else B()},N=()=>{while(M.size){let $=Array.from(M);M.clear();for(let B of $)B()}},h=($)=>{U++;try{$()}finally{N(),U--}},O=($,B)=>{let W=V;V=B;try{$()}finally{V=W}},v=($,B)=>new Promise((W,z)=>{k.set(B||Symbol(),()=>{try{W($())}catch(G){z(G)}}),d()});var j=($)=>typeof $==="function",P=($,B)=>Object.prototype.toString.call($)===`[object ${B}]`,C=($)=>$ instanceof Error?$:Error(String($));class R extends Error{constructor($){super(`Circular dependency in ${$} detected`);this.name="CircularDependencyError"}}var _="State",S=($)=>{let B=new Set,W=$,z={[Symbol.toStringTag]:_,get:()=>{return I(B),W},set:(G)=>{if(Object.is(W,G))return;if(W=G,F(B),K===W)B.clear()},update:(G)=>{z.set(G(W))}};return z},T=($)=>P($,_);var K=Symbol(),p=($)=>T($)||b($),o=($)=>p($)?$:g($)?E($):S($);var m="Computed",E=($)=>{let B=new Set,W=K,z,G,X=!0,L=!1,J=!1,x=(H)=>{if(!Object.is(H,W))W=H,L=!0;z=void 0,X=!1},q=()=>{L=K!==W,W=K,z=void 0},Z=(H)=>{let Q=C(H);L=!z||Q.name!==z.name||Q.message!==z.message,W=K,z=Q},A=(H)=>(Q)=>{if(J=!1,G=void 0,H(Q),L)F(B)},y=Y(()=>{if(X=!0,G?.abort("Aborted because source signal changed"),B.size)F(B);else y.cleanup()}),w=()=>O(()=>{if(J)throw new R("computed");if(L=!1,j($)&&$.constructor.name==="AsyncFunction"){if(G)return W;G=new AbortController,G.signal.addEventListener("abort",()=>{J=!1,G=void 0,w()},{once:!0})}let H;J=!0;try{H=G?$(G.signal):$()}catch(Q){if(Q instanceof DOMException&&Q.name==="AbortError")q();else Z(Q);J=!1;return}if(H instanceof Promise)H.then(A(x),A(Z));else if(H==null||K===H)q();else x(H);J=!1},y);return{[Symbol.toStringTag]:m,get:()=>{if(I(B),N(),X)w();if(z)throw z;return W}}},b=($)=>P($,m),g=($)=>j($)&&$.length<2;function s($){let{signals:B,ok:W,err:z=(J)=>{console.error(J)},nil:G=()=>{}}=j($)?{signals:[],ok:$}:$,X=!1,L=Y(()=>O(()=>{if(X)throw new R("effect");X=!0;let J=[],x=!1,q=B.map((A)=>{try{let y=A.get();if(y===K)x=!0;return y}catch(y){return J.push(C(y)),K}}),Z;try{Z=x?G():J.length?z(...J):W(...q)}catch(A){Z=z(C(A))}finally{if(j(Z))L.off(Z)}X=!1},L));return L(),()=>L.cleanup()}export{Y as watch,o as toSignal,I as subscribe,S as state,O as observe,F as notify,T as isState,p as isSignal,j as isFunction,g as isComputedCallback,b as isComputed,N as flush,v as enqueue,s as effect,E as computed,h as batch,K as UNSET,_ as TYPE_STATE,m as TYPE_COMPUTED,R as CircularDependencyError};
|
package/index.ts
CHANGED
|
@@ -1,25 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @name Cause & Effect
|
|
3
|
-
* @version 0.14.
|
|
3
|
+
* @version 0.14.2
|
|
4
4
|
* @author Esther Brunner
|
|
5
5
|
*/
|
|
6
|
-
|
|
7
|
-
export {
|
|
8
|
-
type Signal,
|
|
9
|
-
type MaybeSignal,
|
|
10
|
-
UNSET,
|
|
11
|
-
isSignal,
|
|
12
|
-
isComputedCallback,
|
|
13
|
-
toSignal,
|
|
14
|
-
} from './src/signal'
|
|
15
|
-
export { type State, state, isState } from './src/state'
|
|
6
|
+
|
|
16
7
|
export {
|
|
17
8
|
type Computed,
|
|
18
9
|
type ComputedCallback,
|
|
19
10
|
computed,
|
|
20
11
|
isComputed,
|
|
12
|
+
TYPE_COMPUTED,
|
|
21
13
|
} from './src/computed'
|
|
22
|
-
export { type MemoCallback, memo } from './src/memo'
|
|
23
|
-
export { type TaskCallback, task } from './src/task'
|
|
24
14
|
export { type EffectMatcher, effect } from './src/effect'
|
|
25
|
-
export {
|
|
15
|
+
export {
|
|
16
|
+
batch,
|
|
17
|
+
type Cleanup,
|
|
18
|
+
enqueue,
|
|
19
|
+
flush,
|
|
20
|
+
notify,
|
|
21
|
+
observe,
|
|
22
|
+
subscribe,
|
|
23
|
+
type Updater,
|
|
24
|
+
type Watcher,
|
|
25
|
+
watch,
|
|
26
|
+
} from './src/scheduler'
|
|
27
|
+
export {
|
|
28
|
+
isComputedCallback,
|
|
29
|
+
isSignal,
|
|
30
|
+
type MaybeSignal,
|
|
31
|
+
type Signal,
|
|
32
|
+
type SignalValues,
|
|
33
|
+
toSignal,
|
|
34
|
+
UNSET,
|
|
35
|
+
} from './src/signal'
|
|
36
|
+
export { isState, type State, state, TYPE_STATE } from './src/state'
|
|
37
|
+
export { CircularDependencyError, isFunction } from './src/util'
|