Returning an observable that's dependent on a Promise


#1

Hi there!

I will start by saying that I’m still wrapping my head around obsevables in general, so there is likely an obvious solution that I just haven’t found yet, ha. With that said… I have a service that is returning an Observable generated from an http request (as seen below). However, I am hitting a snag because I need to include Geolocation data (retrieved via promise) in the in the method that returns the Observable. The problem is that I can’t include the return Observable in the “.then()” of the Promise, because my function would then return a Promise instead of an observable… Any help would be much appreciated!

In service

pollTrips() {
    return Observable.interval(this.pollInterval)
        .switchMap(() => { return this.refreshPoll() });
}

private refreshPoll() {
    let currentLocation: any;

    Geolocation.getCurrentPosition().then(location => {
        currentLocation = location.coords;
        // I thought about simply returning this method, and putting everything below in this "then" handler,
        // but then my function won't return an Observable and can't be "subscribed to".  It also just feels
        // "wrong" to return an Observable wrapped in a Promise...
    });

    this.pollData = {
        lastPoll: this.pollInterval / 1000,
        tripIds: map(this.trips, 'tripId'),
        track: '',
        positionAge: this.pollInterval / 1000,
        lat: currentLocation.lat || {}, // PROBLEM: currentLocation hasn't been resolved from the promise yet
        lng: currentLocation.lng || {}  // PROBLEM: currentLocation hasn't been resolved from the promise yet
    };

    return this.api.post('/drivertrips/poll', this.pollData)
        .map(tripUpdates => {
            // Do cool stuff and modify this.trips based on tripUpdates
            return this.trips;
        });
}

In Component (which consumes the service)

private pollListener;
private trips: Array<Trip>;

ngOnInit() {
    this.pollListener = this.tripsService.pollTrips().subscribe(results => {
        this.trips = results;
    });           
}

#2

You have a few options, but the most important heuristic is that if you are defining a method that returns some sort of future (either an Observable or a Promise), 99 times out of 100 the very first word of the function should be ‘return’. Following that will save you from a lot of frustrating bugs. Heuristic #2 is “don’t modify state from future resolution if you’re part of a chain”, and that applies here as well. Lose the currentLocation variable.

You should be able to do something roughly like this:

return Observable.fromPromise(Geolocation.getCurrentPosition())
.map((location) => location.coords))
.map((coords) => {
  return {
    lastPoll: this.pollInterval / 1000,
    lat: coords.lat,
    // other fields 
  });
// use flatMap instead of map because we are returning another Observable here
.flatMap((polldata) => {
  return this.api.post('/drivertrips/poll', polldata);
  // explicitly do not do cool stuff and modify this.trips here
  // if you really need to do that, subscribe to the observable
  // you are returning here, but generally down this road lies pain.  
});

#3

Thank you so much for your super quick response and detailed answer; you’ve helped me before and I appreciate you doing it again. What you’ve proposed looks much cleaner and more like what I was trying to do.

As for the “do cool stuff” though, in that section of code I’m mapping values from the api to values that my component expects. If I put this in the component I will have to duplicate it in another part of the app (that uses the same data but a different view) that would otherwise call this service method. Should I still put this in each component, or would it be appropriate to leave this in the service method? Ie:

return this.api.post('/drivertrips/poll', this.pollData)
            .map(tripUpdates => {
                if (!isEmpty(tripUpdates.addedTrips)) {                    
                    this.formatTrips(tripUpdates.addedTrips);
                    this.trips.push.apply(this.trips, tripUpdates.addedTrips);
                }

                if (!isEmpty(tripUpdates.removedTrips)) {                    
                    let removedTripIds = map(tripUpdates.removedTrips, (removed: any) => {
                        return { tripId: removed.tripId };
                    });

                    pullAllBy(this.trips, removedTripIds, 'tripId');
                }

                return this.trips;
            });

And then in each component I simply subscribe to the “pollTrips” method in my first comment. But either way, I feel like I have a better understanding of this now and will be experimenting tonight to confirm. Thanks so much again!!


#4

I would leave it in the service method, but I would not store it there as a raw variable property. If consumers of the trips property are always accessing it via this refreshPoll() method (which it sounds like they aren’t, or the method is named weirdly), then you have no problem. In the generally more common case where your consumers want the most recent trips, and the push to update them comes from somewhere separate (like a refresh button), my favorite idiom is to have trips be a ReplaySubject with a stack size of 1. Consumers can subscribe to it (I generally expose it as an function returning Observable instead of making it a public property). You subscribe trips to the stuff happening in refreshPoll, and anybody in turn subscribing to trips automatically gets the freshest data whenever it comes in.