kefirjs / kefir

A Reactive Programming library for JavaScript
https://kefirjs.github.io/kefir/
MIT License
1.87k stars 97 forks source link

Zip with property not producing anything unless property has a subscriber #34

Closed myndzi closed 9 years ago

myndzi commented 9 years ago

I'm trying to gather data from an html document; some of the data is static or periodic, but at the lowest level I want to associate these values with items in a list. I've created some helpers to give me streams and data, but I'm having some difficulty when combining them.

Example: in the header of the page is a span containing a location. It occurs only once on the page, but it occurs before any of the list items I'm interested in. I do something like this:

var location = select(tokenized, '#header span.city')
    .flatMap(innerText)
    .toProperty();

var item = select(tokenized, 'ul.results a')
    .flatMap(function (obs) {
        var hrefs = obs.flatMap(attr('href')),
            innerText = obs.flatMap(innerText);

        return Kefir.zip([location, href, innerText]);
    });

item.onValue(function (a) {
    console.log(a);
});

Run as-is, I get nothing. But if I call location.onValue(function () { }); it works as expected. I would expect Kefir.zip to act as a subscriber on 'location', but it appears not to. Is this a bug or intentional? What should I be doing instead, if intentional?

rpominov commented 9 years ago

I don't understand your example completely as there obviously some parts are missing, but it looks like it should work if you add .onValue subscriber to the item stream. Also it's not clear what you mean when saying "works as expected", what is expected result here? Is something should be printed to console for example? I mean I don't see any side effects defined in your code. As long as there is no subscribers nothing is running, this is one of key features of Kefir.

myndzi commented 9 years ago

Oh, sorry. Yes, there is an .onValue subscriber on the item stream; If I remove 'location' from the zip, it works as expected. If I add 'location', nothing is ever emitted until I have a listener on the location property. They both come from the same source stream, and I've verified independently that 'hrefs' and 'innerText' emit data even when the zip doesn't.

What I expect is for it to emit a line for every matching value for 'ul.results a', but to include the current value of 'location' along with it. It does this, but only if I separately bind a dummy subscriber to location.

If you have IRC or Skype or something, get ahold of me (myndzi on either) and I can get you the full code so you can see what I'm running into?

myndzi commented 9 years ago

P.S. I tried with fromBinder also but no difference. I'm not sure entirely what the docs mean by 'control over the active state' but I don't see any way with fromBinder to force it into the 'active state'

rpominov commented 9 years ago

The "control over the active state" means that .fromBinder allows you to know when first subscriber is added to the stream that you created and when the last one is removed.

The reason why your code is not working might be is because you are using emitters. Consider this example:

var a = Kefir.emitter();
var b = a.toProperty();

a.emit(1); // the `b` property will not get this value because it not listening to `a` yet.
           // It will begin to listen to `a` only when the `b` itself get first subscriber

b.log(); // will not print anything as `b` didn't get the `1` value  

You can solve this, using the .fromBinder method:

var a = Kefir.fromBinder(function(emitter) {
 // When we inside this function, it means that `a` has a subscriber, in this case the `b` property subscribed to `a`

  emitter.emit(1);
});
var b = a.toProperty();

b.log(); 

You can read more about active state in docs https://pozadi.github.io/kefir/#active-state (especially the note section)

Try to rewrite you code using .fromBinder method, if it still won't work please let me know.

myndzi commented 9 years ago

Okay, so I've worked it out. You are correct in that it was a sync/async problem; the trouble was that I had once source feeding multiple values -- and I was only subscribing to/pulling from the header value in the callback of the other one, which left the subscription until after the data had occurred.

The fix is to do things what I assume is more properly, by defining each of the data sources and combining them; the combining step is synchronous with the definitions of the data sources, so it ensures that they all kick off at the start of the origin stream.

New code looks something like this:

var location = select(tokenized, '#header span.city')
    .flatMap(innerText)
    .toProperty();

var item = select(tokenized, 'ul.results a')
    .flatMap(function (obs) {
        var hrefs = obs.flatMap(attr('href')),
            innerText = obs.flatMap(innerText);

        return Kefir.zip([href, innerText]);
    });

var combined = Kefir.combine([ location, item ], function (l, i) {
    return {
        location: l,
        href: i[0],
        text: i[1]
    };
});

combined.onValue(function (a) {
    console.log(a);
});