A Work in Progress

developer's blog by Wei
lots of work in progress notes
TimelineCredits

Don't Render, I'm Still Loading the Script...

November 07, 2019

Nowadays we normally load script tags asynchronously and try not to block other things as much as possible. A lot of APIs are designed to be innately asynchronous. What can we do to make sure certain things happen after certain script is executed? And in particular, when the callback isn't explicitly given as a Promise?

Problem

Today, well, not today, recently, I've got this problem:

The gapi works this way:

  1. you include the gapi bundle
  2. the bundle has no other job but to load some other bundle (one that you will use)
  3. after the module you need (auth in my case) is loaded, you still need to initialize an instance with some customized data (client id, as an example), which is yet another asynchronous call
  4. then you have access to that instance, which in turn has the instance methods you will use

From Google's docs, here's how you load the script for the API:

<script
  src="https://apis.google.com/js/platform.js?onload=init"
  async
  defer
></script>

What this does is it will load the said script asynchronously, meaning it will not block other resources from loading or executing, and defer means the script is meant to be executed after the document has been parsed (MDN docs).

The ?onload=init search query means that it will call the init function you bind to window once the gapi bundle is ready. So normally you write gapi.load in an init function:

function init() {
  gapi.load('auth2', function() {
    /* Ready. Make a call to gapi.auth2.init or some other API */
  });
}

The instance initialization (step 3 mentioned above) is another async call that returns a "thenable" type.

This workflow probably works desirably for most sites. Except in a particular project I'm working on recently, I want to block something from happening until the whole authentication instance is ready. We're talking about login, and in particular, I need the authentication instance to be instantiated as early as possible, and whichever page that ever inquires whether the user is logged in or not will hold its first execution until the auth instance is ready.

There are other reasons why I cannot let that particular part of my app go through multiple render passes but to wait on its first render. In summary, I have very tight requirement on how this workflow must happen once and each part must wait properly.

So how to waiiiiiiit

The script tag

  • put it in head
  • remove async, defer, make it blocking 🙄

Waiting for "thenable" object

window.gapi.auth2
  .init({
    client_id: 'client-id',
  })
  .then(auth => {
    auth.signIn(); // or other instance methods
  });

If something is "thenable", i.e., async functions and promises, we can await:

await window.gapi.auth2.init();

and it will properly wait.

Turning a load callback into a promise

What got me a lot of trouble was this line

gapi.load('auth2', callback);

I need to provide that callback, which will happen once auth2 is loaded. But I have no control over when that happens and finishes. So how 🤷🏻‍♀️

I was very stuck until I saw an one of the examples from Google, where you can create and return a promise, and pass resolve as a callback:

var loadGapiClient = new Promise(function(resolve, reject) {
  gapi.load('clent:auth2', resolve);
});

MDN docs also gave another simpler example but somehow it never occured to me that I could create a Promise and pass its resolve as callback to gapi.load, which effectively transfers the callback into something "thenable", until I saw Google's example in a closer context.

const load = new Promise(function(resolve, reject) {
  gapi.load('auth2', resolve);
});

This turns the original candidate for callback, we don't have hold of its start and end of execution, into "thenable" type. Note how even callback may not even be asynchronous to begin with, because it was previously at a "callback" position, we lose control of that workflow.

After wrapping it in a Promise, we still don't have hold of when it happens, but we get better control of our entire workflow because it now expresses that certain things should happen in sequence.

load.then(() => {
  window.gapi.auth2
    .init({
      client_id: 'client-id',
    })
    .then(auth => {
      auth.signIn(); // or other instance methods
    });
});

But I was still pretty stuck. I now kind of have this process that happens in sequence. How do I make sure that all my related function will hang on this workflow, in case it's not completed?

So I brought this to my senpai who solved the mystery by telling me whenver you have a Promise you can return it and put it into an async function, then you can await that async function that will make it kinda synchronous.

It's probably not the exact words but that's a same kind of words my first college math professor said

you can do an integration by parts when you have two components one multiplied by the derivatives of the other, added by the derivative of itself multiplied by the other

..., something like that. But let's keep math to just that extent.

So eventually my implementation looked like this:

async function loadAuthInstance() {
  const load = new Promise(function(resolve, reject) {
    gapi.load('auth2', resolve);
  });
  return load.then(async () => {
    return await window.gapi.auth2
      .init({
        client_id: 'client-id',
      })
      .then(authInstance => {
        window.authInstance = authInstance;
      });
  });
}

And other parts that depend on window.authInstance can now properly wait:

function login() {
  if (!window.authInstance) {
    await loadAuthInstance();
  }
  window.authInstance.login();
}

On a side note, if you keep returning the await in the then() callbacks, you're "chaining async functions". This is a very hipster action and there are already a whole world of people talking about it.

Christmas wishes

  • include more real world examples in guides
  • summarize use cases in sensible ways
  • get unafraid of Promise, async and await

Speaking of takeaways, this line clicks for me:

Synchronous functions return values, async ones do not and instead invoke callbacks

Relevant links