Skip to content

Commit 3221e37

Browse files
committed
feat: add Task.allSuccesses and Task.prototype.validateError
1 parent a8cc72a commit 3221e37

File tree

6 files changed

+169
-5
lines changed

6 files changed

+169
-5
lines changed

docs/task-instance.md

+27
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,33 @@ type errorUnion = <E2>() => Task<E | E2, S>
9292
{% endtab %}
9393
{% endtabs %}
9494
95+
## validateError
96+
97+
Given a task with an unknown error type, use a type guard to validate the type.
98+
99+
{% tabs %}
100+
{% tab title="Usage" %}
101+
102+
```typescript
103+
const task: Task<string, number> = Task.fromPromise(fetch(URL)).validateError(
104+
isString,
105+
)
106+
```
107+
108+
{% endtab %}
109+
110+
{% tab title="Type Definition" %}
111+
112+
```typescript
113+
type validateError = <E, S, E2 extends E>(
114+
fn: (err: E) => err is E2,
115+
task: Task<E, S>,
116+
): Task<E2, S>
117+
```
118+
119+
{% endtab %}
120+
{% endtabs %}
121+
95122
## mapBoth
96123
97124
Given a task, provide mapping functions for both the success and fail states. Results in a new task which has the type of the two mapping function results.

docs/task-static.md

+28-2
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,32 @@ type all<E, S> = (
164164
{% endtab %}
165165
{% endtabs %}
166166
167+
## allSuccesses
168+
169+
Creates a task that will always run an array of tasks in **parallel**. The result in a new task which returns the successful results as an array, in the same order as the tasks were given. Failed tasks will be excluded.
170+
171+
This can never fail, only return an empty array. Unliked `Task.all`, the resulting array is in order of success, not initial input order.
172+
173+
{% tabs %}
174+
{% tab title="Usage" %}
175+
176+
```typescript
177+
const task: Task<never, number[]> = Task.allSuccesses([fail("Err"), of(10)])
178+
```
179+
180+
{% endtab %}
181+
182+
{% tab title="Type Definition" %}
183+
184+
```typescript
185+
type allSuccesses = <E, S>(
186+
tasks: Array<Task<E, S>>,
187+
): Task<never, S[]>
188+
```
189+
190+
{% endtab %}
191+
{% endtabs %}
192+
167193
## sequence
168194
169195
Creates a task that will always run an array of tasks **serially**. The result in a new task which returns the successful results as an array, in the same order as the tasks were given. If any task fails, the resulting task will fail with that error.
@@ -294,7 +320,7 @@ Promise's do not track an error type (one of the reasons Tasks are more powerful
294320
{% tab title="Usage" %}
295321
296322
```typescript
297-
const task: Task<never, Response> = Task.fromLazyPromise(() => fetch(URL))
323+
const task: Task<unknown, Response> = Task.fromLazyPromise(() => fetch(URL))
298324
```
299325

300326
{% endtab %}
@@ -319,7 +345,7 @@ Given a function which returns a Promise, create a new function which given the
319345
320346
```typescript
321347
const taskFetch = wrapPromiseCreator(fetch)
322-
const task: Task<never, Response> = taskFetch(URL)
348+
const task: Task<unknown, Response> = taskFetch(URL)
323349
```
324350

325351
{% endtab %}

src/Task/Task.ts

+54-1
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ export const firstSuccess = <E, S>(tasks: Array<Task<E, S>>): Task<E[], S> =>
387387
})
388388

389389
/**
390-
* Given an array of task which return a result, return a new task which results an array of results.
390+
* Given an array of task which return a result, return a new task which returns an array of results.
391391
* @alias collect
392392
* @param tasks The tasks to run in parallel.
393393
*/
@@ -428,6 +428,42 @@ export const all = <E, S>(tasks: Array<Task<E, S>>): Task<E, S[]> =>
428428
)
429429
})
430430

431+
/**
432+
* Given an array of task which return a result, return a new task which returns an array of successful results.
433+
* @param tasks The tasks to run in parallel.
434+
*/
435+
export const allSuccesses = <E, S>(
436+
tasks: Array<Task<E, S>>,
437+
): Task<never, S[]> =>
438+
tasks.length === 0
439+
? of([])
440+
: new Task<never, S[]>((_reject, resolve) => {
441+
let runningTasks = tasks.length
442+
443+
const results: S[] = []
444+
445+
return tasks.map(task =>
446+
task.fork(
447+
() => {
448+
runningTasks -= 1
449+
450+
if (runningTasks === 0) {
451+
resolve(results)
452+
}
453+
},
454+
(result: S) => {
455+
runningTasks -= 1
456+
457+
results.push(result)
458+
459+
if (runningTasks === 0) {
460+
resolve(results)
461+
}
462+
},
463+
),
464+
)
465+
})
466+
431467
/**
432468
* Creates a task that waits for two tasks of different types to
433469
* resolve as a two-tuple of the results.
@@ -553,6 +589,18 @@ export const mapError = <E, S, E2>(
553589
task.fork(error => reject(fn(error)), resolve),
554590
)
555591

592+
export const validateError = <E, S, E2 extends E>(
593+
fn: (err: E) => err is E2,
594+
task: Task<E, S>,
595+
): Task<E2, S> =>
596+
mapError(err => {
597+
if (!fn(err)) {
598+
throw new Error(`validateError failed`)
599+
}
600+
601+
return err
602+
}, task)
603+
556604
export const errorUnion = <E, S, E2>(task: Task<E, S>): Task<E | E2, S> => task
557605

558606
/**
@@ -703,6 +751,7 @@ export class Task<E, S> implements PromiseLike<S> {
703751
public static succeedIn = succeedIn
704752
public static of = succeed
705753
public static all = all
754+
public static allSuccesses = allSuccesses
706755
public static sequence = sequence
707756
public static firstSuccess = firstSuccess
708757
public static never = never
@@ -845,6 +894,10 @@ export class Task<E, S> implements PromiseLike<S> {
845894
return mapError(fn, this)
846895
}
847896

897+
public validateError<E2 extends E>(fn: (err: E) => err is E2): Task<E2, S> {
898+
return validateError(fn, this)
899+
}
900+
848901
public errorUnion<E2>(): Task<E | E2, S> {
849902
return errorUnion<E, S, E2>(this)
850903
}
+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { allSuccesses, failIn, succeedIn } from "../Task"
2+
import { ERROR_RESULT } from "./util"
3+
4+
describe("allSuccesses", () => {
5+
test("should run all tasks in parallel and return an array of results in their original order", () => {
6+
const resolve = jest.fn()
7+
const reject = jest.fn()
8+
9+
allSuccesses([succeedIn(200, "A"), succeedIn(100, "B")]).fork(
10+
reject,
11+
resolve,
12+
)
13+
14+
jest.advanceTimersByTime(250)
15+
16+
expect(reject).not.toBeCalled()
17+
expect(resolve).toBeCalledWith(["B", "A"])
18+
})
19+
20+
test("should allow errors", () => {
21+
const resolve = jest.fn()
22+
const reject = jest.fn()
23+
24+
allSuccesses([failIn(200, ERROR_RESULT), succeedIn(100, "B")]).fork(
25+
reject,
26+
resolve,
27+
)
28+
29+
jest.advanceTimersByTime(250)
30+
31+
expect(reject).not.toBeCalled()
32+
expect(resolve).toBeCalledWith(["B"])
33+
})
34+
})

src/Task/__tests__/fromPromise.spec.ts

+20-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { fromPromise } from "../Task"
2-
import { ERROR_RESULT, SUCCESS_RESULT } from "./util"
1+
import { fromPromise, Task } from "../Task"
2+
import { ERROR_RESULT, ERROR_TYPE, isError, SUCCESS_RESULT } from "./util"
33

44
describe("fromPromise", () => {
55
test("should succeed when a promise succeeds", async () => {
@@ -39,4 +39,22 @@ describe("fromPromise", () => {
3939
expect(resolve).toBeCalledWith(SUCCESS_RESULT)
4040
expect(reject).not.toBeCalled()
4141
})
42+
43+
test("should be able to type guard error type", async () => {
44+
const resolve = jest.fn()
45+
const reject = jest.fn()
46+
47+
const promise = Promise.reject(ERROR_RESULT)
48+
const verifyType = (t: Task<ERROR_TYPE, never>) => t
49+
50+
fromPromise(promise)
51+
.validateError(isError)
52+
.map(verifyType)
53+
.fork(reject, resolve)
54+
55+
await promise.catch(() => void 0)
56+
57+
expect(reject).toBeCalledWith(ERROR_RESULT)
58+
expect(resolve).not.toBeCalled()
59+
})
4260
})

src/Task/__tests__/util.ts

+6
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,8 @@
11
export const SUCCESS_RESULT = "__SUCCESS__"
22
export const ERROR_RESULT = "__ERROR__"
3+
4+
export type SUCCESS_TYPE = "__SUCCESS__"
5+
export type ERROR_TYPE = "__ERROR__"
6+
7+
export const isSuccess = (v: unknown): v is SUCCESS_TYPE => v === SUCCESS_RESULT
8+
export const isError = (v: unknown): v is ERROR_TYPE => v === ERROR_RESULT

0 commit comments

Comments
 (0)