5

Is there a way to have a hot observable from an EventEmitter (or equivalent available in Angular 2 alpha 46 / RxJS 5 alpha)? i.e. if we subscribe after the value is resolved, it triggers with the previously resolved value. Similar to what we have when always returning the same promise.

Ideally, only using Angular 2 objects (I read somewhere a light RxJS would be embedded later to remove the dependency), otherwise importing RxJS is fine. AsyncSubject seems to match my need, but it is not available in RxJS 5 alpha.

I tried the following, without success (never triggers). Any idea about how to use it?

let emitter = new EventEmitter<MyObj>();
setTimeout(() => {emitter.next(new MyObj());});
this.observable = emitter;
return this.observable.share();

Full plunker here comparing hot and cold

Usecase: reach some async objects only once (for example a series of HTTP calls merged/wrapped in a new EventEmitter), but provide the resolved async object to any service/component subscribing to it, even if they subscribe after it is resolved (the HTTP responses are received).

EDIT: the question is not about how to merge HTTP responses, but how to get a (hot?) observable from EventEmitter or any equivalent available with Angular 2 alpha 46 / RxJS 5 alpha that allows to subscribe after the async result is retrieved/resolved (HTTP is just an example of async origin). myEventEmitter.share() does not work (cf plunker above), although it works with the Observable returned by HTTP (cf plunker from @Eric Martinez). And as of Angular 2 alpha 46, .toRx() method does not exist any more, the EventEmitter is the observable and subject itself.

This is something working well with promises as long as we always return the same promise object. Since we have observers introduced with HTTP Angular 2 services, I would like to avoid mixing promises and observers (and observers are said to be more powerful than promises, so it should allow to do what is easy with promises).

Specs about share() (I haven't found doc for version 5 alpha - version used by Angular 2) - working on the Observable returned by the Angular 2 HTTP service, not working on EventEmitter.

EDIT: clarified why not using the Observable returned by HTTP and added that not using RxJS directly would be even better.

EDIT: changed description: the concern is about multiple subscriptions, not merging the HTTP results.

Thanks!

user3743222
  • 18,345
  • 5
  • 69
  • 75
Antoine OL
  • 1,270
  • 1
  • 12
  • 17
  • I have an example of http using share in this [plnkr](http://plnkr.co/edit/GFdXPncYPxx0IcqIwQnO?p=info). Why do you ask about converting an EventEmitter to something when your usecase is using Http module? – Eric Martinez Dec 01 '15 at 12:53
  • It's misleading in my description, the usecase is slightly more complex: a series of HTTP calls, and once completed, I merge/wrap the results in a new Observable. Equivalent with promises (angular 1 syntax): `return $q.all(httpPromisesToResolve);` – Antoine OL Dec 01 '15 at 15:59
  • I don't think you need `share()` for this. What you need is [`flatMap`](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/selectmany.md). Check this [example](http://plnkr.co/edit/9sPNlaNLh7Aie9yH1H7i?p=preview) – Eric Martinez Dec 01 '15 at 16:33
  • Actually my concern is more about how to have multiple subscribers, and trigger the HTTP requests / trigger async process only once, regardless of what transformations is done on the results, although it is great to see what is possible with the flatMap (I learnt it, thanks :)). The AsyncSubject suggested by @user3743222 seems to be the solution, now I am checking how to use it in Angular 2 with the RxJS lib v.5 alpha. – Antoine OL Dec 01 '15 at 17:03
  • `share()` is working well on the Observer returned by HTTP (I just checked, it is doing the job, converts to hot observable), but it doesn't work on EventEmitter. – Antoine OL Dec 01 '15 at 17:06
  • `EventEmitter` extends from `Subject` which is already a hot observable. – Eric Martinez Dec 01 '15 at 17:38

2 Answers2

3

The functionality you seem to be describing is not that of a cold observable but more than of a Rx.BehaviourSubject. Have a look here for an explanation on Rxjs subjects : https://github.com/Reactive-Extensions/RxJS/blob/master/doc/gettingstarted/subjects.md.

I quote from there :

BehaviourSubject is similar to ReplaySubject, except that it only stored the last value it published. BehaviourSubject also requires a default value upon initialization. This value is sent to observers when no other value has been received by the subject yet. This means that all subscribers will receive a value instantly on subscribe, unless the Subject has already completed.

The Rx.AsyncSubject would be the closest in behaviour to a promise :

AsyncSubject is similar to the Replay and Behavior subjects, however it will only store the last value, and only publish it when the sequence is completed. You can use the AsyncSubject type for situations when the source observable is hot and might complete before any observer can subscribe to it. In this case, AsyncSubject can still provide the last value and publish it to any future subscribers.

Two more comments:

  • in your plunker : this._coldObservable = emitter.share();. Using share returns a hot observable!
  • EventEmitter actually extends subject in the first place

UPDATE : Wrapping an EventEmitter around an Rx.Observable:

function toRx ( eventEmitter ) {
  return Rx.Observable.create(function ( observer ) {
    eventEmitter.subscribe(function listener ( value ) {observer.onNext(value)});
    // Ideally you also manage error and completion, if that makes sense with Angular2
    return function () {
      /* manage end of subscription here */
    };
  };
)
}

Once you have that Rx.Observable, you can apply share(), shareReplay(1), anything you want.

My bet is that the Angular team will sooner or later propose a brigding function but if you don't want to wait, you can do it yourself.

Manolis
  • 893
  • 1
  • 14
  • 33
user3743222
  • 18,345
  • 5
  • 69
  • 75
  • Thanks for your answer! It seems that `AsyncSubject` is what I am looking for, although an object given by the Angular lib would be even better. But I was not able to find it in the RxJS lib used by Angular 2 (version 5.0.0-alpha.7 - I fixed the version number in the question). Any idea about that? Something like `import {AsyncSubject} from '@reactivex/rxjs/dist/cjs/Rx';` It seems that the BehaviorSubject and ReplaySubject are available, but none of them is completely working as expected here. PS: it seemed I inverted `hot` and `cold` words, *hot* is what I was thinking about. – Antoine OL Dec 01 '15 at 16:43
  • I don't really get what you are after. Supposing your http call returns an observable, you can emulate `$q.all(httpPromisesToResolve)` with `Rx.Observable.forkJoin ([httpCalls])`. Cf. https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/forkjoin.md and http://reactivex.io/documentation/operators/zip.html (scroll down to the js section part and the `forkJoin` documentation). Then with `Rx.Observable.forkJoin ([httpCalls]).share()` you can subscribe all you want to the `share` obs., there will be only be one subscription to the `Rx.Observable.forkJoin ([httpCalls])` – user3743222 Dec 01 '15 at 17:37
  • Now if you want to get the last value even if you subscribe after the observable have completed, use `Rx.Observable.forkJoin ([httpCalls]).shareReplay(1)` instead. Cf. https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/sharereplay.md. What I don't know is if this works the same in the `5.0.0-alpha.7` version. – user3743222 Dec 01 '15 at 17:43
  • Also, if you want to get the observable hidden behind your event emitter, for use in the `forkJoin`, you can use `yourEventEmitter.toRx()`. – user3743222 Dec 01 '15 at 22:20
  • I reworded my initial question to clarify. My concern is about triggering only once the async process even if subscribed multiple times, similar to always returning the same promise even after it is resolved. HTTP is just an example of async process. share() and shareReplay(1) are working on HTTP observable, not on EventEmitter, as showed by the plunker. Thanks for suggesting the forkJoin anyway! It will help to improve the code fore sure, I didn't know it :) – Antoine OL Dec 02 '15 at 05:04
  • Well then if you want to use `Rxjs` in connection with Event Emitter, you will have to bridge it yourself, i.e. make your `.toRx()` function by yourself. I'd be surprised somebody hasn't done that yet. It is not that difficult, just wrap around a `Rx.Observable.create`. Another solution is, if the new `EventEmitter` from angular has a similar API to the `Node.js` event emitter (`emit`, `addListener`, `removeListener`, `on`, `off`), to use `Rx.Observable.fromEvent`. Cf. https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/fromevent.md – user3743222 Dec 02 '15 at 05:43
  • it seems you was very close with shareReplay(1). @robwormald suggested on gitter to replace EventEmitter (not designed for this usecase) by `new ReplaySubject(1);` (would have the same behavior as `AsyncSubject`, if it was still existing, from what I understand), and he provided a working plunker. Cf my answer below. It seems simpler than making my own `.toRx()` :) Thanks for your help! – Antoine OL Dec 02 '15 at 05:53
  • Oh and the Rx object is not easily accessible with latest angular2/rxjs, the syntax would be slightly different. I actually already use this kind of wrappers (in a slightly different way), and the only way I used to create Observable was `EventEmitter`. That was my mistake, since other Subjects/Observables work better. And the `ReplaySubject` is doing directly what I need :) – Antoine OL Dec 02 '15 at 06:04
1

ReplaySubject is doing what I was looking for. @robwormald provided a working example on gitter I slightly modified to better demonstrate.

Exposing HTTP response:

import {Injectable} from 'angular2/angular2';
import {Http} from 'angular2/http';
import {ReplaySubject} from '@reactivex/rxjs/dist/cjs/Rx'

@Injectable()
export class PeopleService {
  constructor(http:Http) {
    this.people = new ReplaySubject(1);

    http.get('api/people.json')
      .map(res => res.json())
      .subscribe(this.people);
  }
}

Subscribing multiple times:

// ... annotations
export class App {
  constructor(peopleService:PeopleService) {

    people.subscribe(v => {
      console.log(v)
    });

    //some time later

    setTimeout(() => {
      people.subscribe(v => {
        console.log(v)
      });
      people.subscribe(v => {
        console.log(v)
      });
    },2000)
  }
}

Full plunker

EDIT: the BehaviorSubject is an alternative. In this usecase, the difference is the initial value, for example if we want to display content from cache before updating with the HTTP response.

Antoine OL
  • 1,270
  • 1
  • 12
  • 17
  • So we get back to the beginning in the end. Glad you found an answer! Takeaways as a new member of SO : 1. formulate your need as separated as possible from the (imagined) solution (you insisted on `event emitter` and ended up not using one - this happens in fact quite often) and present your current (failing) approach, 2. be as precise as can reasonably be in the concepts and the wording. Example : the first sentence of the question :`if we subscribe after the value is resolved, it triggers with the previously resolved value.` - understandable with promises but not anymore with observables. – user3743222 Dec 02 '15 at 06:13
  • But anyways, it will always happen that you need a chat session to properly express your issue. So just mentioning the points before for everybody who will look at the question and answer. – user3743222 Dec 02 '15 at 06:16
  • Seems I didn't use the right words to express the behavior I was looking for (I don't know how to say `resolve` with Observable vocabulary), thanks for your advices! I will try to apply it. – Antoine OL Dec 02 '15 at 14:28