From 1601db5bba84ccc2624fd3eb618651ef2dfc09e7 Mon Sep 17 00:00:00 2001 From: "Ryan L. Bell" Date: Fri, 22 Nov 2019 10:17:03 -0500 Subject: [PATCH] feat(elseDo): Side effects for failed tasks --- src/Task.ts | 44 ++++++++++++++++++++--- tests/Task.test.ts | 88 +++++++++++++++++++++++++++++++++------------- 2 files changed, 102 insertions(+), 30 deletions(-) diff --git a/src/Task.ts b/src/Task.ts index c8cd5d0..b9287cb 100644 --- a/src/Task.ts +++ b/src/Task.ts @@ -159,7 +159,10 @@ class Task { */ public map(f: (t: T) => A): Task { 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)) + ); }); } @@ -168,7 +171,10 @@ class Task { */ public andThen(f: (t: T) => Task): Task { 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) + ); }); } @@ -192,7 +198,10 @@ class Task { */ public andThenP(f: (t: T) => Promise): Task { 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) + ); }); } @@ -201,7 +210,10 @@ class Task { */ public orElse(f: (err: E) => Task): Task { 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) + ); }); } @@ -210,7 +222,10 @@ class Task { */ public mapError(f: (err: E) => X): Task { 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) + ); }); } @@ -256,6 +271,25 @@ class Task { 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 { + return this.mapError(err => { + fn(err); + return err; + }); + } } export default Task; diff --git a/tests/Task.test.ts b/tests/Task.test.ts index ba844c5..6a9b01b 100644 --- a/tests/Task.test.ts +++ b/tests/Task.test.ts @@ -9,7 +9,7 @@ const cancellable = new Task((reject, resolve: Resolve) => { 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(); }); @@ -17,7 +17,7 @@ test('Task.succeed', t => { 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(); }); @@ -27,12 +27,15 @@ 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(); }); @@ -40,15 +43,24 @@ test('Task.map', t => { 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(); }); @@ -56,18 +68,24 @@ test('Task.andThen', t => { 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(); }); @@ -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(); @@ -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(); @@ -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(); @@ -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(); @@ -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(); @@ -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('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(); @@ -181,7 +199,7 @@ 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)) @@ -189,7 +207,7 @@ test('Task.assign', t => { .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(); @@ -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(); });