Skip to content

Commit

Permalink
feat(elseDo): Side effects for failed tasks
Browse files Browse the repository at this point in the history
  • Loading branch information
kofno committed Nov 22, 2019
1 parent cf19c6c commit 1601db5
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 30 deletions.
44 changes: 39 additions & 5 deletions src/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,10 @@ class Task<E, T> {
*/
public map<A>(f: (t: T) => A): Task<E, A> {
return new Task((reject, resolve) => {
return this.fn(err => reject(err), (a: T) => resolve(f(a)));
return this.fn(
err => reject(err),
(a: T) => resolve(f(a))
);
});
}

Expand All @@ -168,7 +171,10 @@ class Task<E, T> {
*/
public andThen<A>(f: (t: T) => Task<E, A>): Task<E, A> {
return new Task((reject, resolve) => {
return this.fn(err => reject(err), (a: T) => f(a).fork(reject, resolve));
return this.fn(
err => reject(err),
(a: T) => f(a).fork(reject, resolve)
);
});
}

Expand All @@ -192,7 +198,10 @@ class Task<E, T> {
*/
public andThenP<A>(f: (t: T) => Promise<A>): Task<E, A> {
return new Task((reject, resolve) => {
return this.fn(err => reject(err), (a: T) => f(a).then(resolve, reject));
return this.fn(
err => reject(err),
(a: T) => f(a).then(resolve, reject)
);
});
}

Expand All @@ -201,7 +210,10 @@ class Task<E, T> {
*/
public orElse<X>(f: (err: E) => Task<X, T>): Task<X, T> {
return new Task((reject, resolve) => {
return this.fn((x: E) => f(x).fork(reject, resolve), t => resolve(t));
return this.fn(
(x: E) => f(x).fork(reject, resolve),
t => resolve(t)
);
});
}

Expand All @@ -210,7 +222,10 @@ class Task<E, T> {
*/
public mapError<X>(f: (err: E) => X): Task<X, T> {
return new Task((reject, resolve) => {
return this.fn((e: E) => reject(f(e)), t => resolve(t));
return this.fn(
(e: E) => reject(f(e)),
t => resolve(t)
);
});
}

Expand Down Expand Up @@ -256,6 +271,25 @@ class Task<E, T> {
return v;
});
}

/**
* Inject a side-effectual function into a task call chain. Task themselves are the
* appropriate way to handle side-effects that you care about, but sometimes you
* may want to do an _fire-and-forget_ side effect. The most common example of this
* is performing a logging the current value of a Task.
*
* Task.fail('oops')
* .elseDo(v => console.log(v))
* .orElse(doSomethingWithError)
*
* `elseDo` will only run in the context of a failed task.
*/
public elseDo(fn: (err: E) => void): Task<E, T> {
return this.mapError(err => {
fn(err);
return err;
});
}
}

export default Task;
88 changes: 63 additions & 25 deletions tests/Task.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ const cancellable = new Task((reject, resolve: Resolve<string>) => {
test('Task.succeed', t => {
Task.succeed(42).fork(
_ => t.fail('Task 42 should always succeed'),
v => t.pass(`Task always succeeds with ${v}`),
v => t.pass(`Task always succeeds with ${v}`)
);
t.end();
});

test('Task.fail', t => {
Task.fail('Ooops!').fork(
err => t.pass(`Task always fails with ${err}`),
_ => t.fail('Task should always fail'),
_ => t.fail('Task should always fail')
);
t.end();
});
Expand All @@ -27,47 +27,65 @@ test('Task.map', t => {
.map(v => v - 12)
.fork(
_ => t.fail('Task should always succeed'),
result => t.pass(`Task succeeded with ${result}`),
result => t.pass(`Task succeeded with ${result}`)
);

Task.fail('Opps!')
.map(_ => t.fail('map should never run'))
.fork(err => t.pass(`Task errored with ${err}`), _ => t.fail('Task should have failed'));
.fork(
err => t.pass(`Task errored with ${err}`),
_ => t.fail('Task should have failed')
);

t.end();
});

test('Task.andThen', t => {
Task.succeed(42)
.andThen(v => Task.succeed(v - 12))
.fork(err => t.fail('Task should have succeeded'), v => t.pass(`Task succeeded with ${v}`));
.fork(
err => t.fail('Task should have succeeded'),
v => t.pass(`Task succeeded with ${v}`)
);

Task.succeed(42)
.andThen(v => Task.fail('Ooops!'))
.fork(err => t.pass(`Task failed with ${err}`), _ => t.fail('Task should have failed'));
.fork(
err => t.pass(`Task failed with ${err}`),
_ => t.fail('Task should have failed')
);

Task.fail('Oops!')
.andThen(_ => Task.succeed(42))
.fork(err => t.pass(`Task failed with ${err}`), _ => t.fail('Task should have failed'));
.fork(
err => t.pass(`Task failed with ${err}`),
_ => t.fail('Task should have failed')
);

t.end();
});

test('Task.orElse', t => {
Task.fail('Oops!')
.orElse(e => Task.fail(e.toUpperCase()))
.fork(err => t.pass(`Task failed with ${err}`), _ => t.fail('Task should have failed'));
.fork(
err => t.pass(`Task failed with ${err}`),
_ => t.fail('Task should have failed')
);

Task.fail('Oops!')
.orElse(e => Task.succeed(e))
.fork(
err => t.fail('Task should have become a success'),
v => t.pass(`Task succeeded with ${v}`),
v => t.pass(`Task succeeded with ${v}`)
);

Task.succeed(42)
.orElse(e => Task.fail('WAT!?'))
.fork(err => t.fail('Task should have succeeded'), v => t.pass(`Task succeeded with ${v}`));
.fork(
err => t.fail('Task should have succeeded'),
v => t.pass(`Task succeeded with ${v}`)
);

t.end();
});
Expand All @@ -77,7 +95,7 @@ test('Task.mapError', t => {
.mapError(e => e.toUpperCase())
.fork(
err => t.equal('OOPS!', err, `Task failed with ${err}`),
_ => t.fail('Task should have failed'),
_ => t.fail('Task should have failed')
);

t.end();
Expand All @@ -86,7 +104,7 @@ test('Task.mapError', t => {
test('Cancel task', t => {
const cancel = cancellable.fork(
err => t.fail(`Task should not have failed; ${err}`),
v => t.fail(`Task should never have finished; ${v}`),
v => t.fail(`Task should never have finished; ${v}`)
);
cancel();

Expand All @@ -98,7 +116,7 @@ test('Cancel mapped task', t => {

const cancel = task.fork(
err => t.fail(`Task should not have failed; ${err}`),
s => t.fail(`Task should never have finished; ${s}`),
s => t.fail(`Task should never have finished; ${s}`)
);
cancel();

Expand All @@ -112,12 +130,12 @@ test('Cancel sequenced tasks', t => {
resolve(s.toUpperCase());
// tslint:disable-next-line:no-empty
return () => {};
}),
})
);

const cancel = task.fork(
err => t.fail(`Task should not have failed; ${err}`),
s => t.fail(`Task should never have finished; ${s}`),
s => t.fail(`Task should never have finished; ${s}`)
);
cancel();

Expand All @@ -130,12 +148,12 @@ test('Cancel sequenced asynced tasks', t => {
new Task((reject, resolve) => {
const x = setTimeout(() => resolve(s.toUpperCase()), 3000);
return () => clearTimeout(x);
}),
})
);

const cancel = task.fork(
err => t.fail(`Task should not have failed; ${err}`),
s => t.fail(`Task should never have finished; ${s}`),
s => t.fail(`Task should never have finished; ${s}`)
);
cancel();

Expand All @@ -147,28 +165,28 @@ test('Promises', t => {
.map(n => n + 8)
.fork(
err => t.fail(`Task should have succeeded: ${err}`),
n => t.assert(n === 50, 'Promise converted to task'),
n => t.assert(n === 50, 'Promise converted to task')
);

Task.fromPromise(() => Promise.reject<number>('whoops!'))
.map(n => n + 8)
.fork(
err => t.pass(`Task handled a failed promise. Error: ${err}`),
n => t.fail(`Task should not have succeeded: ${n}`),
n => t.fail(`Task should not have succeeded: ${n}`)
);

Task.succeed(42)
.andThenP(n => Promise.resolve(n + 8))
.fork(
err => t.fail(`Promise should have resolved as a successful task: ${err}`),
n => t.assert(50 === n, 'Promise chained as a task'),
n => t.assert(50 === n, 'Promise chained as a task')
);

Task.succeed(42)
.andThenP(n => Promise.reject('Whoops!'))
.fork(
err => t.pass(`Promise failure chained as task. Error ${err}`),
n => t.fail(`Promise chain should not have succeeded: ${n}`),
n => t.fail(`Promise chain should not have succeeded: ${n}`)
);

t.end();
Expand All @@ -181,15 +199,15 @@ test('Task.assign', t => {
.assign('z', s => Task.succeed(String(s.x + s.y)))
.fork(
m => t.fail(`Should have succeeded: ${m}`),
value => t.deepEqual(value, { x: 42, y: 8, z: '50' }),
value => t.deepEqual(value, { x: 42, y: 8, z: '50' })
);
Task.succeed({})
.assign('x', Task.succeed(42))
.assign('y', Task.fail<string, number>('Ooops!'))
.assign('z', s => Task.succeed(String(s.x + s.y)))
.fork(
m => t.pass(`Expected a failure: ${m}`),
value => t.fail(`Expected a failure: ${JSON.stringify(value)}`),
value => t.fail(`Expected a failure: ${JSON.stringify(value)}`)
);

t.end();
Expand All @@ -198,13 +216,33 @@ test('Task.assign', t => {
test('Task.do', t => {
Task.succeed(42)
.do(v => t.pass('This is the side-effect'))
.fork(e => t.fail(`Should have succeeded: ${JSON.stringify(e)}`), v => t.equal(42, v));
.fork(
e => t.fail(`Should have succeeded: ${JSON.stringify(e)}`),
v => t.equal(42, v)
);

Task.fail('Oops!')
.do(v => t.fail('This is the side-effect'))
.fork(
e => t.pass(`Should fail: ${JSON.stringify(e)}`),
v => t.fail(`Should NOT be ok: ${JSON.stringify(v)}`),
v => t.fail(`Should NOT be ok: ${JSON.stringify(v)}`)
);
t.end();
});

test('Task.elseDo', t => {
Task.succeed(42)
.elseDo(v => t.fail('This is the side-effect'))
.fork(
e => t.fail(`Should have succeeded: ${JSON.stringify(e)}`),
v => t.equal(42, v)
);

Task.fail('Oops!')
.elseDo(v => t.pass('This is the side-effect'))
.fork(
e => t.pass(`Should fail: ${JSON.stringify(e)}`),
v => t.fail(`Should NOT be ok: ${JSON.stringify(v)}`)
);
t.end();
});

0 comments on commit 1601db5

Please sign in to comment.