0

I have the following code which works intermittently. Sometimes the getCurrentPosition promise does not resolve in time, and undefined gets passed to the first switchMap in my pipe, which breaks everything of course.

Location Service:

    // Get user location
    public getUserLocation(): Observable<LocationModel> {
        const options = {
            enableHighAccuracy: true,
            timeout: environment.userLocationTimeout
        };

        return from(Geolocation.getCurrentPosition(options)).pipe(
            map((position) => {
                const location = {
                    lat: position.coords.latitude,
                    lng: position.coords.longitude
                };

                return location;
            })
        );
    }

Subscriptions Page:

    // Get Subscriptions
    private getSubscriptions(): void {
        this.locationService
            .getUserLocation()
            .pipe(
                switchMap((location: LocationModel) => this.locationAddressService.getAddress(location)),
                switchMap((address: AddressModel) => this.subscriptionsService.getSubscriptions(address)),
                takeUntil(this.destroy$)
            )
            .subscribe((subscriptions) =>
                this.zone.run(() => {
                    this.subscriptions = subscriptions;
                })
            );
    }

I am expecting the from() to only return the LocationModel when the promise is resolved, but that doesn't seem to be what's happening. Am I using it incorrectly? How can I ensure that there is a LocationModel ready for the switchMap?

We are trying to stay away from promises as much as possible as they mess with our error logging, so are trying to use observables wherever we can.

Additionally, assuming the location is returned on time, I need to set the UI element inside the NgZone, if I don't then it takes a long time to update the UI after the subscriptions have been returned. I don't think this is the same issue however.

  • 2
    I would recommend to use the `filter` operator before `switchMap`. You can check required properties are presented in a response from `Geolocation` and if so - proceed next or skip emissions until you get a proper response from `getCurrentPosition()`. – Dmitry S. Nov 25 '22 at 10:11
  • 1
    Are you using some library or the built-in [`GeoLocation`](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/getCurrentPosition)? This function doesn't return anything, but works with callbacks. If you are using different library please make sure that the promise doesn't return `undefined`. I don't see any chance that your `Observable` would return `undefined`, the `map()` will _always_ emit an object (unless it crashes of course). – Lukas-T Nov 25 '22 at 10:23
  • 2
    Also your assumption that "getCurrentPosition promise does not resolve in time" is probably wrong, Observables don't get bored and emit `undefined` for no reason ;) The pipe will only continue after the `Promise` is resolved. I'd inspect the Promise more deeply and maybe you could add some logging to trace the value and show us the log. – Lukas-T Nov 25 '22 at 10:27
  • @churill We're using Ionic. Geolocation is a plugin for Ionic which uses either phone or browser location data to return the coordinates as a promise. It definitely does return this info, if I setup breakpoints I can them get hit in this order: return from(Geolocaiton...) -> switchMap 1 (location is undefined) -> switchMap 2 (breaks because undefined) -> return location If I have these breakpoints in place it fails every time, but if I don't have breakpoints it only fails sometimes, hence me thinking its some sort of timing issue – Philip Konick Nov 25 '22 at 10:40
  • @DmitryS. Thanks I will try this - I was thinking I might need some sort of retry logic to solve it. – Philip Konick Nov 25 '22 at 10:41
  • @PhilipKonick Don't have much experience with ionic, but are you sure you don't have to inject the `GeoLocation` in your service constructor instead of accessing directly? This way doesn't look very 'angulary' to me. – akotech Nov 25 '22 at 10:59
  • @akotech I should have actually said Capacitor instead of Ionic, but I am using it correctly. Capacitor is the 'upgrade' of Cordova which used to use constructor injection for the plugins. I can't remember off hand the reasoning for the change but it's supposed to be better... – Philip Konick Nov 25 '22 at 11:21
  • @churill - "Observables don't get bored and emit undefined for no reason" – BizzyBob Nov 25 '22 at 13:23
  • @PhilipKonick Thanks for the update, interesting observations. I stick to my comments above, your obersable can never emit `undefined` and the pipes seem fine. You should inspect the values returned by each service and some logging, maybe using `tap`. – Lukas-T Nov 26 '22 at 19:03

1 Answers1

0

From what you've written, I have to assume your promise is resolving with undefined. The "right" way to fix this is to figure out the conditions which cause Geolocation.getCurrentPosition to resolve undefined and guard against those:

someCondition().then(_ =>
  Geolocation.getCurrentPosition(options)
)

or with RxJS:

defer(() => someCondition()).pipe(
  switchMap(_ => Geolocation.getCurrentPosition(options)),
  map((position) => ({
    lat: position.coords.latitude,
    lng: position.coords.longitude
  })
)

If that's difficult to investigate, then you can just retry until you get a value. As an aside, it's almost always a mistake to use RxJS::from with a promise. You can use defer instead to unify promises with RxJS's lazy semantics.

defer(() => Geolocation.getCurrentPosition(options)).pipe(
  tap(v => {
    if(v == null) {
      throw "error";
    }
  ),
  retry(_ => timer(500)),
  map((position) => ({
    lat: position.coords.latitude,
    lng: position.coords.longitude
  })
)

If retry works, I would upgrade it to a version that doesn't retry forever. I'll leave that exercise up to you.

Mrk Sef
  • 7,557
  • 1
  • 9
  • 21