Skip to content

Commit

Permalink
Merge pull request #261 from fluture-js/avaq/fork-catch
Browse files Browse the repository at this point in the history
Add forkCatch - fork with exception recovery
  • Loading branch information
Avaq committed Jul 25, 2018
2 parents 26be7e2 + 87be4c0 commit 3078c78
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 0 deletions.
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ sponsoring the project.
<details><summary>Consuming/forking Futures</summary>

- [`fork`: Standard way to run a Future and get at its result](#fork)
- [`forkCatch`: Fork with exception recovery](#forkcatch)
- [`value`: Shorter variant of `fork` for Futures sure to succeed](#value)
- [`done`: Nodeback style `fork`](#done)
- [`promise`: Convert a Future to a Promise](#promise)
Expand Down Expand Up @@ -1017,6 +1018,37 @@ var cancel = fut.fork(console.error, console.log);
cancel();
//Nothing will happen. The Future was cancelled before it could settle.
```
#### forkCatch

<details><summary><code>forkCatch :: (Error -> Any) -> (a -> Any) -> (b -> Any) -> Future a b -> Cancel</code></summary>

```hs
forkCatch :: (Error -> Any) -> (a -> Any) -> (b -> Any) -> Future a b -> Cancel
Future.prototype.forkCatch :: Future a b ~> (Error -> Any, a -> Any, b -> Any) -> Cancel
```

</details>

An advanced version of [fork](#fork) that allows you to recover from exceptions
that were thrown during the executation of the computation.

**Using this function is a trade-off;**

Generally it's best to let a program crash and restart when an unexpected
exception occurs. Restarting is the surest way to restore the memory that was
allocated by the program to an expected state.

By using `forkCatch`, you might be able to keep your program alive longer,
which can be very beneficial when the program is being used by multiple
clients. However, you also forego the certainty that your program will be in a
valid state after this happens. The more isolated the memory consumed by the
particular computation was, the more certain you will be that recovery is safe.

```js
var fut = Future.after(300, null).map(x => x.foo);
fut.forkCatch(console.error, console.error, console.log);
//! TypeError: Cannot read property 'foo' of null
```

#### value

Expand Down
13 changes: 13 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
declare module 'fluture' {

export interface RecoverFunction {
(exception: Error): void
}

export interface RejectFunction<L> {
(error: L): void
}
Expand Down Expand Up @@ -92,6 +96,9 @@ declare module 'fluture' {
/** Fork the Future into the two given continuations. See https://github.com/fluture-js/Fluture#fork */
fork(reject: RejectFunction<L>, resolve: ResolveFunction<R>): Cancel

/** Fork with exception recovery. See https://github.com/fluture-js/Fluture#forkCatch */
forkCatch(recover: RecoverFunction, reject: RejectFunction<L>, resolve: ResolveFunction<R>): Cancel

/** Set up a cleanup Future to run after this one is done. See https://github.com/fluture-js/Fluture#finally */
lastly(cleanup: Future<L, any>): Future<L, R>

Expand Down Expand Up @@ -227,6 +234,12 @@ declare module 'fluture' {
export function fork<L, R>(reject: RejectFunction<L>, resolve: ResolveFunction<R>): (source: Future<L, R>) => Cancel
export function fork<L, R>(reject: RejectFunction<L>): AwaitingTwo<ResolveFunction<R>, Future<L, R>, Cancel>

/** Fork with exception recovery. See https://github.com/fluture-js/Fluture#forkCatch */
export function forkCatch<L, R>(recover: RecoverFunction, reject: RejectFunction<L>, resolve: ResolveFunction<R>, source: Future<L, R>): Cancel
export function forkCatch<L, R>(recover: RecoverFunction, reject: RejectFunction<L>, resolve: ResolveFunction<R>): (source: Future<L, R>) => Cancel
export function forkCatch<L, R>(recover: RecoverFunction, reject: RejectFunction<L>): AwaitingTwo<ResolveFunction<R>, Future<L, R>, Cancel>
export function forkCatch<L, R>(recover: RecoverFunction): AwaitingThree<RejectFunction<L>, ResolveFunction<R>, Future<L, R>, Cancel>

/** Build a coroutine using Futures. See https://github.com/fluture-js/Fluture#go */
export function go<L, R>(generator: Generator<Future<L, any>, R>): Future<L, R>

Expand Down
8 changes: 8 additions & 0 deletions src/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,14 @@ Future.prototype.fork = function Future$fork(rej, res){
return this._interpret(Future$onCrash, rej, res);
};

Future.prototype.forkCatch = function Future$fork(rec, rej, res){
if(!isFuture(this)) throwInvalidContext('Future#fork', this);
if(!isFunction(rec)) throwInvalidArgument('Future#fork', 0, 'to be a Function', rec);
if(!isFunction(rej)) throwInvalidArgument('Future#fork', 1, 'to be a Function', rej);
if(!isFunction(res)) throwInvalidArgument('Future#fork', 2, 'to be a Function', res);
return this._interpret(rec, rej, res);
};

Future.prototype.value = function Future$value(res){
if(!isFuture(this)) throwInvalidContext('Future#value', this);
if(!isFunction(res)) throwInvalidArgument('Future#value', 0, 'to be a Function', res);
Expand Down
1 change: 1 addition & 0 deletions src/dispatchers.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export {fold} from './dispatchers/fold';

export {done} from './dispatchers/done';
export {fork} from './dispatchers/fork';
export {forkCatch} from './dispatchers/fork-catch';
export {promise} from './dispatchers/promise';
export {value} from './dispatchers/value';

Expand Down
15 changes: 15 additions & 0 deletions src/dispatchers/fork-catch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {isFuture} from '../core';
import {partial1, partial2, partial3} from '../internal/fn';
import {isFunction} from '../internal/is';
import {throwInvalidArgument, throwInvalidFuture} from '../internal/throw';

export function forkCatch(f, g, h, m){
if(!isFunction(f)) throwInvalidArgument('Future.forkCatch', 0, 'be a function', f);
if(arguments.length === 1) return partial1(forkCatch, f);
if(!isFunction(g)) throwInvalidArgument('Future.forkCatch', 1, 'be a function', g);
if(arguments.length === 2) return partial2(forkCatch, f, g);
if(!isFunction(h)) throwInvalidArgument('Future.forkCatch', 2, 'be a function', h);
if(arguments.length === 3) return partial3(forkCatch, f, g, h);
if(!isFuture(m)) throwInvalidFuture('Future.forkCatch', 3, m);
return m._interpret(f, g, h);
}
102 changes: 102 additions & 0 deletions test/1.future.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Future,
isFuture,
fork,
forkCatch,
value,
done,
promise,
Expand Down Expand Up @@ -77,6 +78,57 @@ describe('Future', function (){

});

describe('.forkCatch()', function (){

it('is a curried quaternary function', function (){
expect(forkCatch).to.be.a('function');
expect(forkCatch.length).to.equal(4);
expect(forkCatch(U.noop)).to.be.a('function');
expect(forkCatch(U.noop, U.noop)).to.be.a('function');
expect(forkCatch(U.noop)(U.noop)).to.be.a('function');
expect(forkCatch(U.noop)(U.noop, U.noop)).to.be.a('function');
expect(forkCatch(U.noop, U.noop)(U.noop)).to.be.a('function');
expect(forkCatch(U.noop)(U.noop)(U.noop)).to.be.a('function');
});

it('throws when not given a Function as first argument', function (){
var f = function (){ return forkCatch(1) };
expect(f).to.throw(TypeError, /Future.*first/);
});

it('throws when not given a Function as second argument', function (){
var f = function (){ return forkCatch(U.add(1), 1) };
expect(f).to.throw(TypeError, /Future.*second/);
});

it('throws when not given a Function as third argument', function (){
var f = function (){ return forkCatch(U.add(1), U.add(1), 1) };
expect(f).to.throw(TypeError, /Future.*third/);
});

it('throws when not given a Future as fourth argument', function (){
var f = function (){ return forkCatch(U.add(1), U.add(1), U.add(1), 1) };
expect(f).to.throw(TypeError, /Future.*fourth/);
});

it('dispatches to #_interpret()', function (done){
var a = function (){};
var b = function (){};
var c = function (){};
var mock = Object.create(F.mock);

mock._interpret = function (rec, rej, res){
expect(rec).to.equal(a);
expect(rej).to.equal(b);
expect(res).to.equal(c);
done();
};

forkCatch(a, b, c, mock);
});

});

describe('.value()', function (){

it('is a curried binary function', function (){
Expand Down Expand Up @@ -254,6 +306,56 @@ describe('Future', function (){

});

describe('#forkCatch()', function (){

it('throws when invoked out of context', function (){
var f = function (){ return Future.prototype.forkCatch.call(null, U.noop, U.noop, U.noop) };
expect(f).to.throw(TypeError, /Future/);
});

it('throws TypeError when first argument is not a function', function (){
var xs = [NaN, {}, [], 1, 'a', new Date, undefined, null];
var fs = xs.map(function (x){ return function (){ return F.mock.forkCatch(x, U.noop) } });
fs.forEach(function (f){ return expect(f).to.throw(TypeError, /Future/) });
});

it('throws TypeError when second argument is not a function', function (){
var xs = [NaN, {}, [], 1, 'a', new Date, undefined, null];
var fs = xs.map(function (x){ return function (){ return F.mock.forkCatch(U.noop, x) } });
fs.forEach(function (f){ return expect(f).to.throw(TypeError, /Future/) });
});

it('throws TypeError when third argument is not a function', function (){
var xs = [NaN, {}, [], 1, 'a', new Date, undefined, null];
var fs = xs.map(function (x){ return function (){ return F.mock.forkCatch(U.noop, U.noop, x) } });
fs.forEach(function (f){ return expect(f).to.throw(TypeError, /Future/) });
});

it('does not throw when all arguments are functions', function (){
var mock = Object.create(F.mock);
mock._interpret = U.noop;
var f = function (){ return mock.forkCatch(U.noop, U.noop, U.noop) };
expect(f).to.not.throw();
});

it('dispatches to #_interpret()', function (done){
var a = function (){};
var b = function (){};
var c = function (){};
var mock = Object.create(F.mock);

mock._interpret = function (rec, rej, res){
expect(rec).to.equal(a);
expect(rej).to.equal(b);
expect(res).to.equal(c);
done();
};

mock.forkCatch(a, b, c);
});

});

describe('#value()', function (){

it('throws when invoked out of context', function (){
Expand Down

0 comments on commit 3078c78

Please sign in to comment.