Top-Level Awaiting

For Godot

With your host

@MylesBorins

DotJS 2018

Bonjour!

My Name is Myles

itsa me!

I am gainfully employed by Google as a Developer Advocate

Focusing on the Node.js ecosystem and GCP

Google Cloud Platform
The opinions expressed in this talk are solely my own

Chapter One

Asynchronicity

is hard

Don't stop,

don't stop the thread

From the beginning of time (1995)

We had callbacks in JavaScript

a((aa) => {
    b(aa, (bb) => {
        c(bb, (cc) => {
            d(cc, (dd) => {
                e(dd, (OMG) => {
                    throw 'me into the abyss';
                });
            });
        });
    });
});

Promises

Made it into the language ~20 years later in 2014

a.then((aa) => {
  return b(aa);
}),then((bb) => {
  return c(bb);
}).then((cc) => {
  return d(cc);
}).then((OMG) => {
  throw 'me into the abyss';
}).catch((e) => {
  console.log('I got chu bae');
});

Async / Await

Arrived in December of 2016

(async function main () {
  try {
    const aa = await a();
    const bb = await b(aa);
    const cc = await c(bb);
    const OMG = await d(cc);
    throw 'me into the absyss';
  }
  catch (e) {
    console.log('never mind');
  }
}());

Chapter Two

Don't tell me

what I can't do

February 21 2017

Node.js 7.6.0 is release

It includes V8 5.5

which has support for async / await

I was really excited to use await until...

error message when trying to use Top-Level Await today

It looked like

Top-Level Await

Wasn't a thing

May 31 2017

My First attempt to make TLA work in node

a naive node patch to make TLA work that broke everything

Spoiler: It Didn't

It turned out that the Node.js test suite

Wasn't the only place that had an issue

With Top-Level Await

Chapter Three

Top-level await

is a footgun 👣🔫

In September of 2016

Rich Harris wrote

Top-Level Await is a footgun

https://gist.github.com/Rich-Harris/0b6f317657f5167663b493c722647221

a gist that would be brought up whenever i mentioned

Top-Level Await

TL;DR

Despite this strong critique

There was no prior decision from TC39 regarding Top-Level Await

I decided to take Top-Level Await to the committee

Worst case scenario, get a solid no and stop flame wars

Chapter Four

A history lesson

Top-Level await was always being considered

while async / await was being standardized

I did some research by reviewing the agendas of past meetings.

http://github.com/tc39/agendas/

Some quick history of

async / await

taking a step back

how is language babby formed

The Spec is

ECMA-262

The Committee who implements it is

logo for tc39

Consensus

The Stages

  • Stage 0: strawman
  • Stage 1: proposal
  • Stage 2: draft
  • Stage 3: candidate
  • Stage 4: finished
  • Chapter Five

    A footgun in the door

    In January 2018

    Top-Level await was brought TC39

    Attempting for Stage 1

    The proposal included

    Motivation #1

    IIAFE

    Immediately invoked async function expression
    import static1 from './static1.mjs';
    import { readFile } from 'fs';
    
    (async () => {
      const dynamic = await import('./dynamic'
        + process.env.something + '.mjs');
      const file = JSON.parse(await readFile('./config.json'));
      // main program goes here...
    })();
    

    Motivation #2

    Completely Async Modules

    export default async () => {
      // import other modules like this one
      const import1 = await (await import('./import1.mjs')).default();
      const import2 = await (await import('./import2.mjs')).default();
    
      // compute some exports...
      return {
        export1: ...,
        export2: ...
      };
    };
    

    Potential Solutions

    Variant A

    A call to top-level await would block execution of the graph until it had resolved.

    Assuming A, B, C

    all have a Top-Level await

    import a from './a.mjs'
    import b from './b.mjs'
    import c from './c.mjs'
    
    console.log(a, b, c);
    

    The code is equivalent to

    (async () => {
      const a = await import('./a.mjs');
      const b = await import('./b.mjs');
      const c = await import('./c.mjs');
    
      console.log(a, b, c);
    })();
    

    Variant B

    A call to top-level await would block execution of child nodes in the graph but would allow siblings to continue to execute.

    Assuming A, B, C

    all have a Top-Level await

    import a from './a.mjs'
    import b from './b.mjs'
    import c from './c.mjs'
    
    console.log(a, b, c);
    

    The code is equivalent to

    (async () => {
      const [a, b, c] = await Promise.all([
        import('./a.mjs'),
        import('./b.mjs'),
        import('./c.mjs')
      ]);
      console.log(a, b, c);
    })();
    

    Optional Constraint

    Enforcing that top-level await could only be used inside of a module without exports.

    Use Cases

    Dynamic dependency pathing

    export const strings =
      await import(`/i18n/${navigator.language}`);
    

    Resource initialization

    const connection = await dbConnector();

    Dependency fallbacks

    let jQuery;
    try {
      jQuery = await import('https://cdn-a.com/jQuery');
    } catch {
      jQuery = await import('https://cdn-b.com/jQuery');
    }
    

    Constraints

    There are existing ways to halt progress in JavaScript

    Infinite Loops

    for (const n of primes()) {
      console.log(`${n} is prime}`);
    }
    

    Infinite Recursion

    const fibb = n => (n ? fibb(n - 1) : 1);
    fibb(Infinity);
    

    Atomics.wait

    Atomics.wait(shared_array_buffer, 0, 0);
    Atomics allow blocking forward progress by waiting on an index that never changes

    Is Blocking Execution

    A strong enough reason

    To block Top-Level Await?

    Another Constraint:

    Deadlock

    Achievement unlocked

    Top-Level Await

    was moved to Stage 1

    Chapter Six

    The Road

    To Stage 2

    Reaching stage 1 was a big step

    It signaled that TC39 was interested

    In exploring the problem space of TLA

    Moving to stage 2 requires reaching consensus

    On the general shape of the solution to the problem

    We identified necessary

    spec changes

    When your ESM code is executed

    It is represented

    as a graph of

    Source Text Module Records

    A Source Text Module Record is used to represent information about a module that was defined from ECMAScript source text that was parsed using the goal symbol Module.

    ECMA262 defines how these Records are instantiated

    and how they are represented in memory

    We made all modules in the graph

    Async

    To do this we abstracted code from the

    Async Function Specification

    for instantiation of Module Records in the graph

    With this infrastructure in place

    Every module would now have a promise capability

    Allowing for deferred execution at the Top-Level

    We were able to defer execution by calling

    Spec-Level Await on the execution of the module

    Yes, the spec has it's own abstractions

    this algorithm supports Variant A

    A spec level Promise.all would add support

    for Variant B

    as such we were able to defer

    making a decision

    on A versus B

    Unblocking Objections to either semantics

    We clarified that the feature would only be supported

    In the module goal

    Unblocking objections about interoperability

    We deferred to current behavior

    For handling deadlock

    Unblocking objections related to cycles

    In may of 2018

    Top-Level Await

    went to stage 2

    Chapter Seven

    What's next?

    I want to move forward

    With Variant B

    We need to build consensus around these semantics

    We are going through a variety of examples

    To define semantics for deadlock

    The intent is to make things fail early and obviously

    We need to finish writing the spec text

    The text then needs to be reviewed and approved

    ETA, ¯\_(ツ)_/¯

    const [you, me] = Promise.all([
      await topLevelAwait('patience')
      await topLevelAwait('persistence');
    ]);

    Thank You

    a surfing puppy

    @MylesBorins