Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementing error handling as described in issue #35 #104

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about adding an s.either() that returns the left or right value ?


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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should also have a companion s.isRight() especially if s() can throw if it's not a right. Oh, I see in the code that does exist. So maybe just add to the docs ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch I'll update the docs.


### 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