segunda-feira, 29 de agosto de 2016

Promise in Smart Mobile Studio

The traditional way to deal with asynchronous tasks in smart pascal are callbacks; call a method, give it a function reference to execute once that method is done. For example, say I want to run code asynchronous that:
a) fetch the person object, after half a second;
b) create the message;
c) change the message; and
d) display the message.


type
  TCallbackFn = procedure(d: variant; f: variant);

var person := JSON.parse('{"firstName": "Warley", "lastName":"Alex", "favoriteTeam":"Cruzeiro"}');

var api = CLASS
 getPerson := lambda(callback: TCallbackFn)
          w3_SetTimeout(lambda()
       callback(null, person);
    end, 500);
 end;

 getMessage := lambda(person: variant; callback: TCallbackFn)
   w3_SetTimeout(lambda()
     callback(null, person.firstName + ' ' + person.lastName + ' likes the ' + person.favoriteTeam);
   end, 500);
 end;

 mapMessage := lambda(message: variant): variant
     Result := message + ', the 2nd worst team in the Brazilian footbal league.';
 end;
END;

{ using callback approach }
api.getPerson(lambda(err, person)
  api.getMessage(person, lambda(err, message)
    console.log(api.mapMessage(message));
  end);
end);

{ Warley Alex likes the Cruzeiro, the 2nd worst team in the Brazilian footbal league.}

This is pretty neat, it is often much easier to read inline anonymous functions, as in the example. It will also spare you from having to find a name for all the callbacks. In such cases, using inline anonymous functions is even more preferable.
But, it has some drawbacks; for one, combining or chaining multiple asynchronous processes is tricky; it either leads to a lot of boilerplate code, or what's known as callback hell.

Callback hell is any code where the use of function callbacks in async code becomes obscure or difficult to follow. Generally, when there is more than one level of indirection, code using callbacks can become harder to follow, harder to refactor, and harder to test. A code smell is multiple levels of indentation due to passing multiple layers of function literals.

There is an alternative to dealing with asynchronous code in smart pascal: using Promises.

Promises are an alternative to callbacks for delivering the results of an asynchronous computation.
They require more effort from implementors of asynchronous functions, but provide several benefits for users of those functions. Promises help deal with the callback nesting and with the exception handling.

The Simplest Use Case
Let's begin our promise implementation as simple in Smart Mobile Studio.
Just create an unit and define this externals classes:

type
  JDeferred = class;
  TCallback = procedure(Value: Variant);
  TCallbackFn = procedure(d: variant; f: variant);
  TJDeferred_object_new_fn_ = function (d: TCallback): variant;
  TJDeferred_object_new_fn  = procedure (resolve: TCallback; reject: TCallback);
  TPromiseCallback = function(Value: Variant): Variant;
 TEventHandler = function(event: variant): Variant;

  JPromise = class external "Promise"
    constructor create(fn: TJDeferred_object_new_fn_{ = nil}); overload;
    constructor create(resolve: TJDeferred_object_new_fn_; reject: TJDeferred_object_new_fn_); overload;
    constructor create(fn: TJDeferred_object_new_fn); overload;
    function always(alwaysCallbacks: array of variant): JPromise;
    function done(doneCallbacks: array of variant): JPromise; overload;
    function done(doneCallbacks: variant): JPromise; overload;
    function fail(failCallbacks: array of variant): JPromise;
    function progress(progressCallbacks: array of variant): JPromise;
    function state(): string;
    function &then(doneCallbacks: variant; failCallbacks: variant = undefined; progressCallbacks: variant = undefined): JPromise;
    function &then(onFulfilled: TPromiseCallback = nil): JPromise; overload;
    function &then(onFulfilled: TPromiseCallback; onRejected: TPromiseCallback): JPromise; overload;
    function catch(rejectCallback: Variant = nil): JPromise; overload;
    function catch(rejectCallback: TPromiseCallback): JPromise; overload;
    class function promise(target: Variant): JPromise;
  end;

type
  JDeferred = class external "Promise"(JPromise)
    function notify(args: array of variant): JDeferred;
    function notifyWith(context: variant; args: array of variant): JDeferred;
    function reject(args: array of variant): JDeferred; overload;
    function reject(args: variant): JDeferred; overload;
    function reject(args: TEventHandler): JDeferred; overload;
    function rejectWith(context: variant; args: array of variant): JDeferred;
    function resolve(args: array of variant): JDeferred; overload;
    class function resolve(value: variant = nil): JPromise; overload;
    function resolveWith(context: variant; args: array of variant): JDeferred;
    function all(iterable: Array of Variant): JPromise;
    function race(iterable: Array of Variant): JPromise;
  end;
{ global external functions }
function Promise : JDeferred; external 'Promise' property;
function Error(message: variant): variant; external 'Error';
function document: variant; external "document" property;
function window : Variant; external 'window' property;
function &typeof(obj:variant): variant; overload; external "typeof";

The following function returns a result asynchronously, via a Promise:

function asyncFunction(): JPromise;
begin
Result := new JPromise(
  function (resolve: TCallback): variant
  begin
    w3_SetTimeout(procedure()
    begin
      var value := 42;
      resolve('Async Hello world: ' + IntToStr(value) );
    end, 16);
  end,

  function(reject: TCallback): variant
  begin
   // case rejected
  end);
end;

{* 
  then() always returns a Promise, which enables you to chain method calls:
  Furthermore, note how catch() handles the errors of the asynchronous function asyncFunction(). 
*}


asyncFunction()
  .then(procedure (value: variant)
  begin
    console.log(value); // => 'Async Hello world'
  end)

  .catch(procedure (error: variant)
  begin
    console.log(error);
  end);

Chaining Promises
The real key here is that then() is returning a new promise. Since promises capture the notion of asynchronicity in an object, we can chain them, map them, have them run in parallel or sequential, all kinds of useful things.

Code like the following is very common with promises:

asyncFunction()
.then(filterTheData)
.then(processTheData)
.then(displayTheData);

What value does the second promise resolve to? It receives the return value of the first promise.
In other words, it’s what was passed into the first call to then(). The return value of that first handler is used to resolve the second promise. Thus chaining is accomplished!

The asyncFunction is returning a promise, as evidenced by the call to then(), "then() always returns a promise" but the result of that first then must also be a promise, as we call then() again (and yet again!)
That’s exactly what happens, if we can convince then() to return a promise, things get more interesting.
Since then() always returns a new promise object, there will always be at least one promise object that gets created, resolved and then ignored.

Example:

function asyncFunction(): JPromise;
begin
Result := new JPromise(
  function (resolve: TCallback): variant
  begin
    w3_SetTimeout(procedure()
    begin
      var value := 42;
      resolve('getting_The_Data: ' + IntToStr(value) );
    end, 16);
  end,

  function(reject: TCallback): variant
  begin
   // case rejected
  end);
end;

asyncFunction()
  .then(function (value: variant): variant
  begin
    console.log('first result: '+ value); // => 'getting_The_Data: 42'
    exit('filter_The_Data');
  end)

  .then(function (value: variant): variant
  begin
    console.log('second result: '+ value); // => 'filter_The_Data'
    exit('process_The_Data');  // explicitly returning the string "process_The_Data"
  end)

  .then(function (value: variant): variant
  begin
    console.log('third result: '+ value); // => 'process_The_Data'
    exit('display_The_Data');
  end)

  .then(function (value: variant): variant
  begin
    console.log('forth result: '+ value); // => 'display_The_Data'
  end)

  .catch(procedure (error: variant)
  begin
    console.log(error);
  end);
  
(*  the output is:
 first result: getting_The_Data: 42
 second result: filter_The_Data
 third result: process_The_Data
 forth result: display_The_Data   
*)
What if in the above example, we wanted all the results in the end in one go.

Executing asynchronous functions in parallel


Promises always resolve to one value. If you need to pass more than one value along, you need to create a
multi-value in some fashion (an array, an object, concatinating strings, etc).
With chaining, we would need to manually build up the result ourself. We can create a function to return a promise object such as:

["getting_The_Data: 42","filter_The_Data","process_The_Data","display_The_Data"] 

Example:

function asyncFunction(): JPromise;
begin
Result := new JPromise(
  function (resolve: TCallback): variant
  begin
    w3_SetTimeout(procedure()
    begin
      var value := 42;
      resolve('getting_The_Data: ' + IntToStr(value) );
    end, 16);
  end,

  function(reject: TCallback): variant
  begin
   // case rejected
  end);
end;

var results: array of variant;

asyncFunction()
  .then(function (value: variant): array of variant
  begin
    results := [value];
    results.push('filter_The_Data');
    exit(results);
  end)

  .then(function (value: variant): array of variant
  begin
    results.push('process_The_Data');
    exit(results);
  end)

  .then(function (value: variant): array of variant
  begin
    results.push('display_The_Data');
    exit(results);
  end)

  .then(function (value: variant): variant
  begin
    console.log( JSON.stringify(variant(results)) );
   {* for var i := results.Low to results.High do
    console.log(results[i]); *}

   {*for var j in results do
     console.log(j);*}
  end)

  .catch(procedure (error: variant)
  begin
    console.log(error);
  end);

The Callback is Optional

The callback to then() is not strictly required. If you leave it off, the promise resolves to the same value as the
previous promise.
NOTE: If there is no callback in method then(), it simply resolves the promise and exits.  The value is still the value of the previous promise.

Example:

function asyncFunction(): JPromise;
begin
Result := new JPromise(
  function (resolve: TCallback): variant
  begin
    w3_SetTimeout(procedure()
    begin
      var value := 42;
      resolve('getting_The_Data: ' + IntToStr(value) );
    end, 16);
  end,

  function(reject: TCallback): variant
  begin
   // case rejected
  end);
end;

asyncFunction()
  .then()

  .then()

  .then()

  .then(function (value: variant): variant
  begin
    console.log('forth result: '+ value); // => 'forth result: getting_The_Data: 42'
  end)

  .catch(procedure (error: variant)
  begin
    console.log(error);
  end);

Returning JSON object Inside the Chain

What if one of the resolved values is a JSON? For example:

function doSomethingElse(value: variant): variant;
begin
  result := JSON.parse('{"name":"'+value+'", "id":123}');
end;

asyncFunction()
  .then(function (value: variant): variant
  begin
    console.log('first result: '+ value); // => 
    { doSomethingElse returns a promise }
    exit( doSomethingElse(value) );
  end)

  .then(function (finalResult: variant): variant
  begin
    console.log(finalResult); // => Object {name: "getting_The_Data: 42", id: 123}
  end)

  .catch(procedure (error: variant)
  begin
    console.log(error);
  end);

Rejecting Promises (error handling support)

When something goes wrong during the course of a promise, it needs to be rejected with a reason.
How does the caller know when this happens?
They can find out by passing in a second callback to then()
The promise will transition from pending to either resolved or rejected, never both.
In other words, only one of the above callbacks ever gets called.
Promises enable rejection by means of reject(), the evil twin of resolve().
Here is asyncFunction() with error handling support added


function asyncFunction(): JPromise;
begin
  result := JPromise.create(function(resolve: TCallback; reject: TCallback): variant
  begin
    var res := doSomethingElse();
    if(res.error = true) then
    begin
      reject(res.error);
    end else
      resolve(res.value);
    end);
end;

asyncFunction()
  .then(function (value: variant): variant
  begin
    console.log('Success! '+ value);
    //console.log(value.error);
  end,
  function(error: variant): variant
  begin
    console.log('Uh oh '+ error);
  end)

  .catch(procedure (error: variant)
  begin
    console.log(error);
  end);

When using promises, it’s very easy to omit the error callback.
But if you do, you’ll never get any indication something went wrong. At the very least, the final promise in your chain should have an error callback.

Unexpected Errors Should Also Lead to Rejection
So far our error handling only accounts for known errors. It’s possible an unhandled exception will happen,
completely ruining everything. It’s essential that the promise implementation catch these exceptions and reject
accordingly. It’s possible for a misunderstanding of promises to lead to completely swallowed errors! This trips people up a lot.

Example:

function asyncFunction(): JPromise;
begin
  result := JPromise.create(
  procedure(resolve: TCallback; reject: TCallback)
  begin
    var badJson := "<div>uh oh, this is not JSON at all!</div>";
    resolve(badJson);
  end);
end;

asyncFunction()
  .then(function (jsonData: variant): variant
  begin
    var obj := JSON.parse(jsonData);
    console.log(obj);
    exit(obj);
  end,
  function (error: variant): variant
  begin
    console.log("an error occured: "+ error);
  end);

What is going to happen here? Our callback inside then() is expecting some valid JSON.
So it naively tries to parse it, which leads to an exception. But we have an error callback, so we’re good, right?
Nope. That error callback will not be invoked! If you run this example, you will get no output at all. No errors, no nothing. Pure chilling silence.
Why is this? Since the unhandled exception took place in our callback to then(), it is being caught inside of handle().
This causes handle() to reject the promise that then() returned, not the promise we are already responding to, as that promise has already properly resolved.
Always remember, inside of then()'s callback, the promise you are responding to has already resolved.
The result of your callback will have no influence on this promise.

If you want to capture the above error, you need an error callback further downstream, for instance:

asyncFunction()
  .then(function (jsonData: variant): variant
  begin
    var obj := JSON.parse(jsonData);
    console.log(obj);
    exit(obj);
  end)

  .then(nil, function (error: variant): variant
  begin
    console.log("an error occured: "+ error);
  end)

  .catch(procedure (error: variant)
  begin
    console.log(error);
  end);

Now we will properly log the error. In my experience, a potentially better solution would be implement a catch method to capture errors.


asyncFunction()
  .then(function (jsonData: variant): variant
  begin
    var obj := JSON.parse(jsonData);
    console.log(obj);
  end)

  .catch(procedure (error: variant)
  begin
    console.log(error);
  end);

Loading Images on-the-fly

Now, let's create a more realistic example using promise in SMS.
Let's create a function that dynamically loads an image based on the image URI:

Remember that it is passed an anonymous function with two parameters:
a) resolve() method that you call at some point to set the state of the promise to fulfilled,
b) reject() method to set it to rejected instead.
A Promise object starts out with a state of pending, to indicate the asynchronous code it's monitoring has neither completed (fulfilled) or failed (rejected).

Example:

function getImage(url: variant): JPromise;
begin
Result := JPromise.create(
  procedure(resolve: TCallback; reject: TCallback)
  var img: JHTMLImageElement;
  begin
    img := JHTMLImageElement(Document.createElement('img'));
    JGlobalEventHandlers(img).onload := lambda
      resolve(url)
    end;
    JGlobalEventHandlers(img).onerror := lambda
      reject(url)
    end;
    img.src := url;
end);
end;

The getImage() function returns a Promise object that keeps track of the state of the image load.
Its promise object goes from the initial state of "pending" to either fulfilled or rejected eventually depending on the outcome of the image load.

Notice how we've passed the URI of the image to both the resolve() and reject() method of Promise;
this could be any data you wish to be processed further depending on the outcome of the task.
Ok, at this point Promises may just seem like a pointless exercise to set some object's state to indicate the status of a task. But as you'll soon see, with this mechanism comes the ability to easily and intuitively define what happens next once the task is completed.

The then() and catch() methods
Whenever you instantiate a Promise object, two methods:
a) then() method and
b) catch() method become available to decide what happens next after the conclusion of an asynchronous task.

Take a look at the below:
Here as soon as "test.png" has loaded, we specify that the image be shown inside the "OBJ3" DIV.
The original getImage() function returns a Promise object, so we can call then() on it to specify what happens when the request has been resolved. The URL of the image we passed into the resolve() function when we created getImage() becomes available as the parameter inside the then() function.

What happens if the image failed to load? The then() method can accept a 2nd function to deal with the rejected state of the Promise object:

Example:

getImage('test.png')
  .then(function (successurl: variant): variant
  begin
    document.getElementById('OBJ3').innerHTML := '<img src="' + successurl + '" />';
  end,
  function(errorurl: variant): variant
  begin
    console.log("error loading "+ errorurl);
  end);
In such a construct, if the image loads, the first function inside then() is run, if it fails, the 2nd instead.

Of course, we can also handle errors using the catch() method instead:
Example:

getImage('test.png')
  .then(function (successurl: variant): variant
  begin
    document.getElementById('OBJ3').innerHTML := '<img src="' + successurl + '" />';
  end)
  
  .catch(procedure (errorurl: variant)
  begin
    console.log("error loading "+ errorurl);
  end);  
  
  {*  Calling catch() is equivalent to calling then(undefined, function), 
so the above is the same as: *}
  
getImage('test.png')
  .then(function (successurl: variant): variant
  begin
    document.getElementById('OBJ3').innerHTML := '<img src="' + successurl + '" />';
  end)

  .then(nil, function (errorurl: variant): variant
  begin
    console.log('Error loading ' + errorurl)
  end);

Fetching and displaying images sequentially

Lets say we have an array of images we want to load and display sequentially.
That is to say, first load and show image1, and once that's complete, go on to image2, and so on.
We'll talk about chaining promises together further below to accomplish this, but one approach is just to use
recursion to go through the list of images, calling our getImage() function each time with a then() method that
shows the current image before calling getImage() again until all of the images have been processed.

Here is the code:
function getImage(url: variant): JPromise;
begin
Result := JPromise.create(
  procedure(resolve: TCallback; reject: TCallback)
  var img: JHTMLImageElement;
  begin
    img := JHTMLImageElement(document.createElement('img'));
    JGlobalEventHandlers(img).onload := lambda
      resolve(url)
    end;
    JGlobalEventHandlers(img).onerror := lambda
      reject(url)
    end;
    img.src := url;
end);
end;

var doggyplayground := document.getElementById('OBJ3');
var doggies : Array of variant = ['dog1.png', 'dog2.png', 'dog3.png', 'dog4.png', 'dog5.png'];

procedure displayimages(images: array of variant);
begin
    var targetimage := variant(images).shift(); // process doggies images one at a time
    if (targetimage) then begin // if not end of array

    getImage(targetimage)
        .then(function(url: variant): variant  // load image then...
        begin
        
          var dog := JHTMLImageElement(document.createElement('img'));
          dog.setAttribute('src', url);
          doggyplayground.appendChild(dog); // add image to DIV
          displayimages(images); // recursion- call displayimages() again to process next image/doggy
        end)

        .catch(function(url: variant): variant // handle an image not loading
        begin
          console.log('Error loading ' + url);
          displayimages(images); // recursion- call displayimages() again to process next image/doggy
        end);
    end;
end;

displayimages(doggies);

The displayimages() function takes an array of images and sequentially goes through each image, by calling
images.shift(). For each image, we first call getImage() to fetch the image, then the returned Promise object's then() method to specify what happens next, in this case, add the image to the OBJ3 DIV before calling displayimages() again.

In the case of an image failing to load, the catch() method handles those instances.
The recursion stops when the doggies array is empty, after Array.shift() has gone through all of its elements.
Using recursion with JavaScript Promises is one way to sequentially process a series of asynchronous tasks.
Another more versatile method is by learning the art of chaining promises.

Chaining Promises

We already know that the then() method can be invoked on a Promise instance to specify what happens after the completion of a task. However, we can in fact chain multiple then() methods together, in turn chaining multiple promises together, to specify what happens after each promise has been resolved, in sequence. Using our trusted getImage() function to illustrate, the following fetches one image before fetching another:

Example:

function getImage(url: variant): JPromise;
begin
Result := JPromise.create(
  procedure(resolve: TCallback; reject: TCallback)
  var img: JHTMLImageElement;
  begin
    img := JHTMLImageElement(Document.createElement('img'));
    JGlobalEventHandlers(img).onload := lambda
      resolve(url)
    end;
    JGlobalEventHandlers(img).onerror := lambda
      reject(url)
    end;
    img.src := url;
end);
end;

getImage('dog1.png')
  .then(function (successurl: variant): variant
  begin
    console.log(successurl+ ' fetched!');
    exit( getImage('dog2.png') );
  end)

  .then(function (url: variant): variant
  begin
    console.log(url+ ' fetched!');
  end)

  .catch(procedure (error: variant)
  begin
    console.log(error);
  end);

This fetches "dog2.png" and returns a Promise object. By returning a Promise object inside then(), the next then() waits for that promise to resolve before running, accepting as its parameter the data passed on by the new Promise object. This is the key to chaining multiple promises together- by returning another promise inside the then() method.

Note that we can also simply return a static value inside then(), which would simply be carried on and executed immediately by the next then() method as its parameter value.
With the above example we still want to account for an image not loading, so we'll include the catch() method as well:

Example:

getImage('baddog1.png')
  .then(function (successurl: variant): variant
  begin
    console.log(successurl+ ' fetched!');
  end)

  .catch(procedure (url: variant)
  begin
    console.log(url+ ' failed to load!');
  end)

  .then(function (url: variant): variant
  begin
    exit( getImage('dog2.png') );
  end)

  .then(function (url: variant): variant
  begin
    console.log(url+ ' fetched!');
  end)

  .catch(procedure (url: variant)
  begin
    console.log(url+ ' failed to load!');
  end);

{ OUTPUT
 baddog1.png failed to load!
 dog2.png fetched!
}

Recall that catch() is synonymous with then(undefined, functionref), so after catch() the next then() will still be executed. Notice the organization of the then() and catch() methods- we put the return of the next promise object (or link in the chain) inside its own then() method, after the outcome of the previous promise is completely accounted for via the then() and catch() method proceeding it.
If you wanted to load 3 images in succession, for example, we could just add another set of then() then() catch() to the above code.

Creating a sequence of Promises

Ok, so we know the basic idea of chaining promises together is to return another promise inside the then() method. But manually chaining promises together can quickly become unmanageable. For longer chains, what we need is a way to start with an empty Promise object and programmatically pile on the desired then() and catch() methods to form the final sequence of promises.

We can create a blank Promise object that's resolved to begin with with the line:

var resolvedPromise := JDeferred.resolve();

var doggies : Array of variant = ['dog1.png', 'dog2.png', 'dog3.png', 'dog4.png', 'dog5.png'];

variant(doggies).forEach(
  procedure(targetimage: variant)
  begin

    resolvedPromise
      .then(function(): variant
      begin
        exit( getImage(targetimage) );
      end)

      .then(function(url: variant): variant
      begin
        console.log(url + ' fetched!');
      end)

      .catch(function(err: variant): variant
      begin
        console.log(err + ' failed to load!');
      end);
  end);
  
{
Console log:
 dog1.png fetched
 dog2.png fetched!
 dog3.png fetched!
 dog4.png fetched!
 dog5.png fetched!
}

First we create a resolved Promise object called sequence, then go through each element inside the doggies[] array with forEach(), adding to sequence the required then() and catch() methods to handle each image after it's loaded. The result is a series of then() and catch() methods attached to sequence, creating the desired timeline of loading each image one at a time.

In case you're wondering, instead of using forEach() to cycle through the image array, you can also use a simple for loop instead, though the result may be more than you had bargained for:


var resolvedPromise := JDeferred.resolve();

var doggies : Array of variant = ['dog1.png', 'dog2.png', 'dog3.png', 'dog4.png', 'dog5.png'];

for var i:= 0 to doggies.length - 1 do
begin
  var closure := lambda
    var capturedindex := i;  // capture i at each step of loop

    resolvedPromise
      .then(function(): variant
      begin
        exit( getImage(doggies[capturedindex]) );
      end)

      .then(function(url: variant): variant
      begin
        console.log(url + ' fetched!');
      end)

      .catch(function(err: variant): variant
      begin
        console.log('Err loading '+ err);
      end);
  end;
  closure(); // invoke this function immediately
end;
{
Console log:
 dog1.png fetched
 dog2.png fetched!
 dog3.png fetched!
 dog4.png fetched!
 dog5.png fetched!
}

Inside the for loop, to properly get the value of i at each step and pass it into then(), we need to create an outer closure to capture each value of i. Without the outer closure, the value of i passed into then() each time will simply be the value of i when it's reached the end of the loop, or doggies.length - 1.

Creating an array of promises

Instead of chaining promises together, we can also create an array of promises. This makes it easy to do something after all of the asynchronous tasks have completed, instead of after each task.

For example, the following uses getImage() to fetch two images and store them as an array of promises:

var doggypromises : array of variant = [getImage('dog1.png'),
                                        getImage('dog2.png'),
                                        getImage('dog3.png'),
                                        getImage('dog4.png'),
                                        getImage('dog5.png')];

var resolvedPromise := JDeferred.all( doggypromises );

    resolvedPromise
      .then(function(urls: variant): variant
      begin
        console.log(urls); // logs ["dog1.png", "dog2.png", "dog3.png", "dog4.png", "dog5.png"]
      end)

      .catch(function(err: variant): variant
      begin
        console.log(err);
      end);

Promise.all() takes an iterable (array or array-like list) of promise objects, and waits until all of those promises have been fulfilled before moving on to any then() method attached to it. The then() method is passed an array of returned values from each promise.

So what happens if one of the promises inside the array doesn't resolve (is rejected)? In that case the entire then() portion is ignore, and catch() is executed instead. So in the above scenario, if one or more of the images fails to load, it only logs an array of images that failed to load inside catch().

Displaying images when they have all been fetched (Fetch and display images at all once):

It's high time now to see an example of showing off all the doggies when they have been fetched, instead of one at a time. We'll use Array.map() to ease the pain in creating a promise array:


var doggies : array of variant = ['dog1.png', 'dog2.png', 'dog3.png', 'dog4.png', 'dog5.png'];
var doggypromises := doggies.map(getImage); // call getImage on each array element and return array of promises
var doggyplayground := document.getElementById('OBJ3');

var resolvedPromise := JDeferred.all( doggypromises );

resolvedPromise
  .then(function(urls: variant): variant
  begin
    for var i:=0 to urls.length - 1 do
    begin
      var dog := document.createElement('img');
      dog.setAttribute('src', urls[i]);
      doggyplayground.appendChild(dog);
    end;
  end)

  .catch(function(err: variant): variant
  begin
    console.log(err);
  end);

Array.map() iterates through the original array and calls getImage() on each element, returning a new array using the return value of getImage() at each step, or a promise. The result is twofold- each image gets fetched, and in turn we get back an array of corresponding promises. Then, we put Promise.all() to work, passing in doggypromises to show all the images at once:

Fetch images all at once, but display then in sequence as each one becomes ready 
(fetch images all at once in parallel, but show them sequentially):

Finally, as if the dogs haven't been paraded enough, lets introduce them to the doggy park in an optimized manner, not one by one, not all at once, but the best of both words. We'll fetch all of the images at once to take advantage of parallel downloading in browsers, but show them in sequence as each one becomes available (fetched).

This minimizes the time the doggies show up while still showing them in orderly sequence.
To do this, we just have to do two things we already know- create an array of promises to fetch all images at once (in parallel), then create a sequence of promises to actually show each image one at a time:

var doggies : array of variant = ['dog1.png', 'dog2.png', 'dog3.png', 'dog4.png', 'dog5.png'];
var doggypromises := doggies.map(getImage); // call getImage on each array element and return array of promises
var doggyplayground := document.getElementById('OBJ3');

var resolvedPromise := JDeferred.resolve();

variant(doggypromises).forEach(
  procedure(curPromise: variant)
  begin

    resolvedPromise
      .then(function(): variant
      begin
        exit( curPromise );
      end)

      .then(function(url: variant): variant
      begin
        var dog := document.createElement('img');
        dog.setAttribute('src', url);
        doggyplayground.appendChild(dog);
      end)

      .catch(function(err: variant): variant
      begin
        console.log(err + ' failed to load!');
      end);
  end);

Note that to create our sequence of promises this time, we iterate through the array of promises generated by Array.map(), and not the images array directly. This allows us to create the chain of promises without having to call getImage() each time again, which was done when we decided to fetch all images at once using Array.map() already.

Here's an example I re-created recently using promises with SMS:

Nenhum comentário:

Postar um comentário