Can Angular/TS promises cause out of order execution?

Viewed 95

We have an Angular application which was written for us and we're trying to figure out a specific problem (see below for my voluminous essay). Basically, it's a system where some digital input is supposed to control both the backlight and the page that's displayed. If the digital input is on, the backlight should also be on, and the app should work normally, allowing user interaction.

However, without that digital input signal, the application should give a blank page and turn off the backlight so that the user sees nothing and cannot interact with a real page in some blind fashion.


The backlight is controlled by a back end process, which also deliver information to the front end (both of these run on the same device) using Angular routing. But the decision as to what's displayed on the web page is totally under the control of the front end.

We're seeing a situation where, if the digital input is toggled off and on quickly, we sometimes get the backlight on but with a blank page and I'm wondering whether there's some sort of race condition in the delivery of event through the router.


In terms of code, we have an effect which monitors the digital input signal from the back end (not directly) that looks something like:

@Effect({ dispatch: false })
receiveDigitalInputEvent$ = this.actions.pipe(
  ofType(AppActionTypes.RECEIVE_DIGITAL_INPUT),
  map((action: AppActions.ReceiveDigitalInput) => action.payload),
  withLatestFrom(this.store.pipe(select(getRouterState))),
  tap(([data, routerState]) => {
    const currUrl = routerState ? routerState.state.url : '';
    const { isDigitalInputSet, someOtherStuff } = data;
    this.forceNavigateIfNeeded(isDigitalInputSet, currUrl);
  })
);

forceNavigateIfNeeded(isDigitalInputSet: boolean, currUrl) {
  if (isDigitalInputSet && currUrl.endsWith('/blank')) {
    this.router.navigateByUrl('/welcome');
  else if (! isDigitalInputSet && ! currUrl.endsWith('/blank')) {
    this.router.navigateByUrl('/blank');
  }
}

What this basically does is, on digital input event arrival:

  • if the input is on and the screen is currently blank, go to the welcome screen; or
  • if the input is off and you're not currently on the blank screen, go to the blank screen.

Now, this works normally, it's just the quick transitions that seem to be causing a problem.


I don't have that much knowledge of the Angular internals but I assume the incoming events are being queued somehow for delivery via the routing mechanism (I believe the back end pushed through stuff via web sockets to the front end).

I've looked into the Angular source and noticed that the navigateByUrl uses promises to do its work so the code may return to get the next message queue item before the page is actually changed. I just wanted someone with more Angular nous to check my reasoning or let me know if what I'm suggesting is rubbish. As to what I'm suggesting:

  1. The digital input goes off and a message (OFF) is queued from the back end to the front end. The back end also turns off the backlight.
  2. The digital input goes on again quickly and a message (ON) is queued from the back end to the front end. The back end also reactivates the backlight.
  3. The front end receives the OFF message and processes it. Because we've gotten an OFF message while not on the blank page, we call navigateByUrl('/blank') which starts up a promise.
  4. Before that promise can be fulfilled (and the routerState.state.url changed from non-blank to blank), we start processing the ON message. At this stage, we have an ON message with a (stale) non-blank page, so forceNavigateIfNeeded() does nothing.
  5. The in-progress navigateByUrl('/blank') promise completes, setting the page to blank.

Now we seem to have the situation described as originally, a blank page with the backlight on.

Is this even possible with Angular/Typescript? It seems to rely on the fact that messages coming in are queued and are acted upon (in a single threaded manner) as quickly as possible, while front-end navigation (and, more importantly, the updated record of your current page) may take some time.

Can someone advise as to whether this is possible and, if so, what would be a good way to fix it?

I assume it's a bad idea to wait around in forceNavigateIfNeeded() for the promise to complete, but I'm not sure how else to do it.

1 Answers

Although your problem is quite complex I can see a suspicious code which might be worth inspecting.

...
withLatestFrom(this.store.pipe(select(getRouterState))),
...

This might be the cause of your issue because it's taking what's there, what's latest. It doesn't request the update nor does it wait for it. We had similar issue where also milliseconds were in play and I came up with the workaround, where I replaced withLatestFrom with switchMap operator. I'm not sure if it works in your case but you can give it try.

map((action: AppActions.ReceiveDigitalInput) => action.payload),
switchMap(data => //data === action.payload
 this.store.select(getRouterState).pipe(map(routerState => ({ data, routerState }))
),
tap(({data, routerState}) => { ... }

EDIT: In our case we used a custom selector in the switchMap, which compares current and previous state and returns comparison result. Honestly I'm not 100% sure if that would also help you but it's a necessary piece of a puzzle in our case.

export const getFeshRouterState= pipe(
  select(getRouterState),
  startWith(null as RouterStateInterface),
  pairwise(),
  map(([prev, curr]) => /* do some magic here and return result */),
);

Sorry for initial confusion.

Related