Async JS: World beyond callbacks

Sync vs async

Event loop

Events

Creating & triggering events

var event = new Event('build');

// Listen for the event.
elem.addEventListener('build', function (e) { ... }, false);

// Dispatch the event.
elem.dispatchEvent(event);

Events are "producing" callbacks *

* Except for Rx
** TODO: Extend this presentation with Rx

Callbacks

(function getSmthAsync(callback, context){
  
  setTimeout(callback.bind(context, Math.random()), 3000);

}(console.log, console));

Callbacks R ez

Pyramid of DOOM*

* Nothing to do with callback though

Easy, right?

Also we can execute several async actions like fetching data and count all callbacks executions to execute one "success" handler at the end. 

Handy isn't it?

promises

promise can be:

fulfilled - The action relating to the promise succeeded

rejected  - The action relating to the promise failed

pending - Hasn't fulfilled or rejected yet

settled - Has fulfilled or rejected

thenable  - Promise-like object which has `then` method

browser support

var promise = new Promise(function(resolve, reject) {

  // do a thing, possibly async, then…

  if (/* everything turned out fine */) {
    resolve("Stuff worked!");
  }
  else {
    reject(Error("It broke"));
  }

});

promise example

promise.then(function(result) {

  console.log(result); // "Stuff worked!"

}, function(err) {

  console.log(err); // Error: "It broke"

});

promise example (continue)

var promise = new Promise(function(resolve, reject) {
  resolve(1);
});

promise.then(function(val) {

  console.log(val); // 1
  return val + 2;

}).then(function(val) {

  console.log(val); // 3

});

Chaining & transforming values

var promise = new Promise(function(resolve, reject){

  setTimeout(function(){
      throw new Error("Booyaa!");
  }, 3000);

});

promise
    .then(console.log.bind(console, "resolved"))
    .catch(console.warn.bind(console, "rejected"));

Error handling

var _userMedia;
_userMedia = navigator.mediaDevices && navigator.mediaDevices.getUserMedia ?
    navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices) : function(constraints){

       return new Promise(function(resolve, reject){
           var userMedia = (navigator.webkitGetUserMedia || navigator.mozGetUserMedia).bind(navigator);
           userMedia(constraints, resolve, reject);
       });

};

Promisifying things

var _requestDevices = function(resolve, reject){
    var self = this;

    /**
     * Run 7s timer for user to react on gUM permission dialog
     * Stop it when permissions are granted or not
     * In case user clicks "close" or "not now" on dialog (firefox only)
     * original promise wound be still pending which stops stats polling
     * so in this case promise is rejected to continue
     * and device labels wont be gathered on next try
     */
    var timer = setTimeout(function(){
        self._wasGUMRequested = true;
        reject(new Error("gUM timeout"));
    }, 7000);

    _userMedia({

        audio: true, video: true

    }).then(function(stream){

        stream.getTracks().forEach(function(track){
            track.stop();
        });

        self._wasGUMRequested = true;
        clearTimeout(timer);

        return self._enumerateDevices();

    }).then(function(devices){

        // since previous thenable returned promise
        // this one will wait until that promise is fulfilled
        resolve(devices);

    }).catch(function(err){

        self._wasGUMRequested = true;
        clearTimeout(timer);

        reject(err);

    });
}

Queuing asynchronous actions

var EnumeraTOR = function(){

    this._userMedia = navigator.mediaDevices && navigator.mediaDevices.getUserMedia ?
        navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices) : function(constraints){
    
          return new Promise(function(resolve, reject){
              var userMedia = (navigator.webkitGetUserMedia || navigator.mozGetUserMedia)
                    .bind(navigator);
              userMedia(constraints, resolve, reject);
          });

    };

    this._enumerateDevices = navigator.mediaDevices && navigator.mediaDevices.enumerateDevices ?
        navigator.mediaDevices.enumerateDevices.bind(navigator.mediaDevices) : function(){

            return new Promise(function(resolve){
                MediaStreamTrack.getSources(resolve);
            });
    };

    this.enumerateDevices = function(){
        var self = this;

        // if gUM permissions were requested before go strait to enumeration
        // no probing or gUM request is necessary
        if (this._wasGUMRequested) {
            return this._enumerateDevices();
        }

        return new Promise(function(resolve, reject){
            // probing maybe gUM was called before and we already has access to label
            self._enumerateDevices()
                .then(self._probe.bind(self, resolve))
                .then(function(devices){
                    // apparently probing was successful
                    if (self._wasGUMRequested) {
                        return devices;
                    }

                    self._requestDevices(resolve, reject);
                })
                .catch(reject);
        });
    };

    /**
     * Probing device list for label availability
     * In case we have access to it it will resolve original promise
     * @param resolve
     * @param devices
     * @private
     */
    this._probe = function(resolve, devices){
        var hasLabel = devices.some(function(device){
            return !!device.label;
        });

        if (hasLabel) {
            this._wasGUMRequested = true;
            resolve(devices);
        }
    };

    this._requestDevices = function(resolve, reject){
        var self = this;
    
        /**
         * Run 7s timer for user to react on gUM permission dialog
         * Stop it when permissions are granted or not
         * In case user clicks "close" or "not now" on dialog (firefox only)
         * original promise wound be still pending which stops stats polling
         * so in this case promise is rejected to continue
         * and device labels wont be gathered on next try
         */
        var timer = setTimeout(function(){
            self._wasGUMRequested = true;
            reject(new Error("gUM timeout"));
        }, 7000);
    
        this._userMedia({
    
            audio: true, video: true
    
        }).then(function(stream){
    
            stream.getTracks().forEach(function(track){
                track.stop();
            });
    
            self._wasGUMRequested = true;
            clearTimeout(timer);
    
            return self._enumerateDevices();
    
        }).then(function(devices){
    
            // since previous thenable returned promise
            // this one will wait until that promise is fulfilled
            resolve(devices);
    
        }).catch(function(err){
    
            self._wasGUMRequested = true;
            clearTimeout(timer);
    
            reject(err);
    
        });
    }

};

All together

Static Methods

Promise.resolve(promise|thenable|obj);

Promise.reject(obj);

 

Promise.all(array);

Make a promise that fulfills when every item in the array fulfills, and rejects if (and when) any item rejects. Each array item is passed to Promise.resolve, so the array can be a mixture of promise-like objects and other objects. The fulfillment value is an array (in order) of fulfillment values. The rejection value is the first rejection value.

 

Promise.race(array);

Make a Promise that fulfills as soon as any item fulfills, or rejects as soon as any item rejects, whichever happens first.

promise.all & return value

var StatsCollecTOR = function(){

    this.start = function(cfg){
        cfg = cfg || { session: { user: {}, room: {} } };

        this._pc = cfg.pc;
        this._sessionInfo = cfg.session;

        if (!this._pc) {
            return;
        }

        if (this._isStarted) {
            this._resetPC();
        }

        this._isStarted = true;

        this._updateSessionStats();
        this._updateStatesStats();

        var _statsPolling = function(){
            var self = this;
            this._getStats().then(function(stats){

                self._cb.call(self._ct, stats[0]);
                self._timerId = setTimeout(_statsPolling, self._INTERVAL);

            }).catch(function(err){

                console.warn(err);
                self._timerId = setTimeout(_statsPolling, self._INTERVAL);

            });
        }.bind(this);

        if (!this._timerId) {
            this._updateSystemStats();
            _statsPolling();
        }
    };

    this._getStats: function(){
        if (!this._pc) {
            return Promise.reject(new Error("no peer connection available for stats module"));
        }

        this._stats.ts = Date.now() / 1000;

        return Promise.all([
            window.chrome ? this._getChromeStats() : this._getFirefoxStats(),
            this._getDeviceStats()
        ]);
    };

};

Dealing with _this, that, self,...

var Fx = function(){

    this.go = function(){
        return this;
    };
    this.do = function(){
        return this;
    };

    this.it = function(){
        var promise = new Promise(function(resolve, reject){
            /* important stuff goes here */
        });

        /**
         * ES5
         */
        var self = this;

        return promise.then(function(input){
            self._process(input);            
        }).catch(function(fail){
            this._reportFailure(fail);
        }.bind(this));

        /**
         * ES6
         */

        return promise.then(input => {
            this._process(input);           
        }).catch((fail) => {
            this._reportFailure(fail);
        });
    };

};

var f = new Fx();
f.go().do().it().then(/* ^_^ */);

Pyramid of theN Doom. again.

promise
    .then(/* action 1 */)
    .then(/* action 2 */)
    .then(/* action 3 */)
    .then(/* action 4 */, 
          /* error handler for action 4 (post-3 actually) */)
    .catch(/* common error handler */);

async/await*

var request = require('request');
 
function getQuote() {
  return new Promise(function(resolve, reject) {
    request('http://ron-swanson-quotes.herokuapp.com/v2/quotes', function(error, response, body) {
      if (error) return reject(error);
      resolve(body);
    });
  });
}
 
async function main() {
  try {
    var quote = await getQuote();
    console.log(quote);
  } catch(error) {
    console.error(error);
  }
}
 
main();
console.log('Ron once said,');

* ES7

* Workers

Web worker

main.js:

var myWorker = new Worker("worker.js");

myWorker.postMessage([first.value,second.value]);
console.log('Message posted to worker');

myWorker.onmessage = function(e) {
  result.textContent = e.data;
  console.log('Message received from worker');
}

// myWorker.terminate();


worker.js:

// importScripts('foo.js', 'bar.js');

onmessage = function(e) {
  console.log('Message received from main script');
  var workerResult = 'Result: ' + (e.data[0] * e.data[1]);

  console.log('Posting message back to main script');
  postMessage(workerResult);

  // close();
}

Can spawn sub-workers

Shared worker

main.js:

var myWorker = new SharedWorker("worker.js");

myWorker.port.start();

myWorker.port.postMessage([first.value,second.value]);
console.log('Message posted to worker');

myWorker.port.onmessage = function(e) {
  result.textContent = e.data;
  console.log('Message received from worker');
}


worker.js:

self.addEventListener('connect', function(e) { // addEventListener() is needed
  var port = e.ports[0];

  port.onmessage = function(e) {
    var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
    port.postMessage(workerResult);
  }

  port.start();  // not necessary since onmessage event handler is being used
});

inlining workers

var worker = new Worker("data:text/javascript;charset=US-ASCII,onmessage%20%3D%20function%20%28oEvent%29%20%7B%0A%09postMessage%28%7B%0A%09%09%22id%22%3A%20oEvent.data.id%2C%0A%09%09%22evaluated%22%3A%20eval%28oEvent.data.code%29%0A%09%7D%29%3B%0A%7D");


<script id="worker" type="javascript/worker">
    self.importScripts("https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/highlight.min.js");
    self.onmessage = function(e) {
        var result = self.hljs.highlightAuto(e.data.text);
        self.postMessage({
            id: e.data.id,
            text: result.value
        });
    };
</script>

var worker = new Worker(
    URL.createObjectURL(
        new Blob(
            [$("#worker").text()], 
            {type: "text/javascript"}
        )
    )
);

That's it for today.

Async JS: World beyond callbacks

By Mykhailo Lieibenson

Async JS: World beyond callbacks

Overview of async technics in JS: event loop, callbacks, promises, events, web workers

  • 1,064