Quantcast
Channel: Hasen Judy
Viewing all articles
Browse latest Browse all 50

Competing asynchronous processes

$
0
0

Suppose you have an SPA (single page application) where all link clicks are handled in Javascript, and where the handling of link clicks might potentially involve an asynchronous request.

Now imagine the user clicked on link A, and this in turn has triggered a process involving some asynchronous request. From the user’s point of view, the page is still active, but with some kind of a “loading” progress bar.

While waiting for the data to load, the user spotted a more interesting link B on the same page. Since A is still loading, they decided to click on link B. Presumably, their intention was to cancel the navigation to A and go to B instead.

However, depending on how your code was written, they might see page A flash briefly before page B is on.

This is bad UX and shouldn’t happen. It might seem like a tiny detail but it says alot about how polished the application is.

It’s not merely about aesthetics though. There’s a worst case where they might see page B briefly before being redirected to page A. This can happen if the requests necessary to load B got completed before the requests necessary for A. In that case, it’s not just some “flicker” in the UX; it’s a failure to do what the user wanted the application to do.

So what needs to happen as part of loading page B is to cancel the processes of loading page A.

This can be troublesome because the process to load A was just fired and forgotten.

Simple Timeouts

Before considering async requests, lets consider a simpler case: timeouts. I say simpler because they are easy to cancel. setTimeout returns an integer handle that can be used later to cancel it.

var timeout = setTimeout(fn, delay);
// .. later ..
clearTimeout(timeout);

The problem is how do you keep track of that variable, and how to know when to clear it?

I propose a simple solution: group timeouts under some kind of a label. Any timeout scheduled under a label will clear the previous timeout scheduled under it. This way, you won’t have two competing timeouts.

var map = {};
var schedule(label, fn, delay) {
    if(map[label]) {
        clearTimeout(map[label]);
    }
    map[label] = setTimeout(fn, delay);
}

This should be fairly straight forward.

If you do: schedule("nav", fn, delay); And then later you do: schedule("nav", fn2, delay);

The previous scheduled timeout will be automatically cleared.

What we are doing is adding more “contextual” information. Instead of firing “fn” and forgetting it, we are keeping track of it.

This works nicely for timeouts, because there’s a mechanism to cancel them. But this kind of solution doesn’t translate well to asynchronous requests, which is how “loading” processes usually start.

Asynchronous requests

XHR requests have an “abort” method, but it’s not exactly the same as clearing a timeout. When you clear a timeout, the scheduled function will not execute. Aborting a request does not have the same effect. The callback function will be called anyway, but it will be in a failure state; the same thing that happens when the request fails due to a network problem or because the server returned an error code.

Typically when a request fails, this indicates a problem. Maybe you need to alert the user the operation cannot be completed because of a network error.

It’s not very wise to overload the request failure mechanism with the process-cancellation semantics.

We need to think of a different approach.

Promises

Promises are an abstraction of an asynchronous process.

As far as I know, there’s no direct mechanism to “cancel” a promise. You could reject it, but that’s like throwing an exception. It suffers the same problem the XHR#abort suffers: it looks the same as if the request had failed. The error handling code (error callback function) will look confusing because it will have to be filled with special cases for checking whether the server failed or someone decided to cancel the request.

Alternate approach

Let’s think about how we would want to handle the situation if we had “people” working instead of code.

Suppose you have a person responsible for displaying stuff on the page, and various people responsible for navigation. When the user clicks “A”, the page-person summons a navigation-person that knows how to grab content for “A”, and tells him to go get the content for “A”, and then waits. Meanwhile, the user clicks “B”, so the page person takes a note and summons another navigation-person to go grab content for “B”. However, at this time, they would like to tell the “A” person to stop/cancel his journey, but they have lost all contact with that person.

After waiting for some time, a navigation person comes back with some page content. Is this navigation person bringing content for page “A” or “B”? If the page-person can’t tell, he can’t make a reasonable judgement about what to do, so he has no choice but to display that content on the page.

A moment later another person will bring another piece of content. We are faced again with the same problem, and have no choice but to display that content. This is obviously a problem; we want to display content for page “B”, but we don’t know what kind of content is being delivered, so we don’t know how to handle it!

If only these navigation-people would tell the page-person what content are they bringing with them, then the navigation person can decide whether to actually display this content or to ignore it.

I hope you see where I’m going with this.

The page-person knows what page they need to display: page “B”. They can ignore the content for page “A” when it arrives. All they need is for the navigation-person bringing content to declare what content are they bringing.

Perhaps the code was originally like this (roughly):

First Version:

function navigageTo(pageUrl) {
    var loaderFn = grabContentLoader(pageUrl);
    loaderFn().then(function(newContent) {
       setPageContent(newContent);
    });
}

After our consideration, we should probably change the loading function to be more like this:

Second Version:

function navigateTo(pageUrl) {
    var loaderFn = grabContentLoader(pageUrl);
    setCurrentLoadingUrl(pageUrl);
    loaderFn().then(function(newContent) {
       notifyContentReady(pageUrl, newContent);
    });    
}

function notifyContentReady(url, content) {
    if(getCurrentLoadingUrl() !== url) {
        return;
    }
    setCurrentLoadingUrl(null);
    setPageContent(content);
}

What we are doing is changing how we handle the success callback. The success callback doesn’t immediately do anything; instead, it notifies the system of what it has done, and lets the system decide what to do.

In the first version, the callback is written with the assumption that the state of the system hasn’t changed since the time the callback was fired: the current page being loaded was implicitly assumed to still be the same.

The second version does not make that assumption. It acknowledges that the system might be in a different state, so instead of presumptuously updating the system, it simply notifies it of what it has brought.


Viewing all articles
Browse latest Browse all 50

Trending Articles