@zeix/cause-effect 0.11.0 → 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 +68 -28
- package/index.d.ts +5 -4
- package/index.js +1 -1
- package/index.ts +5 -7
- package/lib/computed.d.ts +18 -5
- package/lib/computed.ts +99 -52
- package/lib/effect.d.ts +10 -7
- package/lib/effect.ts +17 -42
- package/lib/scheduler.d.ts +40 -0
- package/lib/scheduler.ts +127 -0
- package/lib/signal.d.ts +14 -25
- package/lib/signal.ts +45 -69
- package/lib/state.d.ts +8 -45
- package/lib/state.ts +92 -68
- package/lib/util.d.ts +3 -1
- package/lib/util.ts +12 -1
- package/package.json +5 -2
- package/test/batch.test.ts +99 -0
- package/test/benchmark.test.ts +124 -49
- 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 -597
- package/test/util/pseudo-random.ts +0 -45
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test'
|
|
2
|
+
import { state, computed, effect, batch } from '../'
|
|
3
|
+
|
|
4
|
+
/* === Utility Functions === */
|
|
5
|
+
|
|
6
|
+
const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
|
|
7
|
+
|
|
8
|
+
/* === Tests === */
|
|
9
|
+
|
|
10
|
+
describe('Batch', function () {
|
|
11
|
+
|
|
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
|
+
effect((res) => {
|
|
17
|
+
result = res;
|
|
18
|
+
count++;
|
|
19
|
+
}, cause);
|
|
20
|
+
batch(() => {
|
|
21
|
+
for (let i = 1; i <= 10; i++) {
|
|
22
|
+
cause.set(i);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
expect(result).toBe(10);
|
|
26
|
+
expect(count).toBe(2); // + 1 for effect initialization
|
|
27
|
+
});
|
|
28
|
+
|
|
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
|
+
effect((res) => {
|
|
37
|
+
result = res;
|
|
38
|
+
count++;
|
|
39
|
+
}, sum);
|
|
40
|
+
batch(() => {
|
|
41
|
+
a.set(6);
|
|
42
|
+
b.set(8);
|
|
43
|
+
c.set(10);
|
|
44
|
+
});
|
|
45
|
+
expect(result).toBe(24);
|
|
46
|
+
expect(count).toBe(2); // + 1 for effect initialization
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('should prove example from README works', function() {
|
|
50
|
+
|
|
51
|
+
// State: define an array of Signal<number>
|
|
52
|
+
const signals = [state(2), state(3), state(5)]
|
|
53
|
+
|
|
54
|
+
// Computed: derive a calculation ...
|
|
55
|
+
const sum = computed(
|
|
56
|
+
(...values) => values.reduce((total, v) => total + v, 0),
|
|
57
|
+
...signals
|
|
58
|
+
).map(v => { // ... perform validation and handle errors
|
|
59
|
+
if (!Number.isFinite(v)) throw new Error('Invalid value')
|
|
60
|
+
return v
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
let result = 0
|
|
64
|
+
let okCount = 0
|
|
65
|
+
let errCount = 0
|
|
66
|
+
|
|
67
|
+
// Effect: switch cases for the result
|
|
68
|
+
sum.match({
|
|
69
|
+
ok: v => {
|
|
70
|
+
result = v
|
|
71
|
+
okCount++
|
|
72
|
+
// console.log('Sum:', v)
|
|
73
|
+
},
|
|
74
|
+
err: _error => {
|
|
75
|
+
errCount++
|
|
76
|
+
// console.error('Error:', error)
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
expect(okCount).toBe(1);
|
|
81
|
+
expect(result).toBe(10);
|
|
82
|
+
|
|
83
|
+
// Batch: apply changes to all signals in a single transaction
|
|
84
|
+
batch(() => {
|
|
85
|
+
signals.forEach(signal => signal.update(v => v * 2))
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
expect(okCount).toBe(2);
|
|
89
|
+
expect(result).toBe(20);
|
|
90
|
+
|
|
91
|
+
// Provoke an error
|
|
92
|
+
signals[0].set(NaN)
|
|
93
|
+
|
|
94
|
+
expect(errCount).toBe(1);
|
|
95
|
+
expect(okCount).toBe(2); // should not have changed due to error
|
|
96
|
+
expect(result).toBe(20); // should not have changed due to error
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
});
|
package/test/benchmark.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { describe, test, expect } from 'bun:test'
|
|
2
|
-
import { state, computed, effect, batch } from '../
|
|
1
|
+
import { describe, test, expect, mock } from 'bun:test'
|
|
2
|
+
import { state, computed, effect, batch } from '../';
|
|
3
3
|
import { makeGraph, runGraph, Counter } from "./util/dependency-graph";
|
|
4
4
|
|
|
5
5
|
/* === Utility Functions === */
|
|
@@ -16,30 +16,35 @@ const framework = {
|
|
|
16
16
|
signal: <T extends {}>(initialValue: T) => {
|
|
17
17
|
const s = state<T>(initialValue);
|
|
18
18
|
return {
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
write: (v: T) => s.set(v),
|
|
20
|
+
read: () => s.get(),
|
|
21
21
|
};
|
|
22
22
|
},
|
|
23
23
|
computed: <T extends {}>(fn: () => T) => {
|
|
24
24
|
const c = computed(fn);
|
|
25
|
-
return {
|
|
25
|
+
return {
|
|
26
|
+
read: () => c.get(),
|
|
27
|
+
};
|
|
26
28
|
},
|
|
27
|
-
effect: (fn: () => void) => effect(
|
|
29
|
+
effect: (fn: () => void) => effect({
|
|
30
|
+
ok: fn,
|
|
31
|
+
err: () => {}
|
|
32
|
+
}),
|
|
28
33
|
withBatch: (fn: () => void) => batch(fn),
|
|
29
34
|
withBuild: (fn: () => void) => fn(),
|
|
30
35
|
};
|
|
31
36
|
const testPullCounts = true;
|
|
32
37
|
|
|
33
38
|
function makeConfig() {
|
|
34
|
-
return {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
};
|
|
39
|
+
return {
|
|
40
|
+
width: 3,
|
|
41
|
+
totalLayers: 3,
|
|
42
|
+
staticFraction: 1,
|
|
43
|
+
nSources: 2,
|
|
44
|
+
readFraction: 1,
|
|
45
|
+
expected: {},
|
|
46
|
+
iterations: 1,
|
|
47
|
+
};
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
/* === Test functions === */
|
|
@@ -51,48 +56,118 @@ describe('Basic test', function () {
|
|
|
51
56
|
const name = framework.name;
|
|
52
57
|
|
|
53
58
|
test(`${name} | simple dependency executes`, () => {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
framework.withBuild(() => {
|
|
60
|
+
const s = framework.signal(2);
|
|
61
|
+
const c = framework.computed(() => s.read() * 2);
|
|
62
|
+
|
|
63
|
+
expect(c.read()).toEqual(4);
|
|
64
|
+
});
|
|
58
65
|
});
|
|
59
|
-
|
|
66
|
+
|
|
67
|
+
test(`${name} | simple write`, () => {
|
|
68
|
+
framework.withBuild(() => {
|
|
69
|
+
const s = framework.signal(2);
|
|
70
|
+
const c = framework.computed(() => s.read() * 2);
|
|
71
|
+
expect(s.read()).toEqual(2);
|
|
72
|
+
expect(c.read()).toEqual(4);
|
|
73
|
+
|
|
74
|
+
s.write(3);
|
|
75
|
+
expect(s.read()).toEqual(3);
|
|
76
|
+
expect(c.read()).toEqual(6);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
60
80
|
test(`${name} | static graph`, () => {
|
|
61
81
|
const config = makeConfig();
|
|
62
|
-
const
|
|
82
|
+
const counter = new Counter();
|
|
83
|
+
// @ts-expect-error
|
|
84
|
+
const graph = makeGraph(framework, config, counter);
|
|
85
|
+
// @ts-expect-error
|
|
63
86
|
const sum = runGraph(graph, 2, 1, framework);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
87
|
+
expect(sum).toEqual(16);
|
|
88
|
+
/* if (testPullCounts) {
|
|
89
|
+
expect(counter.count).toEqual(11);
|
|
90
|
+
} else {
|
|
91
|
+
expect(counter.count).toBeGreaterThanOrEqual(11);
|
|
92
|
+
} */
|
|
69
93
|
});
|
|
70
|
-
|
|
94
|
+
|
|
71
95
|
test(`${name} | static graph, read 2/3 of leaves`, () => {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
96
|
+
framework.withBuild(() => {
|
|
97
|
+
const config = makeConfig();
|
|
98
|
+
config.readFraction = 2 / 3;
|
|
99
|
+
config.iterations = 10;
|
|
100
|
+
const counter = new Counter();
|
|
101
|
+
// @ts-expect-error
|
|
102
|
+
const graph = makeGraph(framework, config, counter);
|
|
103
|
+
// @ts-expect-error
|
|
104
|
+
const sum = runGraph(graph, 10, 2 / 3, framework);
|
|
105
|
+
|
|
106
|
+
expect(sum).toEqual(71);
|
|
107
|
+
/* if (testPullCounts) {
|
|
108
|
+
expect(counter.count).toEqual(41);
|
|
109
|
+
} else {
|
|
110
|
+
expect(counter.count).toBeGreaterThanOrEqual(41);
|
|
111
|
+
} */
|
|
112
|
+
});
|
|
82
113
|
});
|
|
83
|
-
|
|
114
|
+
|
|
84
115
|
test(`${name} | dynamic graph`, () => {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
116
|
+
framework.withBuild(() => {
|
|
117
|
+
const config = makeConfig();
|
|
118
|
+
config.staticFraction = 0.5;
|
|
119
|
+
config.width = 4;
|
|
120
|
+
config.totalLayers = 2;
|
|
121
|
+
const counter = new Counter();
|
|
122
|
+
// @ts-expect-error
|
|
123
|
+
const graph = makeGraph(framework, config, counter);
|
|
124
|
+
// @ts-expect-error
|
|
125
|
+
const sum = runGraph(graph, 10, 1, framework);
|
|
126
|
+
|
|
127
|
+
expect(sum).toEqual(72);
|
|
128
|
+
/* if (testPullCounts) {
|
|
129
|
+
expect(counter.count).toEqual(22);
|
|
130
|
+
} else {
|
|
131
|
+
expect(counter.count).toBeGreaterThanOrEqual(22);
|
|
132
|
+
} */
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test(`${name} | withBuild`, () => {
|
|
137
|
+
const r = framework.withBuild(() => {
|
|
138
|
+
const s = framework.signal(2);
|
|
139
|
+
const c = framework.computed(() => s.read() * 2);
|
|
140
|
+
|
|
141
|
+
expect(c.read()).toEqual(4);
|
|
142
|
+
return c.read();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// @ts-expect-error
|
|
146
|
+
expect(r).toEqual(4);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test(`${name} | effect`, () => {
|
|
150
|
+
const spy = (_v) => {};
|
|
151
|
+
const spyMock = mock(spy);
|
|
152
|
+
|
|
153
|
+
const s = framework.signal(2);
|
|
154
|
+
let c: any;
|
|
155
|
+
|
|
156
|
+
framework.withBuild(() => {
|
|
157
|
+
c = framework.computed(() => s.read() * 2);
|
|
158
|
+
|
|
159
|
+
framework.effect(() => {
|
|
160
|
+
spyMock(c.read());
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
expect(spyMock.mock.calls.length).toBe(1);
|
|
164
|
+
|
|
165
|
+
framework.withBatch(() => {
|
|
166
|
+
s.write(3);
|
|
167
|
+
});
|
|
168
|
+
expect(s.read()).toEqual(3);
|
|
169
|
+
expect(c.read()).toEqual(6);
|
|
170
|
+
expect(spyMock.mock.calls.length).toBe(2);
|
|
96
171
|
});
|
|
97
172
|
});
|
|
98
173
|
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test'
|
|
2
|
+
import { state, computed, UNSET, isComputed } from '../'
|
|
3
|
+
|
|
4
|
+
/* === Utility Functions === */
|
|
5
|
+
|
|
6
|
+
const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
|
|
7
|
+
const increment = (n: number) => Number.isFinite(n) ? n + 1 : UNSET;
|
|
8
|
+
|
|
9
|
+
/* === Tests === */
|
|
10
|
+
|
|
11
|
+
describe('Computed', function () {
|
|
12
|
+
|
|
13
|
+
test('should compute a function', function() {
|
|
14
|
+
const derived = computed(() => 1 + 2);
|
|
15
|
+
expect(derived.get()).toBe(3);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('should compute function dependent on a signal', function() {
|
|
19
|
+
const derived = state(42).map(v => ++v);
|
|
20
|
+
expect(derived.get()).toBe(43);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('should compute function dependent on an updated signal', function() {
|
|
24
|
+
const cause = state(42);
|
|
25
|
+
const derived = cause.map(v => ++v);
|
|
26
|
+
cause.set(24);
|
|
27
|
+
expect(derived.get()).toBe(25);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('should compute function dependent on an async signal', async function() {
|
|
31
|
+
const status = state('pending');
|
|
32
|
+
const promised = computed(async () => {
|
|
33
|
+
await wait(100);
|
|
34
|
+
status.set('success');
|
|
35
|
+
return 42;
|
|
36
|
+
});
|
|
37
|
+
const derived = promised.map(increment);
|
|
38
|
+
expect(derived.get()).toBe(UNSET);
|
|
39
|
+
expect(status.get()).toBe('pending');
|
|
40
|
+
await wait(110);
|
|
41
|
+
expect(derived.get()).toBe(43);
|
|
42
|
+
expect(status.get()).toBe('success');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('should handle errors from an async signal gracefully', async function() {
|
|
46
|
+
const status = state('pending');
|
|
47
|
+
const error = state('');
|
|
48
|
+
const promised = computed(async () => {
|
|
49
|
+
await wait(100);
|
|
50
|
+
status.set('error');
|
|
51
|
+
error.set('error occurred');
|
|
52
|
+
return 0
|
|
53
|
+
});
|
|
54
|
+
const derived = promised.map(increment);
|
|
55
|
+
expect(derived.get()).toBe(UNSET);
|
|
56
|
+
expect(status.get()).toBe('pending');
|
|
57
|
+
await wait(110);
|
|
58
|
+
expect(error.get()).toBe('error occurred');
|
|
59
|
+
expect(status.get()).toBe('error');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('should compute async signals in parallel without waterfalls', async function() {
|
|
63
|
+
const a = computed(async () => {
|
|
64
|
+
await wait(100);
|
|
65
|
+
return 10;
|
|
66
|
+
});
|
|
67
|
+
const b = computed(async () => {
|
|
68
|
+
await wait(100);
|
|
69
|
+
return 20;
|
|
70
|
+
});
|
|
71
|
+
const c = computed(() => {
|
|
72
|
+
const aValue = a.get();
|
|
73
|
+
const bValue = b.get();
|
|
74
|
+
return (aValue === UNSET || bValue === UNSET)
|
|
75
|
+
? UNSET
|
|
76
|
+
: aValue + bValue;
|
|
77
|
+
});
|
|
78
|
+
expect(c.get()).toBe(UNSET);
|
|
79
|
+
await wait(110);
|
|
80
|
+
expect(c.get()).toBe(30);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('should compute function dependent on a chain of computed states dependent on a signal', function() {
|
|
84
|
+
const derived = state(42)
|
|
85
|
+
.map(v => ++v)
|
|
86
|
+
.map(v => v * 2)
|
|
87
|
+
.map(v => ++v);
|
|
88
|
+
expect(derived.get()).toBe(87);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('should compute function dependent on a chain of computed states dependent on an updated signal', function() {
|
|
92
|
+
const cause = state(42);
|
|
93
|
+
const derived = cause
|
|
94
|
+
.map(v => ++v)
|
|
95
|
+
.map(v => v * 2)
|
|
96
|
+
.map(v => ++v);
|
|
97
|
+
cause.set(24);
|
|
98
|
+
expect(derived.get()).toBe(51);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('should drop X->B->X updates', function () {
|
|
102
|
+
let count = 0;
|
|
103
|
+
const x = state(2);
|
|
104
|
+
const a = x.map(v => --v);
|
|
105
|
+
const b = computed(() => x.get() + a.get());
|
|
106
|
+
const c = computed(() => {
|
|
107
|
+
count++;
|
|
108
|
+
return 'c: ' + b.get();
|
|
109
|
+
});
|
|
110
|
+
expect(c.get()).toBe('c: 3');
|
|
111
|
+
expect(count).toBe(1);
|
|
112
|
+
x.set(4);
|
|
113
|
+
expect(c.get()).toBe('c: 7');
|
|
114
|
+
expect(count).toBe(2);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('should only update every signal once (diamond graph)', function() {
|
|
118
|
+
let count = 0;
|
|
119
|
+
const x = state('a');
|
|
120
|
+
const a = x.map(v => v);
|
|
121
|
+
const b = x.map(v => v);
|
|
122
|
+
const c = computed(() => {
|
|
123
|
+
count++;
|
|
124
|
+
return a.get() + ' ' + b.get();
|
|
125
|
+
});
|
|
126
|
+
expect(c.get()).toBe('a a');
|
|
127
|
+
expect(count).toBe(1);
|
|
128
|
+
x.set('aa');
|
|
129
|
+
// flush();
|
|
130
|
+
expect(c.get()).toBe('aa aa');
|
|
131
|
+
expect(count).toBe(2);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('should only update every signal once (diamond graph + tail)', function() {
|
|
135
|
+
let count = 0;
|
|
136
|
+
const x = state('a');
|
|
137
|
+
const a = x.map(v => v);
|
|
138
|
+
const b = x.map(v => v);
|
|
139
|
+
const c = computed(() => a.get() + ' ' + b.get());
|
|
140
|
+
const d = computed(() => {
|
|
141
|
+
count++;
|
|
142
|
+
return c.get();
|
|
143
|
+
});
|
|
144
|
+
expect(d.get()).toBe('a a');
|
|
145
|
+
expect(count).toBe(1);
|
|
146
|
+
x.set('aa');
|
|
147
|
+
expect(d.get()).toBe('aa aa');
|
|
148
|
+
expect(count).toBe(2);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('should update multiple times after multiple state changes', function() {
|
|
152
|
+
const a = state(3);
|
|
153
|
+
const b = state(4);
|
|
154
|
+
let count = 0;
|
|
155
|
+
const sum = computed(() => {
|
|
156
|
+
count++;
|
|
157
|
+
return a.get() + b.get()
|
|
158
|
+
});
|
|
159
|
+
expect(sum.get()).toBe(7);
|
|
160
|
+
a.set(6);
|
|
161
|
+
expect(sum.get()).toBe(10);
|
|
162
|
+
b.set(8);
|
|
163
|
+
expect(sum.get()).toBe(14);
|
|
164
|
+
expect(count).toBe(3);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
/*
|
|
168
|
+
* Note for the next two tests:
|
|
169
|
+
*
|
|
170
|
+
* Due to the lazy evaluation strategy, unchanged computed signals may propagate
|
|
171
|
+
* change notifications one additional time before stabilizing. This is a
|
|
172
|
+
* one-time performance cost that allows for efficient memoization and
|
|
173
|
+
* error handling in most cases.
|
|
174
|
+
*/
|
|
175
|
+
test('should bail out if result is the same', function() {
|
|
176
|
+
let count = 0;
|
|
177
|
+
const x = state('a');
|
|
178
|
+
const a = computed(() => {
|
|
179
|
+
x.get();
|
|
180
|
+
return 'foo';
|
|
181
|
+
});
|
|
182
|
+
const b = computed(() => {
|
|
183
|
+
count++;
|
|
184
|
+
return a.get();
|
|
185
|
+
});
|
|
186
|
+
expect(b.get()).toBe('foo');
|
|
187
|
+
expect(count).toBe(1);
|
|
188
|
+
x.set('aa');
|
|
189
|
+
x.set('aaa');
|
|
190
|
+
x.set('aaaa');
|
|
191
|
+
expect(b.get()).toBe('foo');
|
|
192
|
+
expect(count).toBe(2);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('should block if result remains unchanged', function() {
|
|
196
|
+
let count = 0;
|
|
197
|
+
const x = state(42);
|
|
198
|
+
const a = x.map(v => v % 2);
|
|
199
|
+
const b = computed(() => a.get() ? 'odd' : 'even');
|
|
200
|
+
const c = computed(() => {
|
|
201
|
+
count++;
|
|
202
|
+
return `c: ${b.get()}`;
|
|
203
|
+
});
|
|
204
|
+
expect(c.get()).toBe('c: even');
|
|
205
|
+
expect(count).toBe(1);
|
|
206
|
+
x.set(44);
|
|
207
|
+
x.set(46);
|
|
208
|
+
x.set(48);
|
|
209
|
+
expect(c.get()).toBe('c: even');
|
|
210
|
+
expect(count).toBe(2);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('should detect and throw error for circular dependencies', function() {
|
|
214
|
+
const a = state(1);
|
|
215
|
+
const b = computed(() => c.get() + 1);
|
|
216
|
+
const c = computed(() => b.get() + a.get());
|
|
217
|
+
expect(() => {
|
|
218
|
+
b.get(); // This should trigger the circular dependency
|
|
219
|
+
}).toThrow('Circular dependency detected');
|
|
220
|
+
expect(a.get()).toBe(1);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
/* test('should propagate error if an error occurred', function() {
|
|
224
|
+
let count = 0;
|
|
225
|
+
const x = state(0);
|
|
226
|
+
const a = computed(() => {
|
|
227
|
+
if (x.get() === 1) throw new Error('Calculation error');
|
|
228
|
+
return 1;
|
|
229
|
+
});
|
|
230
|
+
const b = a.map(v => v ? 'success' : 'pending');
|
|
231
|
+
const c = computed(() => {
|
|
232
|
+
count++;
|
|
233
|
+
return `c: ${b.get()}`;
|
|
234
|
+
});
|
|
235
|
+
expect(a.get()).toBe(1);
|
|
236
|
+
expect(c.get()).toBe('c: success');
|
|
237
|
+
expect(count).toBe(1);
|
|
238
|
+
x.set(1)
|
|
239
|
+
try {
|
|
240
|
+
expect(a.get()).toBe(1);
|
|
241
|
+
expect(true).toBe(false); // This line should not be reached
|
|
242
|
+
} catch (error) {
|
|
243
|
+
expect(error.message).toBe('Calculation error');
|
|
244
|
+
} finally {
|
|
245
|
+
expect(c.get()).toBe('c: success');
|
|
246
|
+
expect(count).toBe(2);
|
|
247
|
+
}
|
|
248
|
+
}); */
|
|
249
|
+
|
|
250
|
+
test('should return a computed signal with .map()', function() {
|
|
251
|
+
const cause = state(42);
|
|
252
|
+
const derived = cause.map(v => ++v);
|
|
253
|
+
const double = derived.map(v => v * 2);
|
|
254
|
+
expect(isComputed(double)).toBe(true);
|
|
255
|
+
expect(double.get()).toBe(86);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test('should create an effect that reacts on signal changes with .match()', async function() {
|
|
259
|
+
const cause = state(42);
|
|
260
|
+
const derived = computed(async () => {
|
|
261
|
+
await wait(100);
|
|
262
|
+
return cause.get() + 1;
|
|
263
|
+
});
|
|
264
|
+
let okCount = 0;
|
|
265
|
+
let nilCount = 0;
|
|
266
|
+
let result: number = 0;
|
|
267
|
+
derived.match({
|
|
268
|
+
ok: v => {
|
|
269
|
+
result = v;
|
|
270
|
+
okCount++
|
|
271
|
+
},
|
|
272
|
+
nil: () => {
|
|
273
|
+
nilCount++
|
|
274
|
+
}
|
|
275
|
+
})
|
|
276
|
+
cause.set(43);
|
|
277
|
+
expect(okCount).toBe(0);
|
|
278
|
+
expect(nilCount).toBe(1);
|
|
279
|
+
expect(result).toBe(0);
|
|
280
|
+
|
|
281
|
+
await wait(110);
|
|
282
|
+
expect(okCount).toBe(1); // not +1 because initial state never made it here
|
|
283
|
+
expect(nilCount).toBe(1);
|
|
284
|
+
expect(result).toBe(44);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test('should handle complex computed signal with error and async dependencies', async function() {
|
|
288
|
+
const toggleState = state(true);
|
|
289
|
+
const errorProne = toggleState.map(v => {
|
|
290
|
+
if (v) throw new Error('Intentional error');
|
|
291
|
+
return 42;
|
|
292
|
+
});
|
|
293
|
+
const asyncValue = computed(async () => {
|
|
294
|
+
await wait(50);
|
|
295
|
+
return 10;
|
|
296
|
+
});
|
|
297
|
+
let okCount = 0;
|
|
298
|
+
let nilCount = 0;
|
|
299
|
+
let errCount = 0;
|
|
300
|
+
let result: number = 0;
|
|
301
|
+
const complexComputed = computed({
|
|
302
|
+
ok: (x, y) => { // happy path
|
|
303
|
+
okCount++;
|
|
304
|
+
return x + y
|
|
305
|
+
},
|
|
306
|
+
nil: () => { // not ready yet
|
|
307
|
+
nilCount++;
|
|
308
|
+
return 0
|
|
309
|
+
},
|
|
310
|
+
err: (_e) => { // error path
|
|
311
|
+
console.error('Error:', _e);
|
|
312
|
+
errCount++;
|
|
313
|
+
return -1
|
|
314
|
+
},
|
|
315
|
+
}, errorProne, asyncValue);
|
|
316
|
+
|
|
317
|
+
for (let i = 0; i < 10; i++) {
|
|
318
|
+
toggleState.set(!!(i % 2));
|
|
319
|
+
await wait(10);
|
|
320
|
+
result = complexComputed.get();
|
|
321
|
+
console.log(`i: ${i}, result: ${result}`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
expect(nilCount).toBeGreaterThanOrEqual(4);
|
|
325
|
+
expect(okCount).toBeGreaterThanOrEqual(2);
|
|
326
|
+
expect(errCount).toBeGreaterThanOrEqual(2);
|
|
327
|
+
expect(okCount + errCount + nilCount).toBe(10);
|
|
328
|
+
});
|
|
329
|
+
});
|