@zeix/cause-effect 0.13.1 → 0.14.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 +161 -131
- package/eslint.config.js +35 -0
- package/index.d.ts +9 -7
- package/index.js +1 -1
- package/index.ts +20 -10
- package/package.json +32 -29
- package/src/computed.d.ts +31 -0
- package/src/computed.ts +54 -0
- package/src/effect.d.ts +19 -0
- package/src/effect.ts +95 -0
- package/src/memo.d.ts +13 -0
- package/src/memo.ts +91 -0
- package/{lib → src}/scheduler.d.ts +15 -11
- package/{lib → src}/scheduler.ts +60 -48
- package/src/signal.d.ts +31 -0
- package/src/signal.ts +69 -0
- package/{lib → src}/state.d.ts +4 -7
- package/src/state.ts +89 -0
- package/src/task.d.ts +17 -0
- package/src/task.ts +153 -0
- package/{lib → src}/util.d.ts +1 -1
- package/{lib → src}/util.ts +23 -11
- package/test/batch.test.ts +23 -28
- package/test/benchmark.test.ts +115 -103
- package/test/computed.test.ts +133 -147
- package/test/effect.test.ts +42 -37
- package/test/state.test.ts +12 -79
- package/test/util/dependency-graph.ts +147 -145
- package/test/util/framework-types.ts +22 -22
- package/test/util/perf-tests.ts +28 -28
- package/test/util/reactive-framework.ts +11 -12
- package/lib/computed.d.ts +0 -33
- package/lib/computed.ts +0 -206
- package/lib/effect.d.ts +0 -22
- package/lib/effect.ts +0 -61
- package/lib/signal.d.ts +0 -45
- package/lib/signal.ts +0 -102
- package/lib/state.ts +0 -118
package/src/signal.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { isState, state } from './state'
|
|
2
|
+
import {
|
|
3
|
+
type ComputedCallback,
|
|
4
|
+
isComputed,
|
|
5
|
+
computed,
|
|
6
|
+
} from './computed'
|
|
7
|
+
import { isFunction } from './util'
|
|
8
|
+
|
|
9
|
+
/* === Types === */
|
|
10
|
+
|
|
11
|
+
type Signal<T extends {}> = {
|
|
12
|
+
get(): T
|
|
13
|
+
}
|
|
14
|
+
type MaybeSignal<T extends {}> = T | Signal<T> | ComputedCallback<T>
|
|
15
|
+
|
|
16
|
+
/* === Constants === */
|
|
17
|
+
|
|
18
|
+
const UNSET: any = Symbol()
|
|
19
|
+
|
|
20
|
+
/* === Exported Functions === */
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check whether a value is a Signal or not
|
|
24
|
+
*
|
|
25
|
+
* @since 0.9.0
|
|
26
|
+
* @param {unknown} value - value to check
|
|
27
|
+
* @returns {boolean} - true if value is a Signal, false otherwise
|
|
28
|
+
*/
|
|
29
|
+
const isSignal = /*#__PURE__*/ <T extends {}>(
|
|
30
|
+
value: unknown,
|
|
31
|
+
): value is Signal<T> => isState(value) || isComputed(value)
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check if the provided value is a callback that may be used as input for toSignal() to derive a computed state
|
|
35
|
+
*
|
|
36
|
+
* @since 0.12.0
|
|
37
|
+
* @param {unknown} value - value to check
|
|
38
|
+
* @returns {boolean} - true if value is a callback or callbacks object, false otherwise
|
|
39
|
+
*/
|
|
40
|
+
const isComputedCallback = /*#__PURE__*/ <T extends {}>(
|
|
41
|
+
value: unknown,
|
|
42
|
+
): value is ComputedCallback<T> => isFunction(value) && value.length < 2
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Convert a value to a Signal if it's not already a Signal
|
|
46
|
+
*
|
|
47
|
+
* @since 0.9.6
|
|
48
|
+
* @param {MaybeSignal<T>} value - value to convert to a Signal
|
|
49
|
+
* @returns {Signal<T>} - converted Signal
|
|
50
|
+
*/
|
|
51
|
+
const toSignal = /*#__PURE__*/ <T extends {}>(
|
|
52
|
+
value: MaybeSignal<T>,
|
|
53
|
+
): Signal<T> =>
|
|
54
|
+
isSignal<T>(value)
|
|
55
|
+
? value
|
|
56
|
+
: isComputedCallback<T>(value)
|
|
57
|
+
? computed(value)
|
|
58
|
+
: state(value as T)
|
|
59
|
+
|
|
60
|
+
/* === Exports === */
|
|
61
|
+
|
|
62
|
+
export {
|
|
63
|
+
type Signal,
|
|
64
|
+
type MaybeSignal,
|
|
65
|
+
UNSET,
|
|
66
|
+
isSignal,
|
|
67
|
+
isComputedCallback,
|
|
68
|
+
toSignal,
|
|
69
|
+
}
|
package/{lib → src}/state.d.ts
RENAMED
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
import { type TapMatcher } from './effect';
|
|
3
|
-
export type State<T extends {}> = {
|
|
1
|
+
type State<T extends {}> = {
|
|
4
2
|
[Symbol.toStringTag]: 'State';
|
|
5
3
|
get(): T;
|
|
6
4
|
set(v: T): void;
|
|
7
5
|
update(fn: (v: T) => T): void;
|
|
8
|
-
map<U extends {}>(fn: (v: T) => U | Promise<U>): Computed<U>;
|
|
9
|
-
tap(matcher: TapMatcher<T> | ((v: T) => void | (() => void))): () => void;
|
|
10
6
|
};
|
|
11
7
|
/**
|
|
12
8
|
* Create a new state signal
|
|
@@ -15,7 +11,7 @@ export type State<T extends {}> = {
|
|
|
15
11
|
* @param {T} initialValue - initial value of the state
|
|
16
12
|
* @returns {State<T>} - new state signal
|
|
17
13
|
*/
|
|
18
|
-
|
|
14
|
+
declare const state: <T extends {}>(initialValue: T) => State<T>;
|
|
19
15
|
/**
|
|
20
16
|
* Check if the provided value is a State instance
|
|
21
17
|
*
|
|
@@ -23,4 +19,5 @@ export declare const state: <T extends {}>(initialValue: T) => State<T>;
|
|
|
23
19
|
* @param {unknown} value - value to check
|
|
24
20
|
* @returns {boolean} - true if the value is a State instance, false otherwise
|
|
25
21
|
*/
|
|
26
|
-
|
|
22
|
+
declare const isState: <T extends {}>(value: unknown) => value is State<T>;
|
|
23
|
+
export { type State, state, isState };
|
package/src/state.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { UNSET } from './signal'
|
|
2
|
+
import { isObjectOfType } from './util'
|
|
3
|
+
import { type Watcher, notify, subscribe } from './scheduler'
|
|
4
|
+
|
|
5
|
+
/* === Types === */
|
|
6
|
+
|
|
7
|
+
type State<T extends {}> = {
|
|
8
|
+
[Symbol.toStringTag]: 'State'
|
|
9
|
+
get(): T
|
|
10
|
+
set(v: T): void
|
|
11
|
+
update(fn: (v: T) => T): void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* === Constants === */
|
|
15
|
+
|
|
16
|
+
const TYPE_STATE = 'State'
|
|
17
|
+
|
|
18
|
+
/* === Functions === */
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Create a new state signal
|
|
22
|
+
*
|
|
23
|
+
* @since 0.9.0
|
|
24
|
+
* @param {T} initialValue - initial value of the state
|
|
25
|
+
* @returns {State<T>} - new state signal
|
|
26
|
+
*/
|
|
27
|
+
const state = /*#__PURE__*/ <T extends {}>(initialValue: T): State<T> => {
|
|
28
|
+
const watchers: Set<Watcher> = new Set()
|
|
29
|
+
let value: T = initialValue
|
|
30
|
+
|
|
31
|
+
const s: State<T> = {
|
|
32
|
+
[Symbol.toStringTag]: TYPE_STATE,
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get the current value of the state
|
|
36
|
+
*
|
|
37
|
+
* @since 0.9.0
|
|
38
|
+
* @returns {T} - current value of the state
|
|
39
|
+
*/
|
|
40
|
+
get: (): T => {
|
|
41
|
+
subscribe(watchers)
|
|
42
|
+
return value
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Set a new value of the state
|
|
47
|
+
*
|
|
48
|
+
* @since 0.9.0
|
|
49
|
+
* @param {T} v
|
|
50
|
+
* @returns {void}
|
|
51
|
+
*/
|
|
52
|
+
set: (v: T): void => {
|
|
53
|
+
if (Object.is(value, v)) return
|
|
54
|
+
value = v
|
|
55
|
+
notify(watchers)
|
|
56
|
+
|
|
57
|
+
// Setting to UNSET clears the watchers so the signal can be garbage collected
|
|
58
|
+
if (UNSET === value) watchers.clear()
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Update the state with a new value using a function
|
|
63
|
+
*
|
|
64
|
+
* @since 0.10.0
|
|
65
|
+
* @param {(v: T) => T} fn - function to update the state
|
|
66
|
+
* @returns {void} - updates the state with the result of the function
|
|
67
|
+
*/
|
|
68
|
+
update: (fn: (v: T) => T): void => {
|
|
69
|
+
s.set(fn(value))
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return s
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check if the provided value is a State instance
|
|
78
|
+
*
|
|
79
|
+
* @since 0.9.0
|
|
80
|
+
* @param {unknown} value - value to check
|
|
81
|
+
* @returns {boolean} - true if the value is a State instance, false otherwise
|
|
82
|
+
*/
|
|
83
|
+
const isState = /*#__PURE__*/ <T extends {}>(
|
|
84
|
+
value: unknown,
|
|
85
|
+
): value is State<T> => isObjectOfType(value, TYPE_STATE)
|
|
86
|
+
|
|
87
|
+
/* === Exports === */
|
|
88
|
+
|
|
89
|
+
export { type State, state, isState }
|
package/src/task.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type Computed } from './computed';
|
|
2
|
+
/**
|
|
3
|
+
* Callback for async computation tasks
|
|
4
|
+
* This explicitly returns a Promise<T> to differentiate from MemoCallback
|
|
5
|
+
*
|
|
6
|
+
* @since 0.14.0
|
|
7
|
+
*/
|
|
8
|
+
type TaskCallback<T extends {}> = (abort: AbortSignal) => Promise<T>;
|
|
9
|
+
/**
|
|
10
|
+
* Create a derived signal that supports asynchronous computations
|
|
11
|
+
*
|
|
12
|
+
* @since 0.14.0
|
|
13
|
+
* @param {TaskCallback<T>} fn - async computation callback
|
|
14
|
+
* @returns {Computed<T>} - Computed signal
|
|
15
|
+
*/
|
|
16
|
+
declare const task: <T extends {}>(fn: TaskCallback<T>) => Computed<T>;
|
|
17
|
+
export { type TaskCallback, task };
|
package/src/task.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { UNSET } from './signal'
|
|
2
|
+
import {
|
|
3
|
+
CircularDependencyError,
|
|
4
|
+
isAbortError,
|
|
5
|
+
isPromise,
|
|
6
|
+
toError,
|
|
7
|
+
} from './util'
|
|
8
|
+
import {
|
|
9
|
+
type Cleanup,
|
|
10
|
+
type Watcher,
|
|
11
|
+
flush,
|
|
12
|
+
notify,
|
|
13
|
+
subscribe,
|
|
14
|
+
watch,
|
|
15
|
+
} from './scheduler'
|
|
16
|
+
import {
|
|
17
|
+
type Computed,
|
|
18
|
+
TYPE_COMPUTED,
|
|
19
|
+
} from './computed'
|
|
20
|
+
|
|
21
|
+
/* === Types === */
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Callback for async computation tasks
|
|
25
|
+
* This explicitly returns a Promise<T> to differentiate from MemoCallback
|
|
26
|
+
*
|
|
27
|
+
* @since 0.14.0
|
|
28
|
+
*/
|
|
29
|
+
type TaskCallback<T extends {}> = (abort: AbortSignal) => Promise<T>
|
|
30
|
+
|
|
31
|
+
/* === Function === */
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Create a derived signal that supports asynchronous computations
|
|
35
|
+
*
|
|
36
|
+
* @since 0.14.0
|
|
37
|
+
* @param {TaskCallback<T>} fn - async computation callback
|
|
38
|
+
* @returns {Computed<T>} - Computed signal
|
|
39
|
+
*/
|
|
40
|
+
const task = <T extends {}>(fn: TaskCallback<T>): Computed<T> => {
|
|
41
|
+
const watchers: Set<Watcher> = new Set()
|
|
42
|
+
|
|
43
|
+
// Internal state
|
|
44
|
+
let value: T = UNSET
|
|
45
|
+
let error: Error | undefined
|
|
46
|
+
let dirty = true
|
|
47
|
+
let changed = false
|
|
48
|
+
let computing = false
|
|
49
|
+
let controller: AbortController | undefined
|
|
50
|
+
|
|
51
|
+
// Functions to update internal state
|
|
52
|
+
const ok = (v: T) => {
|
|
53
|
+
if (!Object.is(v, value)) {
|
|
54
|
+
value = v
|
|
55
|
+
dirty = false
|
|
56
|
+
error = undefined
|
|
57
|
+
changed = true
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const nil = () => {
|
|
61
|
+
changed = UNSET !== value
|
|
62
|
+
value = UNSET
|
|
63
|
+
error = undefined
|
|
64
|
+
}
|
|
65
|
+
const err = (e: unknown) => {
|
|
66
|
+
const newError = toError(e)
|
|
67
|
+
changed = !(
|
|
68
|
+
error &&
|
|
69
|
+
newError.name === error.name &&
|
|
70
|
+
newError.message === error.message
|
|
71
|
+
)
|
|
72
|
+
value = UNSET
|
|
73
|
+
error = newError
|
|
74
|
+
}
|
|
75
|
+
const resolve = (v: T) => {
|
|
76
|
+
computing = false
|
|
77
|
+
controller = undefined
|
|
78
|
+
ok(v)
|
|
79
|
+
if (changed) notify(watchers)
|
|
80
|
+
}
|
|
81
|
+
const reject = (e: unknown) => {
|
|
82
|
+
computing = false
|
|
83
|
+
controller = undefined
|
|
84
|
+
err(e)
|
|
85
|
+
if (changed) notify(watchers)
|
|
86
|
+
}
|
|
87
|
+
const abort = () => {
|
|
88
|
+
computing = false
|
|
89
|
+
controller = undefined
|
|
90
|
+
compute() // retry
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Called when notified from sources (push)
|
|
94
|
+
const mark = (() => {
|
|
95
|
+
dirty = true
|
|
96
|
+
controller?.abort('Aborted because source signal changed')
|
|
97
|
+
if (watchers.size) {
|
|
98
|
+
notify(watchers)
|
|
99
|
+
} else {
|
|
100
|
+
mark.cleanups.forEach(fn => fn())
|
|
101
|
+
mark.cleanups.clear()
|
|
102
|
+
}
|
|
103
|
+
}) as Watcher
|
|
104
|
+
mark.cleanups = new Set<Cleanup>()
|
|
105
|
+
|
|
106
|
+
// Called when requested by dependencies (pull)
|
|
107
|
+
const compute = () =>
|
|
108
|
+
watch(() => {
|
|
109
|
+
if (computing) throw new CircularDependencyError('task')
|
|
110
|
+
changed = false
|
|
111
|
+
controller = new AbortController()
|
|
112
|
+
controller.signal.addEventListener('abort', abort, {
|
|
113
|
+
once: true,
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
let result: T | Promise<T>
|
|
117
|
+
computing = true
|
|
118
|
+
try {
|
|
119
|
+
result = fn(controller.signal)
|
|
120
|
+
} catch (e) {
|
|
121
|
+
if (isAbortError(e)) nil()
|
|
122
|
+
else err(e)
|
|
123
|
+
computing = false
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
if (isPromise(result)) result.then(resolve, reject)
|
|
127
|
+
else if (null == result || UNSET === result) nil()
|
|
128
|
+
else ok(result)
|
|
129
|
+
computing = false
|
|
130
|
+
}, mark)
|
|
131
|
+
|
|
132
|
+
const c: Computed<T> = {
|
|
133
|
+
[Symbol.toStringTag]: TYPE_COMPUTED,
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get the current value of the computed
|
|
137
|
+
*
|
|
138
|
+
* @returns {T} - current value of the computed
|
|
139
|
+
*/
|
|
140
|
+
get: (): T => {
|
|
141
|
+
subscribe(watchers)
|
|
142
|
+
flush()
|
|
143
|
+
if (dirty) compute()
|
|
144
|
+
if (error) throw error
|
|
145
|
+
return value
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
return c
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* === Exports === */
|
|
152
|
+
|
|
153
|
+
export { type TaskCallback, task }
|
package/{lib → src}/util.d.ts
RENAMED
|
@@ -8,4 +8,4 @@ declare const toError: (reason: unknown) => Error;
|
|
|
8
8
|
declare class CircularDependencyError extends Error {
|
|
9
9
|
constructor(where: string);
|
|
10
10
|
}
|
|
11
|
-
export { isFunction, isAsyncFunction, isObjectOfType, isError, isAbortError, isPromise, toError, CircularDependencyError };
|
|
11
|
+
export { isFunction, isAsyncFunction, isObjectOfType, isError, isAbortError, isPromise, toError, CircularDependencyError, };
|
package/{lib → src}/util.ts
RENAMED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
/* === Utility Functions === */
|
|
2
2
|
|
|
3
|
-
const isFunction = /*#__PURE__*/ <T>(
|
|
4
|
-
|
|
3
|
+
const isFunction = /*#__PURE__*/ <T>(
|
|
4
|
+
value: unknown,
|
|
5
|
+
): value is (...args: unknown[]) => T => typeof value === 'function'
|
|
5
6
|
|
|
6
|
-
const isAsyncFunction = /*#__PURE__*/ <T>(
|
|
7
|
+
const isAsyncFunction = /*#__PURE__*/ <T>(
|
|
8
|
+
value: unknown,
|
|
9
|
+
): value is (...args: unknown[]) => Promise<T> =>
|
|
7
10
|
isFunction(value) && value.constructor.name === 'AsyncFunction'
|
|
8
11
|
|
|
9
|
-
const isObjectOfType = /*#__PURE__*/ <T>(
|
|
10
|
-
|
|
12
|
+
const isObjectOfType = /*#__PURE__*/ <T>(
|
|
13
|
+
value: unknown,
|
|
14
|
+
type: string,
|
|
15
|
+
): value is T => Object.prototype.toString.call(value) === `[object ${type}]`
|
|
11
16
|
|
|
12
17
|
const isError = /*#__PURE__*/ (value: unknown): value is Error =>
|
|
13
18
|
value instanceof Error
|
|
@@ -20,13 +25,20 @@ const toError = (reason: unknown): Error =>
|
|
|
20
25
|
|
|
21
26
|
class CircularDependencyError extends Error {
|
|
22
27
|
constructor(where: string) {
|
|
23
|
-
|
|
28
|
+
super(`Circular dependency in ${where} detected`)
|
|
24
29
|
return this
|
|
25
|
-
|
|
30
|
+
}
|
|
26
31
|
}
|
|
27
32
|
|
|
33
|
+
/* === Exports === */
|
|
34
|
+
|
|
28
35
|
export {
|
|
29
|
-
isFunction,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
isFunction,
|
|
37
|
+
isAsyncFunction,
|
|
38
|
+
isObjectOfType,
|
|
39
|
+
isError,
|
|
40
|
+
isAbortError,
|
|
41
|
+
isPromise,
|
|
42
|
+
toError,
|
|
43
|
+
CircularDependencyError,
|
|
44
|
+
}
|
package/test/batch.test.ts
CHANGED
|
@@ -1,20 +1,15 @@
|
|
|
1
1
|
import { describe, test, expect } from 'bun:test'
|
|
2
|
-
import { state,
|
|
3
|
-
|
|
4
|
-
/* === Utility Functions === */
|
|
5
|
-
|
|
6
|
-
const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
|
|
2
|
+
import { state, memo, batch, effect } from '../'
|
|
7
3
|
|
|
8
4
|
/* === Tests === */
|
|
9
5
|
|
|
10
6
|
describe('Batch', function () {
|
|
11
|
-
|
|
12
|
-
test('should be triggered only once after repeated state change', function() {
|
|
7
|
+
test('should be triggered only once after repeated state change', function () {
|
|
13
8
|
const cause = state(0)
|
|
14
9
|
let result = 0
|
|
15
10
|
let count = 0
|
|
16
|
-
|
|
17
|
-
result =
|
|
11
|
+
effect(() => {
|
|
12
|
+
result = cause.get()
|
|
18
13
|
count++
|
|
19
14
|
})
|
|
20
15
|
batch(() => {
|
|
@@ -23,17 +18,18 @@ describe('Batch', function () {
|
|
|
23
18
|
}
|
|
24
19
|
})
|
|
25
20
|
expect(result).toBe(10)
|
|
26
|
-
expect(count).toBe(2)
|
|
21
|
+
expect(count).toBe(2) // + 1 for effect initialization
|
|
27
22
|
})
|
|
28
23
|
|
|
29
|
-
test('should be triggered only once when multiple signals are set', function() {
|
|
24
|
+
test('should be triggered only once when multiple signals are set', function () {
|
|
30
25
|
const a = state(3)
|
|
31
26
|
const b = state(4)
|
|
32
27
|
const c = state(5)
|
|
33
|
-
const sum =
|
|
28
|
+
const sum = memo(() => a.get() + b.get() + c.get())
|
|
34
29
|
let result = 0
|
|
35
30
|
let count = 0
|
|
36
|
-
|
|
31
|
+
effect({
|
|
32
|
+
signals: [sum],
|
|
37
33
|
ok: res => {
|
|
38
34
|
result = res
|
|
39
35
|
count++
|
|
@@ -46,27 +42,27 @@ describe('Batch', function () {
|
|
|
46
42
|
c.set(10)
|
|
47
43
|
})
|
|
48
44
|
expect(result).toBe(24)
|
|
49
|
-
expect(count).toBe(2)
|
|
45
|
+
expect(count).toBe(2) // + 1 for effect initialization
|
|
50
46
|
})
|
|
51
47
|
|
|
52
|
-
test('should prove example from README works', function() {
|
|
53
|
-
|
|
48
|
+
test('should prove example from README works', function () {
|
|
54
49
|
// State: define an array of Signal<number>
|
|
55
50
|
const signals = [state(2), state(3), state(5)]
|
|
56
51
|
|
|
57
52
|
// Computed: derive a calculation ...
|
|
58
|
-
const sum =
|
|
59
|
-
.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
53
|
+
const sum = memo(() => {
|
|
54
|
+
const v = signals.reduce((total, v) => total + v.get(), 0)
|
|
55
|
+
if (!Number.isFinite(v)) throw new Error('Invalid value')
|
|
56
|
+
return v
|
|
57
|
+
})
|
|
63
58
|
|
|
64
59
|
let result = 0
|
|
65
60
|
let okCount = 0
|
|
66
61
|
let errCount = 0
|
|
67
|
-
|
|
62
|
+
|
|
68
63
|
// Effect: switch cases for the result
|
|
69
|
-
|
|
64
|
+
effect({
|
|
65
|
+
signals: [sum],
|
|
70
66
|
ok: v => {
|
|
71
67
|
result = v
|
|
72
68
|
okCount++
|
|
@@ -75,7 +71,7 @@ describe('Batch', function () {
|
|
|
75
71
|
err: _error => {
|
|
76
72
|
errCount++
|
|
77
73
|
// console.error('Error:', error)
|
|
78
|
-
}
|
|
74
|
+
},
|
|
79
75
|
})
|
|
80
76
|
|
|
81
77
|
expect(okCount).toBe(1)
|
|
@@ -92,9 +88,8 @@ describe('Batch', function () {
|
|
|
92
88
|
// Provoke an error
|
|
93
89
|
signals[0].set(NaN)
|
|
94
90
|
|
|
95
|
-
|
|
96
|
-
expect(okCount).toBe(2)
|
|
97
|
-
|
|
91
|
+
expect(errCount).toBe(1)
|
|
92
|
+
expect(okCount).toBe(2) // should not have changed due to error
|
|
93
|
+
expect(result).toBe(20) // should not have changed due to error
|
|
98
94
|
})
|
|
99
|
-
|
|
100
95
|
})
|