@zeix/cause-effect 0.14.0 → 0.14.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/README.md +68 -72
- package/index.d.ts +6 -8
- package/index.js +1 -1
- package/index.ts +17 -6
- package/package.json +1 -1
- package/src/computed.d.ts +10 -8
- package/src/computed.ts +135 -11
- package/src/effect.d.ts +2 -4
- package/src/effect.ts +33 -50
- package/src/scheduler.d.ts +13 -4
- package/src/scheduler.ts +36 -13
- package/src/signal.d.ts +5 -10
- package/src/signal.ts +7 -13
- package/src/state.d.ts +2 -1
- package/src/state.ts +1 -1
- package/src/util.d.ts +1 -5
- package/src/util.ts +2 -22
- package/test/batch.test.ts +3 -3
- package/test/benchmark.test.ts +2 -2
- package/test/computed.test.ts +47 -49
- package/test/effect.test.ts +6 -6
- 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/test/computed.test.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { describe, test, expect } from 'bun:test'
|
|
2
2
|
import {
|
|
3
3
|
state,
|
|
4
|
-
|
|
5
|
-
task,
|
|
4
|
+
computed,
|
|
6
5
|
UNSET,
|
|
7
6
|
isComputed,
|
|
8
7
|
isState,
|
|
@@ -19,37 +18,37 @@ const increment = (n: number) => (Number.isFinite(n) ? n + 1 : UNSET)
|
|
|
19
18
|
describe('Computed', function () {
|
|
20
19
|
test('should identify computed signals with isComputed()', () => {
|
|
21
20
|
const count = state(42)
|
|
22
|
-
const doubled =
|
|
21
|
+
const doubled = computed(() => count.get() * 2)
|
|
23
22
|
expect(isComputed(doubled)).toBe(true)
|
|
24
23
|
expect(isState(doubled)).toBe(false)
|
|
25
24
|
})
|
|
26
25
|
|
|
27
26
|
test('should compute a function', function () {
|
|
28
|
-
const derived =
|
|
27
|
+
const derived = computed(() => 1 + 2)
|
|
29
28
|
expect(derived.get()).toBe(3)
|
|
30
29
|
})
|
|
31
30
|
|
|
32
31
|
test('should compute function dependent on a signal', function () {
|
|
33
32
|
const cause = state(42)
|
|
34
|
-
const derived =
|
|
33
|
+
const derived = computed(() => cause.get() + 1)
|
|
35
34
|
expect(derived.get()).toBe(43)
|
|
36
35
|
})
|
|
37
36
|
|
|
38
37
|
test('should compute function dependent on an updated signal', function () {
|
|
39
38
|
const cause = state(42)
|
|
40
|
-
const derived =
|
|
39
|
+
const derived = computed(() => cause.get() + 1)
|
|
41
40
|
cause.set(24)
|
|
42
41
|
expect(derived.get()).toBe(25)
|
|
43
42
|
})
|
|
44
43
|
|
|
45
44
|
test('should compute function dependent on an async signal', async function () {
|
|
46
45
|
const status = state('pending')
|
|
47
|
-
const promised =
|
|
46
|
+
const promised = computed(async () => {
|
|
48
47
|
await wait(100)
|
|
49
48
|
status.set('success')
|
|
50
49
|
return 42
|
|
51
50
|
})
|
|
52
|
-
const derived =
|
|
51
|
+
const derived = computed(() => increment(promised.get()))
|
|
53
52
|
expect(derived.get()).toBe(UNSET)
|
|
54
53
|
expect(status.get()).toBe('pending')
|
|
55
54
|
await wait(110)
|
|
@@ -60,13 +59,13 @@ describe('Computed', function () {
|
|
|
60
59
|
test('should handle errors from an async signal gracefully', async function () {
|
|
61
60
|
const status = state('pending')
|
|
62
61
|
const error = state('')
|
|
63
|
-
const promised =
|
|
62
|
+
const promised = computed(async () => {
|
|
64
63
|
await wait(100)
|
|
65
64
|
status.set('error')
|
|
66
65
|
error.set('error occurred')
|
|
67
66
|
return 0
|
|
68
67
|
})
|
|
69
|
-
const derived =
|
|
68
|
+
const derived = computed(() => increment(promised.get()))
|
|
70
69
|
expect(derived.get()).toBe(UNSET)
|
|
71
70
|
expect(status.get()).toBe('pending')
|
|
72
71
|
await wait(110)
|
|
@@ -75,15 +74,15 @@ describe('Computed', function () {
|
|
|
75
74
|
})
|
|
76
75
|
|
|
77
76
|
test('should compute task signals in parallel without waterfalls', async function () {
|
|
78
|
-
const a =
|
|
77
|
+
const a = computed(async () => {
|
|
79
78
|
await wait(100)
|
|
80
79
|
return 10
|
|
81
80
|
})
|
|
82
|
-
const b =
|
|
81
|
+
const b = computed(async () => {
|
|
83
82
|
await wait(100)
|
|
84
83
|
return 20
|
|
85
84
|
})
|
|
86
|
-
const c =
|
|
85
|
+
const c = computed(() => {
|
|
87
86
|
const aValue = a.get()
|
|
88
87
|
const bValue = b.get()
|
|
89
88
|
return aValue === UNSET || bValue === UNSET
|
|
@@ -97,17 +96,17 @@ describe('Computed', function () {
|
|
|
97
96
|
|
|
98
97
|
test('should compute function dependent on a chain of computed states dependent on a signal', function () {
|
|
99
98
|
const x = state(42)
|
|
100
|
-
const a =
|
|
101
|
-
const b =
|
|
102
|
-
const c =
|
|
99
|
+
const a = computed(() => x.get() + 1)
|
|
100
|
+
const b = computed(() => a.get() * 2)
|
|
101
|
+
const c = computed(() => b.get() + 1)
|
|
103
102
|
expect(c.get()).toBe(87)
|
|
104
103
|
})
|
|
105
104
|
|
|
106
105
|
test('should compute function dependent on a chain of computed states dependent on an updated signal', function () {
|
|
107
106
|
const x = state(42)
|
|
108
|
-
const a =
|
|
109
|
-
const b =
|
|
110
|
-
const c =
|
|
107
|
+
const a = computed(() => x.get() + 1)
|
|
108
|
+
const b = computed(() => a.get() * 2)
|
|
109
|
+
const c = computed(() => b.get() + 1)
|
|
111
110
|
x.set(24)
|
|
112
111
|
expect(c.get()).toBe(51)
|
|
113
112
|
})
|
|
@@ -115,9 +114,9 @@ describe('Computed', function () {
|
|
|
115
114
|
test('should drop X->B->X updates', function () {
|
|
116
115
|
let count = 0
|
|
117
116
|
const x = state(2)
|
|
118
|
-
const a =
|
|
119
|
-
const b =
|
|
120
|
-
const c =
|
|
117
|
+
const a = computed(() => x.get() - 1)
|
|
118
|
+
const b = computed(() => x.get() + a.get())
|
|
119
|
+
const c = computed(() => {
|
|
121
120
|
count++
|
|
122
121
|
return 'c: ' + b.get()
|
|
123
122
|
})
|
|
@@ -131,9 +130,9 @@ describe('Computed', function () {
|
|
|
131
130
|
test('should only update every signal once (diamond graph)', function () {
|
|
132
131
|
let count = 0
|
|
133
132
|
const x = state('a')
|
|
134
|
-
const a =
|
|
135
|
-
const b =
|
|
136
|
-
const c =
|
|
133
|
+
const a = computed(() => x.get())
|
|
134
|
+
const b = computed(() => x.get())
|
|
135
|
+
const c = computed(() => {
|
|
137
136
|
count++
|
|
138
137
|
return a.get() + ' ' + b.get()
|
|
139
138
|
})
|
|
@@ -148,10 +147,10 @@ describe('Computed', function () {
|
|
|
148
147
|
test('should only update every signal once (diamond graph + tail)', function () {
|
|
149
148
|
let count = 0
|
|
150
149
|
const x = state('a')
|
|
151
|
-
const a =
|
|
152
|
-
const b =
|
|
153
|
-
const c =
|
|
154
|
-
const d =
|
|
150
|
+
const a = computed(() => x.get())
|
|
151
|
+
const b = computed(() => x.get())
|
|
152
|
+
const c = computed(() => a.get() + ' ' + b.get())
|
|
153
|
+
const d = computed(() => {
|
|
155
154
|
count++
|
|
156
155
|
return c.get()
|
|
157
156
|
})
|
|
@@ -166,7 +165,7 @@ describe('Computed', function () {
|
|
|
166
165
|
const a = state(3)
|
|
167
166
|
const b = state(4)
|
|
168
167
|
let count = 0
|
|
169
|
-
const sum =
|
|
168
|
+
const sum = computed(() => {
|
|
170
169
|
count++
|
|
171
170
|
return a.get() + b.get()
|
|
172
171
|
})
|
|
@@ -189,11 +188,11 @@ describe('Computed', function () {
|
|
|
189
188
|
test('should bail out if result is the same', function () {
|
|
190
189
|
let count = 0
|
|
191
190
|
const x = state('a')
|
|
192
|
-
const a =
|
|
191
|
+
const a = computed(() => {
|
|
193
192
|
x.get()
|
|
194
193
|
return 'foo'
|
|
195
194
|
})
|
|
196
|
-
const b =
|
|
195
|
+
const b = computed(() => {
|
|
197
196
|
count++
|
|
198
197
|
return a.get()
|
|
199
198
|
})
|
|
@@ -209,9 +208,9 @@ describe('Computed', function () {
|
|
|
209
208
|
test('should block if result remains unchanged', function () {
|
|
210
209
|
let count = 0
|
|
211
210
|
const x = state(42)
|
|
212
|
-
const a =
|
|
213
|
-
const b =
|
|
214
|
-
const c =
|
|
211
|
+
const a = computed(() => x.get() % 2)
|
|
212
|
+
const b = computed(() => (a.get() ? 'odd' : 'even'))
|
|
213
|
+
const c = computed(() => {
|
|
215
214
|
count++
|
|
216
215
|
return `c: ${b.get()}`
|
|
217
216
|
})
|
|
@@ -226,11 +225,11 @@ describe('Computed', function () {
|
|
|
226
225
|
|
|
227
226
|
test('should detect and throw error for circular dependencies', function () {
|
|
228
227
|
const a = state(1)
|
|
229
|
-
const b =
|
|
230
|
-
const c =
|
|
228
|
+
const b = computed(() => c.get() + 1)
|
|
229
|
+
const c = computed(() => b.get() + a.get())
|
|
231
230
|
expect(() => {
|
|
232
231
|
b.get() // This should trigger the circular dependency
|
|
233
|
-
}).toThrow('Circular dependency in
|
|
232
|
+
}).toThrow('Circular dependency in computed detected')
|
|
234
233
|
expect(a.get()).toBe(1)
|
|
235
234
|
})
|
|
236
235
|
|
|
@@ -238,13 +237,13 @@ describe('Computed', function () {
|
|
|
238
237
|
let okCount = 0
|
|
239
238
|
let errCount = 0
|
|
240
239
|
const x = state(0)
|
|
241
|
-
const a =
|
|
240
|
+
const a = computed(() => {
|
|
242
241
|
if (x.get() === 1) throw new Error('Calculation error')
|
|
243
242
|
return 1
|
|
244
243
|
})
|
|
245
244
|
|
|
246
245
|
// Replace matcher with try/catch in a computed
|
|
247
|
-
const b =
|
|
246
|
+
const b = computed(() => {
|
|
248
247
|
try {
|
|
249
248
|
a.get() // just check if it works
|
|
250
249
|
return `c: success`
|
|
@@ -253,7 +252,7 @@ describe('Computed', function () {
|
|
|
253
252
|
return `c: recovered`
|
|
254
253
|
}
|
|
255
254
|
})
|
|
256
|
-
const c =
|
|
255
|
+
const c = computed(() => {
|
|
257
256
|
okCount++
|
|
258
257
|
return b.get()
|
|
259
258
|
})
|
|
@@ -276,7 +275,7 @@ describe('Computed', function () {
|
|
|
276
275
|
|
|
277
276
|
test('should create an effect that reacts on async computed changes', async function () {
|
|
278
277
|
const cause = state(42)
|
|
279
|
-
const derived =
|
|
278
|
+
const derived = computed(async () => {
|
|
280
279
|
await wait(100)
|
|
281
280
|
return cause.get() + 1
|
|
282
281
|
})
|
|
@@ -306,11 +305,11 @@ describe('Computed', function () {
|
|
|
306
305
|
|
|
307
306
|
test('should handle complex computed signal with error and async dependencies', async function () {
|
|
308
307
|
const toggleState = state(true)
|
|
309
|
-
const errorProne =
|
|
308
|
+
const errorProne = computed(() => {
|
|
310
309
|
if (toggleState.get()) throw new Error('Intentional error')
|
|
311
310
|
return 42
|
|
312
311
|
})
|
|
313
|
-
const asyncValue =
|
|
312
|
+
const asyncValue = computed(async () => {
|
|
314
313
|
await wait(50)
|
|
315
314
|
return 10
|
|
316
315
|
})
|
|
@@ -319,8 +318,7 @@ describe('Computed', function () {
|
|
|
319
318
|
let errCount = 0
|
|
320
319
|
// let _result: number = 0
|
|
321
320
|
|
|
322
|
-
|
|
323
|
-
const complexComputed = memo(() => {
|
|
321
|
+
const complexComputed = computed(() => {
|
|
324
322
|
try {
|
|
325
323
|
const x = errorProne.get()
|
|
326
324
|
const y = asyncValue.get()
|
|
@@ -355,7 +353,7 @@ describe('Computed', function () {
|
|
|
355
353
|
test('should handle signal changes during async computation', async function () {
|
|
356
354
|
const source = state(1)
|
|
357
355
|
let computationCount = 0
|
|
358
|
-
const derived =
|
|
356
|
+
const derived = computed(async abort => {
|
|
359
357
|
computationCount++
|
|
360
358
|
expect(abort?.aborted).toBe(false)
|
|
361
359
|
await wait(100)
|
|
@@ -376,7 +374,7 @@ describe('Computed', function () {
|
|
|
376
374
|
test('should handle multiple rapid changes during async computation', async function () {
|
|
377
375
|
const source = state(1)
|
|
378
376
|
let computationCount = 0
|
|
379
|
-
const derived =
|
|
377
|
+
const derived = computed(async abort => {
|
|
380
378
|
computationCount++
|
|
381
379
|
expect(abort?.aborted).toBe(false)
|
|
382
380
|
await wait(100)
|
|
@@ -400,7 +398,7 @@ describe('Computed', function () {
|
|
|
400
398
|
|
|
401
399
|
test('should handle errors in aborted computations', async function () {
|
|
402
400
|
const source = state(1)
|
|
403
|
-
const derived =
|
|
401
|
+
const derived = computed(async () => {
|
|
404
402
|
await wait(100)
|
|
405
403
|
const value = source.get()
|
|
406
404
|
if (value === 2) throw new Error('Intentional error')
|
package/test/effect.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, test, expect, mock } from 'bun:test'
|
|
2
|
-
import { state,
|
|
2
|
+
import { state, computed, effect, UNSET } from '../'
|
|
3
3
|
|
|
4
4
|
/* === Utility Functions === */
|
|
5
5
|
|
|
@@ -21,11 +21,11 @@ describe('Effect', function () {
|
|
|
21
21
|
})
|
|
22
22
|
|
|
23
23
|
test('should be triggered after computed async signals resolve without waterfalls', async function () {
|
|
24
|
-
const a =
|
|
24
|
+
const a = computed(async () => {
|
|
25
25
|
await wait(100)
|
|
26
26
|
return 10
|
|
27
27
|
})
|
|
28
|
-
const b =
|
|
28
|
+
const b = computed(async () => {
|
|
29
29
|
await wait(100)
|
|
30
30
|
return 20
|
|
31
31
|
})
|
|
@@ -62,7 +62,7 @@ describe('Effect', function () {
|
|
|
62
62
|
|
|
63
63
|
test('should handle errors in effects', function () {
|
|
64
64
|
const a = state(1)
|
|
65
|
-
const b =
|
|
65
|
+
const b = computed(() => {
|
|
66
66
|
const v = a.get()
|
|
67
67
|
if (v > 5) throw new Error('Value too high')
|
|
68
68
|
return v * 2
|
|
@@ -99,7 +99,7 @@ describe('Effect', function () {
|
|
|
99
99
|
})
|
|
100
100
|
|
|
101
101
|
test('should handle UNSET values in effects', async function () {
|
|
102
|
-
const a =
|
|
102
|
+
const a = computed(async () => {
|
|
103
103
|
await wait(100)
|
|
104
104
|
return 42
|
|
105
105
|
})
|
|
@@ -133,7 +133,7 @@ describe('Effect', function () {
|
|
|
133
133
|
|
|
134
134
|
try {
|
|
135
135
|
const a = state(1)
|
|
136
|
-
const b =
|
|
136
|
+
const b = computed(() => {
|
|
137
137
|
const v = a.get()
|
|
138
138
|
if (v > 5) throw new Error('Value too high')
|
|
139
139
|
return v * 2
|
package/src/memo.d.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { type Computed } from './computed';
|
|
2
|
-
type MemoCallback<T extends {} & {
|
|
3
|
-
then?: void;
|
|
4
|
-
}> = () => T;
|
|
5
|
-
/**
|
|
6
|
-
* Create a derived signal for synchronous computations
|
|
7
|
-
*
|
|
8
|
-
* @since 0.14.0
|
|
9
|
-
* @param {MemoCallback<T>} fn - synchronous computation callback
|
|
10
|
-
* @returns {Computed<T>} - Computed signal
|
|
11
|
-
*/
|
|
12
|
-
declare const memo: <T extends {}>(fn: MemoCallback<T>) => Computed<T>;
|
|
13
|
-
export { type MemoCallback, memo };
|
package/src/memo.ts
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { UNSET } from './signal'
|
|
2
|
-
import { CircularDependencyError } from './util'
|
|
3
|
-
import {
|
|
4
|
-
type Cleanup,
|
|
5
|
-
type Watcher,
|
|
6
|
-
flush,
|
|
7
|
-
notify,
|
|
8
|
-
subscribe,
|
|
9
|
-
watch,
|
|
10
|
-
} from './scheduler'
|
|
11
|
-
import { type Computed, TYPE_COMPUTED } from './computed'
|
|
12
|
-
|
|
13
|
-
/* === Types === */
|
|
14
|
-
|
|
15
|
-
type MemoCallback<T extends {} & { then?: void }> = () => T
|
|
16
|
-
|
|
17
|
-
/* === Functions === */
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Create a derived signal for synchronous computations
|
|
21
|
-
*
|
|
22
|
-
* @since 0.14.0
|
|
23
|
-
* @param {MemoCallback<T>} fn - synchronous computation callback
|
|
24
|
-
* @returns {Computed<T>} - Computed signal
|
|
25
|
-
*/
|
|
26
|
-
const memo = <T extends {}>(fn: MemoCallback<T>): Computed<T> => {
|
|
27
|
-
const watchers: Set<Watcher> = new Set()
|
|
28
|
-
|
|
29
|
-
// Internal state - simplified for sync only
|
|
30
|
-
let value: T = UNSET
|
|
31
|
-
let error: Error | undefined
|
|
32
|
-
let dirty = true
|
|
33
|
-
let computing = false
|
|
34
|
-
|
|
35
|
-
// Called when notified from sources (push)
|
|
36
|
-
const mark = (() => {
|
|
37
|
-
dirty = true
|
|
38
|
-
if (watchers.size) {
|
|
39
|
-
notify(watchers)
|
|
40
|
-
} else {
|
|
41
|
-
mark.cleanups.forEach(fn => fn())
|
|
42
|
-
mark.cleanups.clear()
|
|
43
|
-
}
|
|
44
|
-
}) as Watcher
|
|
45
|
-
mark.cleanups = new Set<Cleanup>()
|
|
46
|
-
|
|
47
|
-
// Called when requested by dependencies (pull)
|
|
48
|
-
const compute = () =>
|
|
49
|
-
watch(() => {
|
|
50
|
-
if (computing) throw new CircularDependencyError('memo')
|
|
51
|
-
computing = true
|
|
52
|
-
try {
|
|
53
|
-
const result = fn()
|
|
54
|
-
if (null == result || UNSET === result) {
|
|
55
|
-
value = UNSET
|
|
56
|
-
error = undefined
|
|
57
|
-
} else {
|
|
58
|
-
value = result
|
|
59
|
-
dirty = false
|
|
60
|
-
error = undefined
|
|
61
|
-
}
|
|
62
|
-
} catch (e) {
|
|
63
|
-
value = UNSET
|
|
64
|
-
error = e instanceof Error ? e : new Error(String(e))
|
|
65
|
-
} finally {
|
|
66
|
-
computing = false
|
|
67
|
-
}
|
|
68
|
-
}, mark)
|
|
69
|
-
|
|
70
|
-
const c: Computed<T> = {
|
|
71
|
-
[Symbol.toStringTag]: TYPE_COMPUTED,
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Get the current value of the computed
|
|
75
|
-
*
|
|
76
|
-
* @returns {T} - current value of the computed
|
|
77
|
-
*/
|
|
78
|
-
get: (): T => {
|
|
79
|
-
subscribe(watchers)
|
|
80
|
-
flush()
|
|
81
|
-
if (dirty) compute()
|
|
82
|
-
if (error) throw error
|
|
83
|
-
return value
|
|
84
|
-
},
|
|
85
|
-
}
|
|
86
|
-
return c
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/* === Exports === */
|
|
90
|
-
|
|
91
|
-
export { type MemoCallback, memo }
|
package/src/task.d.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
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 }
|