Avoid duplicate requests with the Microsoft Graph JavaScript SDK


Avoid duplicate requests and speed up your app with the Microsoft Graph JavaScript SDK.

Different context, same people

When building collaborative apps for Microsoft 365, one of the central pieces of data is information about people. Whether it’s information about your colleagues, people in your organization who work on relevant files, or people with whom you communicated recently, if you build a collaborative app, it will retrieve information about people from Microsoft 365.

Because we tend to collaborate with a close network of people, the different areas of collaboration will typically spin around the same group of people. And so will our apps keep requesting the same information over and over again: name, job title, contact details, and picture for the same people in different contexts of collaboration.

Cache, cache, cache

Continuously retrieving the same information is slow and unnecessary. Not to mention, if your app does it often, you risk being throttled. Instead, once you retrieve the data, you should cache it, so that subsequent requests can be served from the cache without calling Microsoft Graph. Doing this will significantly improve the performance of your app. Recently, I showed you how you can implement caching in your JavaScript using custom Microsoft Graph middleware. But there is one catch with caching.

Before you cache

Caching works only after you executed a request and stored its response. By matching new requests with requests that have been previously executed, you can decide which information you already have and resolve requests with the data from the cache. But what if your app needs the same information and the same requests are issued simultaneously?

Because the data hasn’t been retrieved yet, there is nothing to serve from the cache so by default the app will call Microsoft Graph for each request. You can however extend the cache middleware with tracking pending requests and have similar requests wait for the response to the original request before proceeding!

Before we continue, you might ask: why should you care? If you’re waiting for the response, what difference does it make if you’re waiting for the response to the original request or if you issue another request and wait for that? After all, the user will have to wait for data to appear either way.

You’re right. Users will have to wait for your app to show the data either way. To them, whether you handle simultaneous requests or not will make little difference. What will make a difference is that your app will issue fewer requests to Microsoft Graph. If your app shows a lot of data, you can significantly lower the number of requests to Graph. As a result, your app is a lot less likely to be throttled which will improve the overall performance and user experience! Additionally, once you have the data, you’ll be able to show it everywhere in your app instantaneously and users will notice that.

Avoid duplicate requests with the Microsoft Graph JavaScript SDK

Avoiding duplicate requests with the Microsoft Graph JavaScript SDK consists of two pieces: caching data retrieved from Microsoft Graph and handling simultaneous requests to Microsoft Graph.

You can find a working sample built using the code in this article on GitHub.

The Microsoft Graph JavaScript SDK cache middleware I showed you previously is a good starting point for our needs. It offers us 90% of the solution. What’s left is to add tracking of pending requests and letting subsequent requests to the same endpoint wait until the original request was resolved.

Let’s start by extending the middleware with a few extra functions:

export function BrowserCacheWithPendingMiddleware(expirationConfig) {
  // ... the original caching middleware trimmed for brevity

  const wait = (durationMs) => {
    return new Promise((resolve) => {
      setTimeout(() => resolve(), durationMs);
    });
  }

  const getPendingRequestKey = (requestKey) => {
    return `pending-${requestKey}`;
  }

  const setPending = (requestKey) => {
    window.sessionStorage.setItem(getPendingRequestKey(requestKey), true);
  }

  const clearPending = (requestKey) => {
    window.sessionStorage.removeItem(getPendingRequestKey(requestKey));
  }

  const isPending = (requestKey) => {
    return window.sessionStorage.getItem(getPendingRequestKey(requestKey));
  }

  // ... the original caching middleware trimmed for brevity
};

The wait function will let us stop the code execution for the specified number of milliseconds. It uses a promise so that it can be easily called in an async function.

The set-, clear- and isPending functions are responsible for tracking and clearing pending requests. They use session storage which is also used by the cache middleware. The storage key uniquely identifies each request. In this sample code, it’s built off of the request’s URL but you could extend it to include the request method and specific headers if needed.

The final step, is to extend the middleware’s execute function with the functions for tracking pending requests:

export function BrowserCacheWithPendingMiddleware(expirationConfig) {
  // ... trimmed for brevity

  return {
    execute: async (context) => {
      const requestKey = btoa(context.request);

      while (isPending(requestKey)) {
        console.debug(`Pending ${context.request}`);
        await wait(10);
      }

      let response = window.sessionStorage.getItem(requestKey);
      let now = new Date();
      if (response) {
        const resp = JSON.parse(response);
        const expiresOn = resp.expiresOn ? new Date(resp.expiresOn) : undefined;
        if (!expiresOn || expiresOn > now) {
          console.debug(`From cache ${context.request}`);

          let body;
          if (resp.headers['content-type'].indexOf('application/json') > -1) {
            body = JSON.stringify(resp.body);
          }
          else {
            body = dataUrlToBlob(resp.body);
          }
          context.response = new Response(body, resp);
          return;
        }
        else {
          console.log(`Cache expired ${context.request}`);
        }
      }

      console.debug(`From Graph ${context.request}`);
      setPending(requestKey);
      await this.nextMiddleware.execute(context);

      if (context.options.method !== 'GET' ||
        context.response.status !== 200) {
        // don't cache non-GET or failed requests
        clearPending(requestKey);
        return;
      }

      const resp = context.response.clone();

      const expiresOn = getExpiresOn(resp.url);
      // reset date to catch expiration set to 0
      now = new Date();
      // don't cache if the item already expired
      if (expiresOn <= now) {
        clearPending(requestKey);
        return;
      }

      const headers = getHeaders(resp.headers);
      let body = '';
      if (headers['content-type'].indexOf('application/json') >= 0) {
        body = await resp.json();
      }
      else {
        body = await blobToDataUrl(await resp.blob());
      }

      response = {
        url: resp.url,
        status: resp.status,
        statusText: resp.statusText,
        headers,
        body,
        expiresOn
      };
      window.sessionStorage.setItem(requestKey, JSON.stringify(response));
      clearPending(requestKey);
    },
    setNext: (next) => {
      this.nextMiddleware = next;
    }
  }
};

We start with checking if there is another request to the same endpoint already pending:

while (isPending(requestKey)) {
  console.debug(`Pending ${context.request}`);
  await wait(10);
}

If there is a pending request already, we wait for 10ms and check again. Once the pending request has been completed, we proceed.

Next, we check if the response for the current request is already in the cache. If we cached the data previously, we return the data and complete the middleware.

If no response for the current request is available in the cache, we mark the request as pending and pass the request on down the middleware chain to call Microsoft Graph:

setPending(requestKey);
await this.nextMiddleware.execute(context);

Next, we need to process the response. Here, we chose to clear the pending state marker of the request on each exit code branch. While you could do it in one place, right after the middleware completed, you’d run into the case where the request has been completed but not yet cached and another request to the same Microsoft Graph endpoint would be executed. You could circumvent this by extending the wait duration but to keep things faster, we chose to clear the pending marker in multiple places.

That’s it! If you check the results in the browser’s console, you’ll see the following.

On the first call, the first request will go to Microsoft Graph. The second request will wait until the first request succeeded:

Browser's console window with multiple log statements

Looking at the network tab, you’ll see that only one request has been issued to Microsoft Graph.

Request to Microsoft Graph listed in the browser's network tab

After reloading the app, you’ll see that all data has been served from the cache:

Browser's console window with multiple log statements

Looking at the network tab will confirm that the app hasn’t called Microsoft Graph.

Browser's network tab with some requests but without any request to Microsoft Graph

That’s it!

Summary

Avoiding issuing simultaneous requests to the same Microsoft Graph endpoint can help you avoid throttling and improve the performance of your app. By building a custom middleware for the Microsoft Graph JavaScript SDK you can implement caching and tracking of pending requests which will allow you to avoid calling the same Microsoft Graph endpoints in parallel.

Others found also helpful: