Skip to content

Commit

Permalink
Implementing error handling as described in issue #35
Browse files Browse the repository at this point in the history
  • Loading branch information
jplikesbikes committed Mar 22, 2016
1 parent 5753f0e commit 61872ce
Show file tree
Hide file tree
Showing 3 changed files with 280 additions and 8 deletions.
54 changes: 54 additions & 0 deletions ERRORS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Errors in flyd

## Ethos
+ It should not be the concern of `flyd` to handle exceptions for the user -- any `throw` should result in a hard failure.
+ Silent failures are bad (current way `flyd` handles Promise.reject)
+ Be as unopinionated in implementation as possible
+ Be functional in design
+ Be as backward compatible as possible with the current api

## Concepts
+ The stream is of `events`
+ Each stream has a `left` and a `right` side (like an Either)
+ The right side is the domain objects
+ The left side is meta (in most cases errors)
+ By default the api operates on the `right` side

## The Api
`s` is a stream

### Setting data s(...) is overloaded
+ `s(value)` is the default case takes a value makes it a right and pushes it down the stream
+ `s(promise)` if the promise resolves pushes a right, otherwise pushes a left
+ `s(either)` pushes down right or left based on either.left either.right
+ `s.left(value)` sets the stream to a left of `value`

### Getting data
+ `s()` get the last right value or throws an exception if there is a left value
+ `s.left()` get the last left value or throws an exception if there is a right value

### Checking stream state
+ `s.isLeft()` return boolean so you know what the stream contains

### Core functions
+ `.map()` works only on rights and ignores lefts
+ `.mapAll()` gets all events as an `Either`
+ `.combine()` and `.merge()` stay the same they work on streams
+ `ap()` works on `rights` only
+ `.scan()` works on `rights` only
+ `.on()` works on `rights` only

### The Either implementation
There are no additional dependencies and we have provided a minimal implementation for basic use. If you plan on using `.mapAll` we recommend overriding the methods in flyd.Either. You can use [folktale/data.either](https://github.com/folktale/data.either) for example as shown below.
```
var DE = require('data.either');
flyd.Either.Right = DE.Right;
flyd.Either.Left = DE.Left;
flyd.Either.isEither = function(obj) { return obj instanceof DE; };
flyd.Either.isRight = function(e) { return e.isRight; };
flyd.Either.getRight = function(e) { return e.value; };
flyd.Either.getLeft = function(e) { return e.value; };
```

### Other functionality
Keeping with the ethos of flyd any further functions like `.swap` or `.onAll` should be implemented as modules.
149 changes: 141 additions & 8 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ var curryN = require('ramda/src/curryN');
function isFunction(obj) {
return !!(obj && obj.constructor && obj.call && obj.apply);
}

function isRight(n) {
return !flyd.Either.isEither(n) || flyd.Either.isRight(n);
}

function trueFn() { return true; }

// Globals
Expand All @@ -20,6 +25,44 @@ var flyd = {}

// /////////////////////////// API ///////////////////////////////// //

/**
* Defines an Either type.
* @type {Object}
*/
flyd.Either = {
Right: function(val) {
return {
right: val,
left: undefined,
isRight: true
};
},

Left: function(err) {
return {
right: undefined,
left: err,
isRight: false
};
},

isEither: function(obj) {
return obj !== undefined && obj !== null && obj.hasOwnProperty('left') && obj.hasOwnProperty('right');
},

isRight: function(either) {
return either.isRight;
},

getRight: function(either) {
return either.right;
},

getLeft: function(either) {
return either.left;
}
};

/**
* Creates a new stream
*
Expand Down Expand Up @@ -178,6 +221,27 @@ flyd.endsOn = function(endS, s) {
*/
// Library functions use self callback to accept (null, undefined) update triggers.
flyd.map = curryN(2, function(f, s) {
return combine(function(s, self) {
if (isRight(s.val)) self(f(s()));
}, [s]);
})

/**
* Returns a new stream consisting of every value, both Lefts and Rights, from `s`
* passed through `f`
* __Signature__: `(a -> result) -> Stream a -> Stream result`
*
* @param {Function} f - the function that produces the elements of the new stream
* @param {stream} s - the stream to map
* @return {stream} - a new stream of mapped Either values
*
* @example
* var numbers = flyd.stream(flyd.Either.Right(1));
* var doubledNumbers = numbers.mapAll(function(n) {
* return flyd.Either.Right(n.right*n.right);
* });
*/
flyd.mapAll = curryN(2, function(f, s) {
return combine(function(s, self) { self(f(s.val)); }, [s]);
})

Expand All @@ -196,7 +260,9 @@ flyd.map = curryN(2, function(f, s) {
* @return {stream} an empty stream (can be ended)
*/
flyd.on = curryN(2, function(f, s) {
return combine(function(s) { f(s.val); }, [s]);
return combine(function(s) {
if (isRight(s.val)) f(s());
}, [s]);
})

/**
Expand All @@ -219,7 +285,7 @@ flyd.on = curryN(2, function(f, s) {
*/
flyd.scan = curryN(3, function(f, acc, s) {
var ns = combine(function(s, self) {
self(acc = f(acc, s.val));
if (isRight(s.val)) self(acc = f(acc, s()));
}, [s]);
if (!ns.hasVal) ns(acc);
return ns;
Expand Down Expand Up @@ -312,7 +378,7 @@ flyd.curryN = curryN

/**
* Returns a new stream identical to the original except every
* value will be passed through `f`.
* value will be passed through `f`. This only operates on values or Rights.
*
* _Note:_ This function is included in order to support the fantasy land
* specification.
Expand All @@ -329,6 +395,24 @@ flyd.curryN = curryN
*/
function boundMap(f) { return flyd.map(f, this); }

/**
* Returns a new stream identical to the original except every
* value or Either will be passed through `f`, even Lefts.
*
* __Signature__: Called bound to `Stream a`: `(a -> b) -> Stream b`
*
* @name stream.mapAll
* @param {Function} function - the function to apply
* @return {stream} a new stream with the values and Eithers mapped
*
* @example
* var numbers = flyd.stream(flyd.Either.Right(1));
* var doubledNumbers = numbers.mapAll(function(n) {
* return flyd.Either.Right(n.right*n.right);
* });
*/
function boundMapAll(f) { return flyd.mapAll(f, this); }

/**
* Returns a new stream which is the result of applying the
* functions from `this` stream to the values in `stream` parameter.
Expand All @@ -353,7 +437,9 @@ function boundMap(f) { return flyd.map(f, this); }
*/
function ap(s2) {
var s1 = this;
return combine(function(s1, s2, self) { self(s1.val(s2.val)); }, [s1, s2]);
return combine(function(s1, s2, self) {
if (isRight(s1.val) && isRight(s2.val)) self(s1()(s2()));
}, [s1, s2]);
}

/**
Expand All @@ -365,6 +451,45 @@ function streamToString() {
return 'stream(' + this.val + ')';
}

/**
* Returns true if `n` is a Left value.
*
* @name stream.isLeft
* @return {Boolean}
*/
function isLeft(n) {
return flyd.Either.isEither(n) && !flyd.Either.isRight(n);
}

/**
* Returns true if the current value of a stream is a Left value.
*
* @name stream.isLeft
* @return {Boolean}
*/
function boundIsLeft() {
return isLeft(this.val);
}

/**
* Returns the value in the stream if it's a not a Right, otherwise it throws a
* TypeError.
*
* @param {*} error - the error to wrap in a Left and set the stream to
* @throws {TypeError} - thrown if the value in the stream is a Right
* @return {stream}
*/
function left(n) {
if (arguments.length === 0) {
if (!isLeft(this.val)) {
throw new TypeError('Stream contains a Right where a Left is expected.');
}
return flyd.Either.getLeft(this.val);
}
updateStreamValue(this, flyd.Either.Left(n));
return this;
}

/**
* @name stream.end
* @memberof stream
Expand Down Expand Up @@ -397,9 +522,14 @@ function streamToString() {
*/
function createStream() {
function s(n) {
if (arguments.length === 0) return s.val
updateStreamValue(s, n)
return s
if (arguments.length === 0) {
if (flyd.Either.isEither(s.val) && isLeft(s.val)) {
throw new TypeError('Stream contains a Left where a Right is expected.');
}
return flyd.Either.isEither(s.val) ? flyd.Either.getRight(s.val) : s.val;
}
updateStreamValue(s, n);
return s;
}
s.hasVal = false;
s.val = undefined;
Expand All @@ -411,6 +541,9 @@ function createStream() {
s.ap = ap;
s.of = flyd.stream;
s.toString = streamToString;
s.left = left.bind(s);
s.isLeft = boundIsLeft;
s.mapAll = boundMapAll;
return s;
}

Expand Down Expand Up @@ -534,7 +667,7 @@ function flushUpdate() {
*/
function updateStreamValue(s, n) {
if (n !== undefined && n !== null && isFunction(n.then)) {
n.then(s);
n.then(s, s.left);
return;
}
s.val = n;
Expand Down
Loading

0 comments on commit 61872ce

Please sign in to comment.