@zeix/cause-effect 0.12.4 → 0.13.1
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/.prettierrc +7 -0
- package/README.md +140 -94
- package/index.d.ts +5 -4
- package/index.js +1 -1
- package/index.ts +6 -6
- package/lib/computed.d.ts +16 -7
- package/lib/computed.ts +100 -44
- package/lib/effect.d.ts +17 -4
- package/lib/effect.ts +49 -17
- package/lib/scheduler.d.ts +9 -6
- package/lib/scheduler.ts +16 -8
- package/lib/signal.d.ts +17 -33
- package/lib/signal.ts +49 -77
- package/lib/state.d.ts +3 -3
- package/lib/state.ts +29 -22
- package/lib/util.d.ts +8 -5
- package/lib/util.ts +18 -11
- package/package.json +2 -2
- package/test/batch.test.ts +43 -42
- package/test/benchmark.test.ts +315 -319
- package/test/computed.test.ts +341 -242
- package/test/effect.test.ts +136 -119
- package/test/state.test.ts +126 -118
package/lib/state.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import { UNSET
|
|
1
|
+
import { UNSET } from './signal'
|
|
2
2
|
import { type Computed, computed } from './computed'
|
|
3
|
-
import { isObjectOfType } from './util'
|
|
3
|
+
import { isFunction, isObjectOfType } from './util'
|
|
4
4
|
import { type Watcher, notify, subscribe } from './scheduler'
|
|
5
|
-
import { effect } from './effect'
|
|
5
|
+
import { type TapMatcher, type EffectMatcher, effect } from './effect'
|
|
6
6
|
|
|
7
7
|
/* === Types === */
|
|
8
8
|
|
|
9
9
|
export type State<T extends {}> = {
|
|
10
|
-
[Symbol.toStringTag]: 'State'
|
|
11
|
-
get(): T
|
|
12
|
-
set(v: T): void
|
|
13
|
-
update(fn: (v: T) => T): void
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
[Symbol.toStringTag]: 'State'
|
|
11
|
+
get(): T
|
|
12
|
+
set(v: T): void
|
|
13
|
+
update(fn: (v: T) => T): void
|
|
14
|
+
map<U extends {}>(fn: (v: T) => U | Promise<U>): Computed<U>
|
|
15
|
+
tap(matcher: TapMatcher<T> | ((v: T) => void | (() => void))): () => void
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/* === Constants === */
|
|
@@ -29,7 +29,7 @@ const TYPE_STATE = 'State'
|
|
|
29
29
|
* @returns {State<T>} - new state signal
|
|
30
30
|
*/
|
|
31
31
|
export const state = /*#__PURE__*/ <T extends {}>(initialValue: T): State<T> => {
|
|
32
|
-
const watchers: Watcher
|
|
32
|
+
const watchers: Set<Watcher> = new Set()
|
|
33
33
|
let value: T = initialValue
|
|
34
34
|
|
|
35
35
|
const s: State<T> = {
|
|
@@ -59,7 +59,7 @@ export const state = /*#__PURE__*/ <T extends {}>(initialValue: T): State<T> =>
|
|
|
59
59
|
notify(watchers)
|
|
60
60
|
|
|
61
61
|
// Setting to UNSET clears the watchers so the signal can be garbage collected
|
|
62
|
-
if (UNSET === value) watchers.
|
|
62
|
+
if (UNSET === value) watchers.clear()
|
|
63
63
|
},
|
|
64
64
|
|
|
65
65
|
/**
|
|
@@ -77,24 +77,31 @@ export const state = /*#__PURE__*/ <T extends {}>(initialValue: T): State<T> =>
|
|
|
77
77
|
* Create a computed signal from the current state signal
|
|
78
78
|
*
|
|
79
79
|
* @since 0.9.0
|
|
80
|
-
* @param {
|
|
80
|
+
* @param {(v: T) => U | Promise<U>} fn - computed callback
|
|
81
81
|
* @returns {Computed<U>} - computed signal
|
|
82
82
|
*/
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
map: <U extends {}>(
|
|
84
|
+
fn: (v: T) => U | Promise<U>
|
|
85
|
+
): Computed<U> =>
|
|
86
|
+
computed({
|
|
87
|
+
signals: [s],
|
|
88
|
+
ok: fn
|
|
89
|
+
}),
|
|
85
90
|
|
|
86
91
|
/**
|
|
87
92
|
* Case matching for the state signal with effect callbacks
|
|
88
93
|
*
|
|
89
|
-
* @since 0.
|
|
90
|
-
* @
|
|
91
|
-
* @
|
|
92
|
-
* @returns {State<T>} - self, for chaining effect callbacks
|
|
94
|
+
* @since 0.13.0
|
|
95
|
+
* @param {TapMatcher<T> | ((v: T) => void | (() => void))} matcher - tap matcher or effect callback
|
|
96
|
+
* @returns {() => void} - cleanup function for the effect
|
|
93
97
|
*/
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
tap: (
|
|
99
|
+
matcher: TapMatcher<T> | ((v: T) => void | (() => void))
|
|
100
|
+
): () => void =>
|
|
101
|
+
effect({
|
|
102
|
+
signals: [s],
|
|
103
|
+
...(isFunction(matcher) ? { ok: matcher } : matcher)
|
|
104
|
+
} as EffectMatcher<[State<T>]>)
|
|
98
105
|
}
|
|
99
106
|
|
|
100
107
|
return s
|
package/lib/util.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
declare const isFunction: <T>(value: unknown) => value is (...args: unknown[]) => T;
|
|
2
|
-
declare const isAsyncFunction: <T>(value: unknown) => value is (...args: unknown[]) => Promise<T
|
|
2
|
+
declare const isAsyncFunction: <T>(value: unknown) => value is (...args: unknown[]) => Promise<T>;
|
|
3
3
|
declare const isObjectOfType: <T>(value: unknown, type: string) => value is T;
|
|
4
|
-
declare const isInstanceOf: <T>(type: new (...args: any[]) => T) => (value: unknown) => value is T;
|
|
5
4
|
declare const isError: (value: unknown) => value is Error;
|
|
6
|
-
declare const
|
|
7
|
-
declare const
|
|
8
|
-
|
|
5
|
+
declare const isAbortError: (value: unknown) => value is DOMException;
|
|
6
|
+
declare const isPromise: <T>(value: unknown) => value is Promise<T>;
|
|
7
|
+
declare const toError: (reason: unknown) => Error;
|
|
8
|
+
declare class CircularDependencyError extends Error {
|
|
9
|
+
constructor(where: string);
|
|
10
|
+
}
|
|
11
|
+
export { isFunction, isAsyncFunction, isObjectOfType, isError, isAbortError, isPromise, toError, CircularDependencyError };
|
package/lib/util.ts
CHANGED
|
@@ -3,23 +3,30 @@
|
|
|
3
3
|
const isFunction = /*#__PURE__*/ <T>(value: unknown): value is (...args: unknown[]) => T =>
|
|
4
4
|
typeof value === 'function'
|
|
5
5
|
|
|
6
|
-
const isAsyncFunction = /*#__PURE__*/ <T>(value: unknown): value is (...args: unknown[]) => Promise<T>
|
|
7
|
-
isFunction(value) &&
|
|
6
|
+
const isAsyncFunction = /*#__PURE__*/ <T>(value: unknown): value is (...args: unknown[]) => Promise<T> =>
|
|
7
|
+
isFunction(value) && value.constructor.name === 'AsyncFunction'
|
|
8
8
|
|
|
9
9
|
const isObjectOfType = /*#__PURE__*/ <T>(value: unknown, type: string): value is T =>
|
|
10
10
|
Object.prototype.toString.call(value) === `[object ${type}]`
|
|
11
11
|
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
const isError = /*#__PURE__*/ (value: unknown): value is Error =>
|
|
13
|
+
value instanceof Error
|
|
14
|
+
const isAbortError = /*#__PURE__*/ (value: unknown): value is DOMException =>
|
|
15
|
+
value instanceof DOMException && value.name === 'AbortError'
|
|
16
|
+
const isPromise = /*#__PURE__*/ <T>(value: unknown): value is Promise<T> =>
|
|
17
|
+
value instanceof Promise
|
|
18
|
+
const toError = (reason: unknown): Error =>
|
|
19
|
+
isError(reason) ? reason : Error(String(reason))
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
class CircularDependencyError extends Error {
|
|
22
|
+
constructor(where: string) {
|
|
23
|
+
super(`Circular dependency in ${where} detected`)
|
|
24
|
+
return this
|
|
25
|
+
}
|
|
26
|
+
}
|
|
21
27
|
|
|
22
28
|
export {
|
|
23
29
|
isFunction, isAsyncFunction,
|
|
24
|
-
isObjectOfType,
|
|
30
|
+
isObjectOfType, isError, isAbortError, isPromise, toError,
|
|
31
|
+
CircularDependencyError
|
|
25
32
|
}
|
package/package.json
CHANGED
package/test/batch.test.ts
CHANGED
|
@@ -10,41 +10,44 @@ const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
|
|
|
10
10
|
describe('Batch', function () {
|
|
11
11
|
|
|
12
12
|
test('should be triggered only once after repeated state change', function() {
|
|
13
|
-
const cause = state(0)
|
|
14
|
-
let result = 0
|
|
15
|
-
let count = 0
|
|
16
|
-
|
|
17
|
-
result = res
|
|
18
|
-
count
|
|
19
|
-
}
|
|
13
|
+
const cause = state(0)
|
|
14
|
+
let result = 0
|
|
15
|
+
let count = 0
|
|
16
|
+
cause.tap(res => {
|
|
17
|
+
result = res
|
|
18
|
+
count++
|
|
19
|
+
})
|
|
20
20
|
batch(() => {
|
|
21
21
|
for (let i = 1; i <= 10; i++) {
|
|
22
|
-
cause.set(i)
|
|
22
|
+
cause.set(i)
|
|
23
23
|
}
|
|
24
|
-
})
|
|
25
|
-
expect(result).toBe(10)
|
|
24
|
+
})
|
|
25
|
+
expect(result).toBe(10)
|
|
26
26
|
expect(count).toBe(2); // + 1 for effect initialization
|
|
27
|
-
})
|
|
27
|
+
})
|
|
28
28
|
|
|
29
29
|
test('should be triggered only once when multiple signals are set', function() {
|
|
30
|
-
const a = state(3)
|
|
31
|
-
const b = state(4)
|
|
32
|
-
const c = state(5)
|
|
33
|
-
const sum = computed(() => a.get() + b.get() + c.get())
|
|
34
|
-
let result = 0
|
|
35
|
-
let count = 0
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
30
|
+
const a = state(3)
|
|
31
|
+
const b = state(4)
|
|
32
|
+
const c = state(5)
|
|
33
|
+
const sum = computed(() => a.get() + b.get() + c.get())
|
|
34
|
+
let result = 0
|
|
35
|
+
let count = 0
|
|
36
|
+
sum.tap({
|
|
37
|
+
ok: res => {
|
|
38
|
+
result = res
|
|
39
|
+
count++
|
|
40
|
+
},
|
|
41
|
+
err: () => {},
|
|
42
|
+
})
|
|
40
43
|
batch(() => {
|
|
41
|
-
a.set(6)
|
|
42
|
-
b.set(8)
|
|
43
|
-
c.set(10)
|
|
44
|
-
})
|
|
45
|
-
expect(result).toBe(24)
|
|
44
|
+
a.set(6)
|
|
45
|
+
b.set(8)
|
|
46
|
+
c.set(10)
|
|
47
|
+
})
|
|
48
|
+
expect(result).toBe(24)
|
|
46
49
|
expect(count).toBe(2); // + 1 for effect initialization
|
|
47
|
-
})
|
|
50
|
+
})
|
|
48
51
|
|
|
49
52
|
test('should prove example from README works', function() {
|
|
50
53
|
|
|
@@ -52,20 +55,18 @@ describe('Batch', function () {
|
|
|
52
55
|
const signals = [state(2), state(3), state(5)]
|
|
53
56
|
|
|
54
57
|
// Computed: derive a calculation ...
|
|
55
|
-
const sum = computed(
|
|
56
|
-
(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return v
|
|
61
|
-
})
|
|
58
|
+
const sum = computed(() => signals.reduce((total, v) => total + v.get(), 0))
|
|
59
|
+
.map(v => { // ... perform validation and handle errors
|
|
60
|
+
if (!Number.isFinite(v)) throw new Error('Invalid value')
|
|
61
|
+
return v
|
|
62
|
+
})
|
|
62
63
|
|
|
63
64
|
let result = 0
|
|
64
65
|
let okCount = 0
|
|
65
66
|
let errCount = 0
|
|
66
67
|
|
|
67
68
|
// Effect: switch cases for the result
|
|
68
|
-
sum.
|
|
69
|
+
sum.tap({
|
|
69
70
|
ok: v => {
|
|
70
71
|
result = v
|
|
71
72
|
okCount++
|
|
@@ -77,23 +78,23 @@ describe('Batch', function () {
|
|
|
77
78
|
}
|
|
78
79
|
})
|
|
79
80
|
|
|
80
|
-
expect(okCount).toBe(1)
|
|
81
|
-
expect(result).toBe(10)
|
|
81
|
+
expect(okCount).toBe(1)
|
|
82
|
+
expect(result).toBe(10)
|
|
82
83
|
|
|
83
84
|
// Batch: apply changes to all signals in a single transaction
|
|
84
85
|
batch(() => {
|
|
85
86
|
signals.forEach(signal => signal.update(v => v * 2))
|
|
86
87
|
})
|
|
87
88
|
|
|
88
|
-
expect(okCount).toBe(2)
|
|
89
|
-
expect(result).toBe(20)
|
|
89
|
+
expect(okCount).toBe(2)
|
|
90
|
+
expect(result).toBe(20)
|
|
90
91
|
|
|
91
92
|
// Provoke an error
|
|
92
93
|
signals[0].set(NaN)
|
|
93
94
|
|
|
94
|
-
expect(errCount).toBe(1)
|
|
95
|
+
expect(errCount).toBe(1)
|
|
95
96
|
expect(okCount).toBe(2); // should not have changed due to error
|
|
96
97
|
expect(result).toBe(20); // should not have changed due to error
|
|
97
|
-
})
|
|
98
|
+
})
|
|
98
99
|
|
|
99
|
-
})
|
|
100
|
+
})
|