Skip to content

Commit

Permalink
feat: add Task.allSuccesses and Task.prototype.validateError
Browse files Browse the repository at this point in the history
  • Loading branch information
tdreyno committed Jan 8, 2021
1 parent a8cc72a commit 3221e37
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 5 deletions.
27 changes: 27 additions & 0 deletions docs/task-instance.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,33 @@ type errorUnion = <E2>() => Task<E | E2, S>
{% endtab %}
{% endtabs %}
## validateError
Given a task with an unknown error type, use a type guard to validate the type.
{% tabs %}
{% tab title="Usage" %}
```typescript
const task: Task<string, number> = Task.fromPromise(fetch(URL)).validateError(
isString,
)
```

{% endtab %}

{% tab title="Type Definition" %}

```typescript
type validateError = <E, S, E2 extends E>(
fn: (err: E) => err is E2,
task: Task<E, S>,
): Task<E2, S>
```
{% endtab %}
{% endtabs %}
## mapBoth
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.
Expand Down
30 changes: 28 additions & 2 deletions docs/task-static.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,32 @@ type all<E, S> = (
{% endtab %}
{% endtabs %}
## allSuccesses
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.
This can never fail, only return an empty array. Unliked `Task.all`, the resulting array is in order of success, not initial input order.
{% tabs %}
{% tab title="Usage" %}
```typescript
const task: Task<never, number[]> = Task.allSuccesses([fail("Err"), of(10)])
```

{% endtab %}

{% tab title="Type Definition" %}

```typescript
type allSuccesses = <E, S>(
tasks: Array<Task<E, S>>,
): Task<never, S[]>
```
{% endtab %}
{% endtabs %}
## sequence
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.
Expand Down Expand Up @@ -294,7 +320,7 @@ Promise's do not track an error type (one of the reasons Tasks are more powerful
{% tab title="Usage" %}
```typescript
const task: Task<never, Response> = Task.fromLazyPromise(() => fetch(URL))
const task: Task<unknown, Response> = Task.fromLazyPromise(() => fetch(URL))
```

{% endtab %}
Expand All @@ -319,7 +345,7 @@ Given a function which returns a Promise, create a new function which given the
```typescript
const taskFetch = wrapPromiseCreator(fetch)
const task: Task<never, Response> = taskFetch(URL)
const task: Task<unknown, Response> = taskFetch(URL)
```

{% endtab %}
Expand Down
55 changes: 54 additions & 1 deletion src/Task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ export const firstSuccess = <E, S>(tasks: Array<Task<E, S>>): Task<E[], S> =>
})

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

/**
* Given an array of task which return a result, return a new task which returns an array of successful results.
* @param tasks The tasks to run in parallel.
*/
export const allSuccesses = <E, S>(
tasks: Array<Task<E, S>>,
): Task<never, S[]> =>
tasks.length === 0
? of([])
: new Task<never, S[]>((_reject, resolve) => {
let runningTasks = tasks.length

const results: S[] = []

return tasks.map(task =>
task.fork(
() => {
runningTasks -= 1

if (runningTasks === 0) {
resolve(results)
}
},
(result: S) => {
runningTasks -= 1

results.push(result)

if (runningTasks === 0) {
resolve(results)
}
},
),
)
})

/**
* Creates a task that waits for two tasks of different types to
* resolve as a two-tuple of the results.
Expand Down Expand Up @@ -553,6 +589,18 @@ export const mapError = <E, S, E2>(
task.fork(error => reject(fn(error)), resolve),
)

export const validateError = <E, S, E2 extends E>(
fn: (err: E) => err is E2,
task: Task<E, S>,
): Task<E2, S> =>
mapError(err => {
if (!fn(err)) {
throw new Error(`validateError failed`)
}

return err
}, task)

export const errorUnion = <E, S, E2>(task: Task<E, S>): Task<E | E2, S> => task

/**
Expand Down Expand Up @@ -703,6 +751,7 @@ export class Task<E, S> implements PromiseLike<S> {
public static succeedIn = succeedIn
public static of = succeed
public static all = all
public static allSuccesses = allSuccesses
public static sequence = sequence
public static firstSuccess = firstSuccess
public static never = never
Expand Down Expand Up @@ -845,6 +894,10 @@ export class Task<E, S> implements PromiseLike<S> {
return mapError(fn, this)
}

public validateError<E2 extends E>(fn: (err: E) => err is E2): Task<E2, S> {
return validateError(fn, this)
}

public errorUnion<E2>(): Task<E | E2, S> {
return errorUnion<E, S, E2>(this)
}
Expand Down
34 changes: 34 additions & 0 deletions src/Task/__tests__/allSuccesses.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { allSuccesses, failIn, succeedIn } from "../Task"
import { ERROR_RESULT } from "./util"

describe("allSuccesses", () => {
test("should run all tasks in parallel and return an array of results in their original order", () => {
const resolve = jest.fn()
const reject = jest.fn()

allSuccesses([succeedIn(200, "A"), succeedIn(100, "B")]).fork(
reject,
resolve,
)

jest.advanceTimersByTime(250)

expect(reject).not.toBeCalled()
expect(resolve).toBeCalledWith(["B", "A"])
})

test("should allow errors", () => {
const resolve = jest.fn()
const reject = jest.fn()

allSuccesses([failIn(200, ERROR_RESULT), succeedIn(100, "B")]).fork(
reject,
resolve,
)

jest.advanceTimersByTime(250)

expect(reject).not.toBeCalled()
expect(resolve).toBeCalledWith(["B"])
})
})
22 changes: 20 additions & 2 deletions src/Task/__tests__/fromPromise.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { fromPromise } from "../Task"
import { ERROR_RESULT, SUCCESS_RESULT } from "./util"
import { fromPromise, Task } from "../Task"
import { ERROR_RESULT, ERROR_TYPE, isError, SUCCESS_RESULT } from "./util"

describe("fromPromise", () => {
test("should succeed when a promise succeeds", async () => {
Expand Down Expand Up @@ -39,4 +39,22 @@ describe("fromPromise", () => {
expect(resolve).toBeCalledWith(SUCCESS_RESULT)
expect(reject).not.toBeCalled()
})

test("should be able to type guard error type", async () => {
const resolve = jest.fn()
const reject = jest.fn()

const promise = Promise.reject(ERROR_RESULT)
const verifyType = (t: Task<ERROR_TYPE, never>) => t

fromPromise(promise)
.validateError(isError)
.map(verifyType)
.fork(reject, resolve)

await promise.catch(() => void 0)

expect(reject).toBeCalledWith(ERROR_RESULT)
expect(resolve).not.toBeCalled()
})
})
6 changes: 6 additions & 0 deletions src/Task/__tests__/util.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
export const SUCCESS_RESULT = "__SUCCESS__"
export const ERROR_RESULT = "__ERROR__"

export type SUCCESS_TYPE = "__SUCCESS__"
export type ERROR_TYPE = "__ERROR__"

export const isSuccess = (v: unknown): v is SUCCESS_TYPE => v === SUCCESS_RESULT
export const isError = (v: unknown): v is ERROR_TYPE => v === ERROR_RESULT

0 comments on commit 3221e37

Please sign in to comment.