ECMAScript 7: Promises in AppStudio

2686
4
06-05-2019 01:00 AM
StephenQuan1
Esri Contributor
2 4 2,686

1. Javascript Promise

AppStudio 3.3, Qt 5.12.1 and ECMAScript 7 (which includes ECMAScript 6) introduced native Javascript Promises.

This blog is to give you a brief overview of what Promises are and how they matter to AppStudio and QML. This blog isn't a comprehensive guide to Promises. There already is a lot of content on the internet. I recommend you seek them out.

Promises is a Javascript coding pattern for working with asynchronous tasks.

AppStudio (and QML) has signals and slots for working with asynchronous tasks. If your applications are written and easy to maintain then you probably don't need to look into Promises right now.

However, if your applications are complex with business logic spanning over a complex sequence of signals and slots then buying into Promises may be the key to reduce code complexity.

Note that the ArcGIS.AppFramework.Promises module is being deprecated in favour of native Javascript Promises. We advise discontinuing using ArcGIS.AppFramework.Promises module and begin removing them from your existing AppStudio apps.

2. Promise Download Sample

Let's look at an AppStudio app that simulates downloading content from the internet.

When the Button is clicked, we simulate 3 concurrent downloads. The downloads will finish 0-1000ms after the Button's onClicked handler has finished. Each download's success/failure will be independent of each other.

    Button {
      text: qsTr("Test Promise timeout")
      onClicked: {
        download( "GET", "https://community.esri.com/groups/appstudio" )
          .then( (message) => { console.log( message ) } )
          .catch( (error) => { console.log( error ) } )
        download( "GET", "https://appstudio.arcgis.com" )
          .then( (message) => { console.log( message ) } )
          .catch( (error) => { console.log( error ) } )
        download( "GET", "https://community.esri.com/groups/survey123" )
          .then( (message) => { console.log( message ) } )
          .catch( (error) => { console.log( error ) } )
      }
    }

    function download( method, url ) {
      return new Promise( (resolve, reject) => {
        if (Math.random() < 0.30)
          setTimeout( reject, Math.floor(1000 * Math.random()), `Download failure ${url}` )
        else
          setTimeout( resolve, Math.floor(1000 * Math.random()), `Download success ${url}` )
      } )
    }‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

View the full PromiseTimeout.qml on GitHub Gist.

Some key features of Promises:

  • The task begins as soon as new Promise() { ... } is called.
  • The task is defined as (resolve, reject) => { /* body */ } where either resolve(data) or reject(error) will be called when the task completes.
  • Use then() method to supply your success handler.
  • Use catch() method to supply your failure handler.

You may be thinking "This looking confusing" and "Do I need this?". I ask you to consider this: the Button's onClick handler fully describes 3 download tasks and what to do when each of them finishes. It describes this all this in one place, in the one function. If you didn't use Promises, we would have to create numerous signals and slots and spread the implementation throughout the application.

3. Promise chaining

Promise chaining is a coding pattern where you want to run your asynchronous tasks sequentially. You may need to do this if you wish to use the output of one task as an input to the next.

When the Button is clicked, we want to start the download of 3 web pages, one after the after. If downloads are successful, you will see the web page's title. If any task fails, we do not continue downloading. (N.B. here, the download function is real. The download function returns the web page's title).

    Button {
      text: qsTr( "Test Promise chaining" )
      onClicked: {
        download( "GET", "https://community.esri.com/groups/appstudio" )
        .then( (data) => {
          console.log( data.title )
          return download( "GET", "https://appstudio.arcgis.com" )
        } )
        .then( (data) => {
          console.log( data.title )
          return download( "GET", "https://community.esri.com/groups/survey123" )
        } )
        .then( (data) => {
          console.log( data.title )
        } )
        .catch( (error) => { console.log( error ) } )
      }
    }‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

View the full PromiseChaining.qml on GitHub Gist.

However, due to the https://bugreports.qt.io/browse/QTBUG-71329, Promise chaining doesn't work in AppStudio 3.3, Qt 5.12.1. Please vote to have it fixed! Because of the bug, the app does not run correctly:

 

4. Promise nesting (alternative to Promise chaining)

Since Promise chaining doesn't work in AppStudio 3.3, Qt 5.12.1 let's explore some alternatives. This one demonstrates Promises nesting where each subsequent task is nested in the success of the previous task. (N.B. here the download function returns an object containing the status, responseText, and title).

    Button {
      text: qsTr( "Test Promise nesting" )
      onClicked: {
        download( "GET", "https://community.esri.com/groups/appstudio" )
        .then( (data) => {
          console.log( data.title )
          download( "GET", "https://appstudio.arcgis.com" )
          .then( (data) => {
            console.log( data.title )
            download( "GET", "https://community.esri.com/groups/survey123" )
            .then( (data) => {
              console.log( data.title )
            } )
            .catch( (error) => { console.log( error ) } )
          } )
          .catch( (error) => { console.log( error ) } )
        } )
        .catch( (error) => { console.log( error) } )  
      }
    }‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

View the full PromiseNesting.qml on GitHub Gist.

This implementation works, but it is extremely ugly. It is infamously known as callbackhell. You see each subsequent task incurs another level of indentation and another copy of the error handler.

5. Promise async/await (alternative to Promise chaining)

async/await is another alternative to Promise chaining. The main characteristic is the use of the new async and await keywords and that the main body reads sequentially and elegantly.

    Button {
      text: qsTr( "Test Promise async/await" )
      onClicked: {
        (async () => {
          try {
            console.log( ( await download( "GET", "https://community.esri.com/groups/appstudio") ).title)
            console.log( ( await download( "GET", "https://appstudio.arcgis.com") ).title)
            console.log( ( await download( "GET", "https://community.esri.com/groups/survey123" ) ).title)
          } catch ( error ) {
            console.log( error )
          }
        } )()
      }
    }‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

View the full PromiseAsyncAwait.qml on GitHub Gist.

Unfortunately, this feature didn't make it to AppStudio 3.3, Qt 5.12.1. Help get this into a future release of AppStudio by voting for it at https://bugreports.qt.io/browse/QTBUG-58620.

6. Promise async/await Babel (alternative to Promise chaining)

Generator functions made it into AppStudio 3.3, Qt 5.12.1. We can use https://babeljs.io/ to convert the async/await into Generator functions. This version works in AppStudio 3.3, Qt 5.12.1. Here's the converted output:

    Button {
      text: qsTr( "Test Promise async/await (Babel)" )
      onClicked: {
        _asyncToGenerator( function*() {
          try {
            console.log( ( yield download( "GET", "https://appstudio.arcgis.com" ) ).title )
            console.log( ( yield download( "GET", "https://community.esri.com/groups/appstudio" ) ).title )
            console.log( ( yield download( "GET", "https://community.esri.com/groups/survey123" ) ).title )
          } catch ( error ) {
            console.log( error )
          }
        } )()
      }
    }‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

View the full PromiseBabel.qml on GitHub Gist.

While waiting for the async/await feature to be implemented, I think the above syntax is compelling.

It is extremely close to async/await syntax and, I think, is significantly better than Promise chaining.

To make it work, _asyncToGenerator() and asyncGeneratorStep() functions are required.


7. Promise.all and Promise.race

Promise.all is a special Promise that succeeds by waiting for every sub Promise to succeeds else it fails to report the error from the first failing sub Promise.

Promise.race is a special Promise in that we are only interested in the first sub Promise that finishes the race. We aren't interested in the completion of the subsequent Promises. We will be notified of the success message or failure error of that first sub Promise.

Surprisingly, no matter how many times you try, you find the AppStudio Home Page always wins the race.

Why is the second Promise always succeeding in the race? That's because the community GeoNet page takes longer to load!

8. Closing remarks

I hope this blog helps gives a good overview ofPromises and why they matter in your AppStudio apps.

Promises, while new in AppStudio 3.3, Qt 5.12.1 is a feature that other Javascript developers have already been enjoying for some time now.

Promises, I believe, work very well with Qt's signal and slot mechanism taking it to a whole new level.

Promise chaining and async/await functions do not work. We need your help to make AppStudio come closer to parity with other Javascript developer environments by voting on getting these two bugs fixed:

Whilst we wait for async/await functions, I highly recommend you use https://babeljs.io/ to get async/await features today.

9. References

Send us your feedback to appstudiofeedback@esri.com

 

Become an AppStudio for ArcGIS developer! Watch this video on how to sign up for a free trial.

 

Follow us on Twitter @AppStudioArcGIS to keep up-to-date on the latest information and let us know about your creations built using AppStudio to be featured in the AppStudio Showcase.

 

The AppStudio team periodically hosts workshops and webinars; please click on this link to leave your email if you are interested in information regarding AppStudio events.

4 Comments