@zeix/cause-effect 0.12.4 → 0.13.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/.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 +16 -33
- package/lib/signal.ts +48 -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 +3 -3
- 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/test/computed.test.ts
CHANGED
|
@@ -1,168 +1,172 @@
|
|
|
1
1
|
import { describe, test, expect } from 'bun:test'
|
|
2
|
-
import { state, computed, UNSET, isComputed } from '../'
|
|
2
|
+
import { state, computed, UNSET, isComputed, isState } from '../index.ts'
|
|
3
3
|
|
|
4
4
|
/* === Utility Functions === */
|
|
5
5
|
|
|
6
6
|
const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
|
|
7
|
-
const increment = (n: number) => Number.isFinite(n) ? n + 1 : UNSET
|
|
7
|
+
const increment = (n: number) => Number.isFinite(n) ? n + 1 : UNSET
|
|
8
8
|
|
|
9
9
|
/* === Tests === */
|
|
10
10
|
|
|
11
11
|
describe('Computed', function () {
|
|
12
12
|
|
|
13
|
+
test('should identify computed signals with isComputed()', () => {
|
|
14
|
+
const count = state(42)
|
|
15
|
+
const doubled = count.map(v => v * 2)
|
|
16
|
+
expect(isComputed(doubled)).toBe(true)
|
|
17
|
+
expect(isState(doubled)).toBe(false)
|
|
18
|
+
})
|
|
19
|
+
|
|
13
20
|
test('should compute a function', function() {
|
|
14
|
-
const derived = computed(() => 1 + 2)
|
|
15
|
-
expect(derived.get()).toBe(3)
|
|
16
|
-
})
|
|
21
|
+
const derived = computed(() => 1 + 2)
|
|
22
|
+
expect(derived.get()).toBe(3)
|
|
23
|
+
})
|
|
17
24
|
|
|
18
25
|
test('should compute function dependent on a signal', function() {
|
|
19
|
-
const derived = state(42).map(v => ++v)
|
|
20
|
-
expect(derived.get()).toBe(43)
|
|
21
|
-
})
|
|
26
|
+
const derived = state(42).map(v => ++v)
|
|
27
|
+
expect(derived.get()).toBe(43)
|
|
28
|
+
})
|
|
22
29
|
|
|
23
30
|
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
|
-
})
|
|
31
|
+
const cause = state(42)
|
|
32
|
+
const derived = cause.map(v => ++v)
|
|
33
|
+
cause.set(24)
|
|
34
|
+
expect(derived.get()).toBe(25)
|
|
35
|
+
})
|
|
29
36
|
|
|
30
37
|
test('should compute function dependent on an async signal', async function() {
|
|
31
|
-
const status = state('pending')
|
|
38
|
+
const status = state('pending')
|
|
32
39
|
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
|
-
})
|
|
40
|
+
await wait(100)
|
|
41
|
+
status.set('success')
|
|
42
|
+
return 42
|
|
43
|
+
})
|
|
44
|
+
const derived = promised.map(increment)
|
|
45
|
+
expect(derived.get()).toBe(UNSET)
|
|
46
|
+
expect(status.get()).toBe('pending')
|
|
47
|
+
await wait(110)
|
|
48
|
+
expect(derived.get()).toBe(43)
|
|
49
|
+
expect(status.get()).toBe('success')
|
|
50
|
+
})
|
|
44
51
|
|
|
45
52
|
test('should handle errors from an async signal gracefully', async function() {
|
|
46
|
-
const status = state('pending')
|
|
47
|
-
const error = state('')
|
|
53
|
+
const status = state('pending')
|
|
54
|
+
const error = state('')
|
|
48
55
|
const promised = computed(async () => {
|
|
49
|
-
await wait(100)
|
|
50
|
-
status.set('error')
|
|
51
|
-
error.set('error occurred')
|
|
56
|
+
await wait(100)
|
|
57
|
+
status.set('error')
|
|
58
|
+
error.set('error occurred')
|
|
52
59
|
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
|
-
})
|
|
60
|
+
})
|
|
61
|
+
const derived = promised.map(increment)
|
|
62
|
+
expect(derived.get()).toBe(UNSET)
|
|
63
|
+
expect(status.get()).toBe('pending')
|
|
64
|
+
await wait(110)
|
|
65
|
+
expect(error.get()).toBe('error occurred')
|
|
66
|
+
expect(status.get()).toBe('error')
|
|
67
|
+
})
|
|
61
68
|
|
|
62
69
|
test('should compute async signals in parallel without waterfalls', async function() {
|
|
63
70
|
const a = computed(async () => {
|
|
64
|
-
await wait(100)
|
|
65
|
-
return 10
|
|
66
|
-
})
|
|
71
|
+
await wait(100)
|
|
72
|
+
return 10
|
|
73
|
+
})
|
|
67
74
|
const b = computed(async () => {
|
|
68
|
-
await wait(100)
|
|
69
|
-
return 20
|
|
70
|
-
})
|
|
71
|
-
const c = computed(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
await wait(110);
|
|
80
|
-
expect(c.get()).toBe(30);
|
|
81
|
-
});
|
|
75
|
+
await wait(100)
|
|
76
|
+
return 20
|
|
77
|
+
})
|
|
78
|
+
const c = computed({
|
|
79
|
+
signals: [a, b],
|
|
80
|
+
ok: (aValue, bValue) => aValue + bValue
|
|
81
|
+
})
|
|
82
|
+
expect(c.get()).toBe(UNSET)
|
|
83
|
+
await wait(110)
|
|
84
|
+
expect(c.get()).toBe(30)
|
|
85
|
+
})
|
|
82
86
|
|
|
83
87
|
test('should compute function dependent on a chain of computed states dependent on a signal', function() {
|
|
84
88
|
const derived = state(42)
|
|
85
89
|
.map(v => ++v)
|
|
86
90
|
.map(v => v * 2)
|
|
87
|
-
.map(v => ++v)
|
|
88
|
-
expect(derived.get()).toBe(87)
|
|
89
|
-
})
|
|
91
|
+
.map(v => ++v)
|
|
92
|
+
expect(derived.get()).toBe(87)
|
|
93
|
+
})
|
|
90
94
|
|
|
91
95
|
test('should compute function dependent on a chain of computed states dependent on an updated signal', function() {
|
|
92
|
-
const cause = state(42)
|
|
96
|
+
const cause = state(42)
|
|
93
97
|
const derived = cause
|
|
94
98
|
.map(v => ++v)
|
|
95
99
|
.map(v => v * 2)
|
|
96
|
-
.map(v => ++v)
|
|
97
|
-
cause.set(24)
|
|
98
|
-
expect(derived.get()).toBe(51)
|
|
99
|
-
})
|
|
100
|
+
.map(v => ++v)
|
|
101
|
+
cause.set(24)
|
|
102
|
+
expect(derived.get()).toBe(51)
|
|
103
|
+
})
|
|
100
104
|
|
|
101
105
|
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 =
|
|
107
|
-
count
|
|
108
|
-
return 'c: ' +
|
|
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
|
-
})
|
|
106
|
+
let count = 0
|
|
107
|
+
const x = state(2)
|
|
108
|
+
const a = x.map(v => --v)
|
|
109
|
+
const b = computed(() => x.get() + a.get())
|
|
110
|
+
const c = b.map(v => {
|
|
111
|
+
count++
|
|
112
|
+
return 'c: ' + v
|
|
113
|
+
})
|
|
114
|
+
expect(c.get()).toBe('c: 3')
|
|
115
|
+
expect(count).toBe(1)
|
|
116
|
+
x.set(4)
|
|
117
|
+
expect(c.get()).toBe('c: 7')
|
|
118
|
+
expect(count).toBe(2)
|
|
119
|
+
})
|
|
116
120
|
|
|
117
121
|
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
|
+
let count = 0
|
|
123
|
+
const x = state('a')
|
|
124
|
+
const a = x.map(v => v)
|
|
125
|
+
const b = x.map(v => v)
|
|
122
126
|
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
|
-
})
|
|
127
|
+
count++
|
|
128
|
+
return a.get() + ' ' + b.get()
|
|
129
|
+
})
|
|
130
|
+
expect(c.get()).toBe('a a')
|
|
131
|
+
expect(count).toBe(1)
|
|
132
|
+
x.set('aa')
|
|
133
|
+
// flush()
|
|
134
|
+
expect(c.get()).toBe('aa aa')
|
|
135
|
+
expect(count).toBe(2)
|
|
136
|
+
})
|
|
133
137
|
|
|
134
138
|
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 =
|
|
141
|
-
count
|
|
142
|
-
return
|
|
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
|
-
})
|
|
139
|
+
let count = 0
|
|
140
|
+
const x = state('a')
|
|
141
|
+
const a = x.map(v => v)
|
|
142
|
+
const b = x.map(v => v)
|
|
143
|
+
const c = computed(() => a.get() + ' ' + b.get())
|
|
144
|
+
const d = c.map(v => {
|
|
145
|
+
count++
|
|
146
|
+
return v
|
|
147
|
+
})
|
|
148
|
+
expect(d.get()).toBe('a a')
|
|
149
|
+
expect(count).toBe(1)
|
|
150
|
+
x.set('aa')
|
|
151
|
+
expect(d.get()).toBe('aa aa')
|
|
152
|
+
expect(count).toBe(2)
|
|
153
|
+
})
|
|
150
154
|
|
|
151
155
|
test('should update multiple times after multiple state changes', function() {
|
|
152
|
-
const a = state(3)
|
|
153
|
-
const b = state(4)
|
|
154
|
-
let count = 0
|
|
156
|
+
const a = state(3)
|
|
157
|
+
const b = state(4)
|
|
158
|
+
let count = 0
|
|
155
159
|
const sum = computed(() => {
|
|
156
|
-
count
|
|
160
|
+
count++
|
|
157
161
|
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
|
-
})
|
|
162
|
+
})
|
|
163
|
+
expect(sum.get()).toBe(7)
|
|
164
|
+
a.set(6)
|
|
165
|
+
expect(sum.get()).toBe(10)
|
|
166
|
+
b.set(8)
|
|
167
|
+
expect(sum.get()).toBe(14)
|
|
168
|
+
expect(count).toBe(3)
|
|
169
|
+
})
|
|
166
170
|
|
|
167
171
|
/*
|
|
168
172
|
* Note for the next two tests:
|
|
@@ -173,166 +177,261 @@ describe('Computed', function () {
|
|
|
173
177
|
* error handling in most cases.
|
|
174
178
|
*/
|
|
175
179
|
test('should bail out if result is the same', function() {
|
|
176
|
-
let count = 0
|
|
177
|
-
const x = state('a')
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
return
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
expect(
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
x.set('aaaa');
|
|
191
|
-
expect(b.get()).toBe('foo');
|
|
192
|
-
expect(count).toBe(2);
|
|
193
|
-
});
|
|
180
|
+
let count = 0
|
|
181
|
+
const x = state('a')
|
|
182
|
+
const b = x.map(() => 'foo').map(v => {
|
|
183
|
+
count++
|
|
184
|
+
return v
|
|
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
194
|
|
|
195
195
|
test('should block if result remains unchanged', function() {
|
|
196
|
-
let count = 0
|
|
197
|
-
const x = state(42)
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
})
|
|
196
|
+
let count = 0
|
|
197
|
+
const x = state(42)
|
|
198
|
+
const c = x.map(v => v % 2)
|
|
199
|
+
.map(v => v ? 'odd' : 'even')
|
|
200
|
+
.map(v => {
|
|
201
|
+
count++
|
|
202
|
+
return `c: ${v}`
|
|
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
212
|
|
|
213
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())
|
|
214
|
+
const a = state(1)
|
|
215
|
+
const b = computed(() => c.get() + 1)
|
|
216
|
+
const c = computed(() => b.get() + a.get())
|
|
217
217
|
expect(() => {
|
|
218
|
-
b.get()
|
|
219
|
-
}).toThrow('Circular dependency in computed detected')
|
|
220
|
-
expect(a.get()).toBe(1)
|
|
221
|
-
})
|
|
218
|
+
b.get() // This should trigger the circular dependency
|
|
219
|
+
}).toThrow('Circular dependency in computed detected')
|
|
220
|
+
expect(a.get()).toBe(1)
|
|
221
|
+
})
|
|
222
222
|
|
|
223
223
|
test('should propagate error if an error occurred', function() {
|
|
224
|
-
let okCount = 0
|
|
225
|
-
let errCount = 0
|
|
226
|
-
const x = state(0)
|
|
224
|
+
let okCount = 0
|
|
225
|
+
let errCount = 0
|
|
226
|
+
const x = state(0)
|
|
227
227
|
const a = x.map(v => {
|
|
228
|
-
if (v === 1) throw new Error('Calculation error')
|
|
229
|
-
return 1
|
|
230
|
-
})
|
|
231
|
-
const
|
|
228
|
+
if (v === 1) throw new Error('Calculation error')
|
|
229
|
+
return 1
|
|
230
|
+
})
|
|
231
|
+
const c = computed({
|
|
232
|
+
signals: [a],
|
|
232
233
|
ok: v => v ? 'success' : 'failure',
|
|
233
|
-
err:
|
|
234
|
-
errCount
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
expect(
|
|
244
|
-
expect(c.get()).toBe('c: success');
|
|
245
|
-
expect(okCount).toBe(1);
|
|
234
|
+
err: () => {
|
|
235
|
+
errCount++
|
|
236
|
+
return 'recovered'
|
|
237
|
+
},
|
|
238
|
+
}).map(v => {
|
|
239
|
+
okCount++
|
|
240
|
+
return `c: ${v}`
|
|
241
|
+
})
|
|
242
|
+
expect(a.get()).toBe(1)
|
|
243
|
+
expect(c.get()).toBe('c: success')
|
|
244
|
+
expect(okCount).toBe(1)
|
|
246
245
|
try {
|
|
247
246
|
x.set(1)
|
|
248
|
-
expect(a.get()).toBe(1)
|
|
247
|
+
expect(a.get()).toBe(1)
|
|
249
248
|
expect(true).toBe(false); // This line should not be reached
|
|
250
249
|
} catch (error) {
|
|
251
|
-
expect(error.message).toBe('Calculation error')
|
|
250
|
+
expect(error.message).toBe('Calculation error')
|
|
252
251
|
} finally {
|
|
253
|
-
expect(c.get()).toBe('c: recovered')
|
|
254
|
-
expect(okCount).toBe(2)
|
|
255
|
-
expect(errCount).toBe(1)
|
|
252
|
+
expect(c.get()).toBe('c: recovered')
|
|
253
|
+
expect(okCount).toBe(2)
|
|
254
|
+
expect(errCount).toBe(1)
|
|
256
255
|
}
|
|
257
|
-
})
|
|
256
|
+
})
|
|
258
257
|
|
|
259
258
|
test('should return a computed signal with .map()', function() {
|
|
260
|
-
const cause = state(42)
|
|
261
|
-
const derived = cause.map(v => ++v)
|
|
262
|
-
const double = derived.map(v => v * 2)
|
|
263
|
-
expect(isComputed(double)).toBe(true)
|
|
264
|
-
expect(double.get()).toBe(86)
|
|
265
|
-
})
|
|
266
|
-
|
|
267
|
-
test('should create an effect that reacts on
|
|
268
|
-
const cause = state(42)
|
|
259
|
+
const cause = state(42)
|
|
260
|
+
const derived = cause.map(v => ++v)
|
|
261
|
+
const double = derived.map(v => v * 2)
|
|
262
|
+
expect(isComputed(double)).toBe(true)
|
|
263
|
+
expect(double.get()).toBe(86)
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
test('should create an effect that reacts on async computed changes with .tap()', async function() {
|
|
267
|
+
const cause = state(42)
|
|
269
268
|
const derived = computed(async () => {
|
|
270
|
-
await wait(100)
|
|
271
|
-
return cause.get() + 1
|
|
272
|
-
})
|
|
273
|
-
let okCount = 0
|
|
274
|
-
let nilCount = 0
|
|
275
|
-
let result: number = 0
|
|
276
|
-
derived.
|
|
269
|
+
await wait(100)
|
|
270
|
+
return cause.get() + 1
|
|
271
|
+
})
|
|
272
|
+
let okCount = 0
|
|
273
|
+
let nilCount = 0
|
|
274
|
+
let result: number = 0
|
|
275
|
+
derived.tap({
|
|
277
276
|
ok: v => {
|
|
278
|
-
result = v
|
|
277
|
+
result = v
|
|
279
278
|
okCount++
|
|
280
279
|
},
|
|
281
280
|
nil: () => {
|
|
282
281
|
nilCount++
|
|
283
282
|
}
|
|
284
283
|
})
|
|
285
|
-
cause.set(43)
|
|
286
|
-
expect(okCount).toBe(0)
|
|
287
|
-
expect(nilCount).toBe(1)
|
|
288
|
-
expect(result).toBe(0)
|
|
284
|
+
cause.set(43)
|
|
285
|
+
expect(okCount).toBe(0)
|
|
286
|
+
expect(nilCount).toBe(1)
|
|
287
|
+
expect(result).toBe(0)
|
|
289
288
|
|
|
290
|
-
await wait(110)
|
|
291
|
-
expect(okCount).toBe(1)
|
|
292
|
-
expect(nilCount).toBe(1)
|
|
293
|
-
expect(result).toBe(44)
|
|
294
|
-
})
|
|
289
|
+
await wait(110)
|
|
290
|
+
expect(okCount).toBe(1) // not +1 because initial state never made it here
|
|
291
|
+
expect(nilCount).toBe(1)
|
|
292
|
+
expect(result).toBe(44)
|
|
293
|
+
})
|
|
295
294
|
|
|
296
295
|
test('should handle complex computed signal with error and async dependencies', async function() {
|
|
297
|
-
const toggleState = state(true)
|
|
296
|
+
const toggleState = state(true)
|
|
298
297
|
const errorProne = toggleState.map(v => {
|
|
299
|
-
if (v) throw new Error('Intentional error')
|
|
300
|
-
return 42
|
|
301
|
-
})
|
|
298
|
+
if (v) throw new Error('Intentional error')
|
|
299
|
+
return 42
|
|
300
|
+
})
|
|
302
301
|
const asyncValue = computed(async () => {
|
|
303
|
-
await wait(50)
|
|
304
|
-
return 10
|
|
305
|
-
})
|
|
306
|
-
let okCount = 0
|
|
307
|
-
let nilCount = 0
|
|
308
|
-
let errCount = 0
|
|
309
|
-
let result: number = 0
|
|
302
|
+
await wait(50)
|
|
303
|
+
return 10
|
|
304
|
+
})
|
|
305
|
+
let okCount = 0
|
|
306
|
+
let nilCount = 0
|
|
307
|
+
let errCount = 0
|
|
308
|
+
let result: number = 0
|
|
310
309
|
const complexComputed = computed({
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
310
|
+
signals: [errorProne, asyncValue],
|
|
311
|
+
ok: v => {
|
|
312
|
+
okCount++
|
|
313
|
+
return v
|
|
314
314
|
},
|
|
315
|
-
nil: () => {
|
|
316
|
-
|
|
315
|
+
nil: () => {
|
|
316
|
+
nilCount++
|
|
317
317
|
return 0
|
|
318
318
|
},
|
|
319
|
-
err: (
|
|
320
|
-
|
|
321
|
-
errCount++;
|
|
319
|
+
err: () => {
|
|
320
|
+
errCount++
|
|
322
321
|
return -1
|
|
323
|
-
}
|
|
324
|
-
}
|
|
322
|
+
}
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
/* computed(() => {
|
|
326
|
+
try {
|
|
327
|
+
const x = errorProne.get()
|
|
328
|
+
const y = asyncValue.get()
|
|
329
|
+
if (y === UNSET) { // not ready yet
|
|
330
|
+
nilCount++
|
|
331
|
+
return 0
|
|
332
|
+
} else { // happy path
|
|
333
|
+
okCount++
|
|
334
|
+
return x + y
|
|
335
|
+
}
|
|
336
|
+
} catch (error) { // error path
|
|
337
|
+
errCount++
|
|
338
|
+
return -1
|
|
339
|
+
}
|
|
340
|
+
}) */
|
|
325
341
|
|
|
326
342
|
for (let i = 0; i < 10; i++) {
|
|
327
|
-
toggleState.set(!!(i % 2))
|
|
328
|
-
await wait(10)
|
|
329
|
-
result = complexComputed.get()
|
|
330
|
-
// console.log(`i: ${i}, result: ${result}`)
|
|
343
|
+
toggleState.set(!!(i % 2))
|
|
344
|
+
await wait(10)
|
|
345
|
+
result = complexComputed.get()
|
|
346
|
+
// console.log(`i: ${i}, result: ${result}`)
|
|
331
347
|
}
|
|
332
348
|
|
|
333
|
-
expect(nilCount).toBeGreaterThanOrEqual(
|
|
334
|
-
expect(okCount).toBeGreaterThanOrEqual(2)
|
|
335
|
-
expect(errCount).toBeGreaterThanOrEqual(
|
|
336
|
-
expect(okCount + errCount + nilCount).toBe(10)
|
|
337
|
-
})
|
|
338
|
-
|
|
349
|
+
expect(nilCount).toBeGreaterThanOrEqual(5)
|
|
350
|
+
expect(okCount).toBeGreaterThanOrEqual(2)
|
|
351
|
+
expect(errCount).toBeGreaterThanOrEqual(3)
|
|
352
|
+
expect(okCount + errCount + nilCount).toBe(10)
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
test('should handle signal changes during async computation', async function() {
|
|
356
|
+
const source = state(1)
|
|
357
|
+
let computationCount = 0
|
|
358
|
+
const derived = computed(async abort => {
|
|
359
|
+
computationCount++
|
|
360
|
+
expect(abort?.aborted).toBe(false)
|
|
361
|
+
await wait(100)
|
|
362
|
+
return source.get()
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
// Start first computation
|
|
366
|
+
expect(derived.get()).toBe(UNSET)
|
|
367
|
+
expect(computationCount).toBe(1)
|
|
368
|
+
|
|
369
|
+
// Change source before first computation completes
|
|
370
|
+
source.set(2)
|
|
371
|
+
await wait(210)
|
|
372
|
+
expect(derived.get()).toBe(2)
|
|
373
|
+
expect(computationCount).toBe(1)
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
test('should handle multiple rapid changes during async computation', async function() {
|
|
377
|
+
const source = state(1)
|
|
378
|
+
let computationCount = 0
|
|
379
|
+
const derived = computed(async abort => {
|
|
380
|
+
computationCount++
|
|
381
|
+
expect(abort?.aborted).toBe(false)
|
|
382
|
+
await wait(100)
|
|
383
|
+
return source.get()
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
// Start first computation
|
|
387
|
+
expect(derived.get()).toBe(UNSET)
|
|
388
|
+
expect(computationCount).toBe(1)
|
|
389
|
+
|
|
390
|
+
// Make multiple rapid changes
|
|
391
|
+
source.set(2)
|
|
392
|
+
source.set(3)
|
|
393
|
+
source.set(4)
|
|
394
|
+
await wait(210)
|
|
395
|
+
|
|
396
|
+
// Should have computed twice (initial + final change)
|
|
397
|
+
expect(derived.get()).toBe(4)
|
|
398
|
+
expect(computationCount).toBe(1)
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
test('should handle errors in aborted computations', async function() {
|
|
402
|
+
// const startTime = performance.now()
|
|
403
|
+
const source = state(1)
|
|
404
|
+
const derived = computed(async () => {
|
|
405
|
+
await wait(100)
|
|
406
|
+
const value = source.get()
|
|
407
|
+
if (value === 2) throw new Error('Intentional error')
|
|
408
|
+
return value
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
/* derived.tap({
|
|
412
|
+
ok: v => {
|
|
413
|
+
console.log(`ok: ${v}, time: ${performance.now() - startTime}ms`)
|
|
414
|
+
},
|
|
415
|
+
nil: () => {
|
|
416
|
+
console.warn(`nil, time: ${performance.now() - startTime}ms`)
|
|
417
|
+
},
|
|
418
|
+
err: e => {
|
|
419
|
+
console.error(`err: ${e.message}, time: ${performance.now() - startTime}ms`)
|
|
420
|
+
}
|
|
421
|
+
}) */
|
|
422
|
+
|
|
423
|
+
// Start first computation
|
|
424
|
+
expect(derived.get()).toBe(UNSET)
|
|
425
|
+
|
|
426
|
+
// Change to error state before first computation completes
|
|
427
|
+
source.set(2)
|
|
428
|
+
await wait(110)
|
|
429
|
+
expect(() => derived.get()).toThrow('Intentional error')
|
|
430
|
+
|
|
431
|
+
// Change to normal state before second computation completes
|
|
432
|
+
source.set(3)
|
|
433
|
+
await wait(100)
|
|
434
|
+
expect(derived.get()).toBe(3)
|
|
435
|
+
|
|
436
|
+
})
|
|
437
|
+
})
|