baconjs / bacon.js

Functional reactive programming library for TypeScript and JavaScript
https://baconjs.github.io
MIT License
6.47k stars 330 forks source link

EventStream created by Bacon.fromPromise should not end on fulfilled or rejected #438

Closed beckyconning closed 10 years ago

beckyconning commented 10 years ago

A promise can be a great starting point for an EventStream. However limiting an EventStream's lifespan to the fulfilment or rejection of that promise is really restricting.

In this example we repeatedly long-poll for changes.

var changesEventStream = function (initialUpdateSeq) {
    var changeNotificationUrl = function (updateSeq) {
        return "http://example.com/people/_changes?feed=longpoll&since=" + updateSeq;
    };

    var getSubsequentChangeNotifications = function getSubsequentChangeNotifications(response) {
        var nextChangeNotificationUrl = changeNotificationUrl(response["last_seq"]);
        return Bacon.fromPromise(httpGetJSON(nextChangeNotificationUrl))
            .flatMapLatest(getSubsequentChangeNotifications);
    };

    return Bacon.fromPromise(httpGetJSON(changeNotificaitonUrl(initialUpdateSeq)))
        .flatMapLatest(getSubsequentChangeNotifications)
        .map(function (changeNotification) { return changeNotification["results"]; })
        .flatMap(Bacon.fromArray);
};

changesEventStream(0)
    .onValue(console.log.bind(console));

This seems so useful to me but its impossible with the current restriction on fromPromise. I am quite new to Bacon.js though, so I might have made a mistake that I can't see.

Maybe this restriction could be default but turn-off-able.

beckyconning commented 10 years ago

It seems to work if it the EventStream isn't started with fromPromise http://nullzzz.blogspot.co.uk/2012/12/baconjs-tutorial-part-iii-ajax-and-stuff.html

beckyconning commented 10 years ago

Shouldn't this log 0 and 1 rather than just 1?

Bacon.fromPromise(Promise.fulfilled(0))
    .flatMap(function (n) { return Bacon.fromPromise(Promise.fulfilled(n+1)); })
    .log();
phadej commented 10 years ago

Your example is essentially the same as

Promise.fulfilled(0)
   .then(function (n) { return Promise.fulfilled(n + 1); })
   .done(console.log)

I guess it should log only single 1.

raimohanska commented 10 years ago

Bacon.fromPromise is based on the assumption that a promise yields at most 1 value. "A promise represents the eventual value returned from the single completion of an operation" http://wiki.commonjs.org/wiki/Promises/A

Not sure what you're trying to accomplish in your example, but it seems you're creating a non-terminating recursively constructing stream where you always poll for more stuff from the server and never output anything.

beckyconning commented 10 years ago

Ah ok. I can see now that this is the defined behaviour of the methods I was using. Is it possible achieve the effect of:

Changes feed example:

Bacon.fromPromise(getChangesSince(0))
    .mergeFlatMapLatest(function getSubsequentChanges(changes) {
        return Bacon.fromPromise(getChangesSince(changes[“last_seq”])
            .mergeFlatMapLatest(getSubsequentChanges);
        });
    })
    .map(function (changes) { return changes[“results”]; )
    .flatMap(Bacon.fromArray)
    .log();
beckyconning commented 10 years ago

screen shot 2014-10-03 at 14 33 35

The equivalent with Javascript Arrays would be:

var g = function (xs, f) { 
    return xs.map(function (x) { return [x, f(x)]; })
        .reduce(function (x, y) { return x.concat(y); }); 
};

and with Haskell Lists would be:

g :: [a] -> (a -> a) -> [a]
g xs f = concat $ map (\x -> [x, f x]) xs

For active streams like those originating from Bacon.interval this can be done by adding this method to EventStream:

mergeFlatMapLatest: (right) ->
  left = this
  rightStream = left.flatMapLatest(right)
  withDescription(left, "mergeFlatMapLatest", right, left.merge(rightStream))

This won't work if the steam that this method is called on originates from Bacon.once. It does seem to work on streams originating form Bacon.fromPromise though:

Bacon.fromPromise(P.fulfilled(1))
    .mergeFlatMapLatest(function (x) { return B.fromPromise(P.fulfilled(x + 1)); })
    .log();

The above gives:

1
2
<end>

Which also lets you do:

var forever = function (x) { 
    return Bacon.fromPromise(Promise.fulfilled(x + 1).delay(1000)).mergeFlatMapLatest(forever); 
};

Bacon.fromPromise(P.fulfilled(0)).mergeFlatMapLatest(forever).log();

Which is a polling pattern that can be applied to all sorts of asynchronous actions.

A longer term goal might be making merge more composable?

Also I normally use spies to unit-test functions in terms of other functions but there doesn't seem to be any spy testing stuff in BaconSpec.coffee. What would the best way of writing a test for a method like this?

beckyconning commented 10 years ago

Actually let me open a new issue for this.