diff --git a/docs/task-instance.md b/docs/task-instance.md index 29f79619..39d7da08 100644 --- a/docs/task-instance.md +++ b/docs/task-instance.md @@ -92,6 +92,33 @@ type errorUnion = () => Task {% 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 = Task.fromPromise(fetch(URL)).validateError( + isString, +) +``` + +{% endtab %} + +{% tab title="Type Definition" %} + +```typescript +type validateError = ( + fn: (err: E) => err is E2, + task: Task, +): Task +``` + +{% 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. diff --git a/docs/task-static.md b/docs/task-static.md index c5c3c090..06556ba1 100644 --- a/docs/task-static.md +++ b/docs/task-static.md @@ -164,6 +164,32 @@ type all = ( {% 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 = Task.allSuccesses([fail("Err"), of(10)]) +``` + +{% endtab %} + +{% tab title="Type Definition" %} + +```typescript +type allSuccesses = ( + tasks: Array>, +): Task +``` + +{% 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. @@ -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 = Task.fromLazyPromise(() => fetch(URL)) +const task: Task = Task.fromLazyPromise(() => fetch(URL)) ``` {% endtab %} @@ -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 = taskFetch(URL) +const task: Task = taskFetch(URL) ``` {% endtab %} diff --git a/src/Task/Task.ts b/src/Task/Task.ts index 5d5468e0..ee2e58c7 100644 --- a/src/Task/Task.ts +++ b/src/Task/Task.ts @@ -387,7 +387,7 @@ export const firstSuccess = (tasks: Array>): Task => }) /** - * 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. */ @@ -428,6 +428,42 @@ export const all = (tasks: Array>): Task => ) }) +/** + * 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 = ( + tasks: Array>, +): Task => + tasks.length === 0 + ? of([]) + : new Task((_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. @@ -553,6 +589,18 @@ export const mapError = ( task.fork(error => reject(fn(error)), resolve), ) +export const validateError = ( + fn: (err: E) => err is E2, + task: Task, +): Task => + mapError(err => { + if (!fn(err)) { + throw new Error(`validateError failed`) + } + + return err + }, task) + export const errorUnion = (task: Task): Task => task /** @@ -703,6 +751,7 @@ export class Task implements PromiseLike { 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 @@ -845,6 +894,10 @@ export class Task implements PromiseLike { return mapError(fn, this) } + public validateError(fn: (err: E) => err is E2): Task { + return validateError(fn, this) + } + public errorUnion(): Task { return errorUnion(this) } diff --git a/src/Task/__tests__/allSuccesses.spec.ts b/src/Task/__tests__/allSuccesses.spec.ts new file mode 100644 index 00000000..26b415df --- /dev/null +++ b/src/Task/__tests__/allSuccesses.spec.ts @@ -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"]) + }) +}) diff --git a/src/Task/__tests__/fromPromise.spec.ts b/src/Task/__tests__/fromPromise.spec.ts index 68b1f740..689e5d41 100644 --- a/src/Task/__tests__/fromPromise.spec.ts +++ b/src/Task/__tests__/fromPromise.spec.ts @@ -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 () => { @@ -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) => t + + fromPromise(promise) + .validateError(isError) + .map(verifyType) + .fork(reject, resolve) + + await promise.catch(() => void 0) + + expect(reject).toBeCalledWith(ERROR_RESULT) + expect(resolve).not.toBeCalled() + }) }) diff --git a/src/Task/__tests__/util.ts b/src/Task/__tests__/util.ts index e1dfc990..44b7f531 100644 --- a/src/Task/__tests__/util.ts +++ b/src/Task/__tests__/util.ts @@ -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