Jump to content
Sign in to follow this  
lynkfs

Promises revisited

Recommended Posts

Promises are a javascript construct enabling async processes to be written in a synchronous manner (sort of).

The status quo at this stage :
- Promises are part of javascript since Ecmascript 6 
- Promises are enabled in all major browsers
- Promises work in node.js too (require('promise'))
- Promises are not used in the rtl, or described in Smart documentation
- There have been some posts on the forum re implementing promises (the best one : )
- The ecma.promise unit in the rtl is based on an older, incompatible, version of the current spec 

The subject of this post is to try and make the ecma.promise unit usable again and provide some examples

A promise is a way to combine an asynchronous action, its possible outcomes and the actions associated with these outcomes in a single construct.
Reading through the API documents (see bottom of this post), the basic syntax to create a promise is :

var promise : variant := new JPromise(resolver)

where resolver is a function with as arguments a resolve and reject function, which resolves the promise or rejects it on error. The return value of the resolver itself is ignored. (so we might as well type it as a procedure rather than a function)
The above is covered by

type
  TPromiseCallback = function(Value: Variant): Variant;

  JPromise = class external 'Promise'
  public
    constructor Create(resolver: procedure(resolve,reject: TPromiseCallback));
  end;

The most important functions of the Promise object are the "then" and the "catch" functions. Basically these are used to process actions when the promise is either resolved or rejected.
So we need to add at least this to the type info

type
  JPromise = class external 'Promise'
  public
    ...
    function &then(onFulFilled: TPromiseCallback): Variant;
    function catch(onRejected:  TPromiseCallback): Variant;
  end;


Some examples
The most quoted example is an external httprequest for some data (1), wait for the request to come back and see if it was succesfull or not (2) and perform the necessary actions (3) or (4).

function example1(url:string):variant;
begin
  var q : variant := new JPromise(procedure(resolve,reject: TPromiseCallback)
    begin
      var FHttp := TW3HttpRequest.Create;
      FHttp.GET(url);                                                //1   do something asynchronous
      FHttp.ondataready := lambda resolve(FHttp.responseText); end;  //2   resolve (and pass 'responseText' on to the then function)
      FHttp.onerror     := lambda reject(FHttp.statusText); end;           reject  (and pass 'statusText'   on to the catch function)
    end)
    .then(function(x:variant):variant begin writeln(x); end)         //3   do something when succesful
    .catch(function(x:variant):variant begin writeln(x); end);       //4   do something when unsuccesfull
  result := q;
end;
Example1('res/textfile.txt');

Really not that different from a normal ajax call :

procedure example2(url:string);
begin
  procedure OnDataReady(x: variant); begin writeln(x) end;           //3
  procedure OnError(x: variant); begin writeln(x) end;               //4

  var FHttp := TW3HttpRequest.Create;
  FHttp.GET(url);                                                    //1
  FHttp.ondataready := lambda OnDataReady(FHttp.responseText) end;   //2
  FHttp.onerror     := lambda OnError(FHttp.statusText) end; 
end;
Example2('res/textfile.txt');

The differences being here that promises allow all code in the one construct.
The other main advantage is that a single promise can be accessed multiple times and at multiple code locations:
Suppose we have a promise which reads an accountname from a db, and the accountname is say displayed on a window title and a report. In both places this can be simply referred to as AccountName = AccountPromise.then(*function*) without having to check if data is actually available.

The promise api also provides a shortcut to create a promise in the resolved or rejected state immediately without going through its constructor :
Promise.resolve('resolved');
Promise.reject('rejected');

For this we need access to the global Promise object, and have to add the next line :

type
...
var Promise external 'Promise': variant;

A possible use is if we have a process consisting of multiple steps, and we want to use the chaining mechanism of the "then" functions of the promise object to tie these steps together :

  Promise.resolve('life')        //life                
   .then(function(x:variant):variant
    begin
      result := x + "'s";        //life's
    end)
   .then(function(x:variant):variant
    begin
      writeln(x + ' good');      //life's good
    end);

The two remaining api functions are
promise = Promise.all(array or set)
promise = Promise.race(array or set)

These functions are initially typed as

  JPromise = class external 'Promise'
  public
    ...
    function all(Iterable: variant): Variant;
    function race(Iterable: variant): Variant;
  end;

Both can be used when running asynchronous tasks in parallel.
Suppose there are say 3 tasks running concurrently which execute a query to Google, Bing and Yahoo
The "all" function returns a promise when all 3 promises have been resolved, and the "race" function as soon as one of the promises has been resolved

Promise.all(requests:array[0..2] of string)
  .then(function(outcome:variant):variant begin writeln('All ' + requests.length + ' query outcomes received'); end);  
  .catch(function(error:variant):variant begin writeln('Error occurred ' + error); end);

Adding it all up, the typed info as per below works perfectly fine for the type of examples listed above

type
  TPromiseCallback = function(Value: Variant): Variant;

  JPromise = class external 'Promise'
  public
    constructor Create(resolver: procedure(resolve,reject: TPromiseCallback));
    function &then(onFulFilled: TPromiseCallback): Variant;
    function catch(onRejected: TPromiseCallback): Variant;
    function all(Iterable: variant): Variant;
    function race(Iterable: variant): Variant;
  end;

  var Promise external 'Promise': variant;

To complicate matters a bit, and usually unnecessarily so :
The function header of the "then" function above is the most used format. "Then" is however actually a function with 2 arguments, both of which are optional. Which means that the possible formats of this function are

    function &then: Variant; overload;
    function &then(onFulFilled: TPromiseCallback): Variant; overload;
    function &then(onFulfilled, onRejected: TPromiseCallback): Variant; overload; 

Mozilla :
"If one or both arguments are omitted or are provided non-functions, then they will be missing the handler(s), but will not generate any errors. If the Promise that then is called on adopts a state (fulfillment or rejection) for which then has no handler, a new Promise is created with no additional handlers, simply adopting the final state of the original Promise on which then was called".
My thoughts exactly ()

By the way this also means that catch(onRejected) is the same as &then(undefined, onRejected); 

Another (usually unnessary) detail is that the static resolve function (Promise.resolve) comes in different flavours as well :
  Promise.resolve(promise);    Returns promise (only if promise.constructor == Promise)
  Promise.resolve(thenable);    Make a new promise from the thenable. A thenable is promise-like in as far as it has a `then() method.
  Promise.resolve(obj);            Make a promise that fulfills to obj. in this situation.
This however is handled by the reference to the global Promise object, and doesn't require any changes to the typed info 

Finally, Mozilla also lists a "finally" function, which is not described in other api documents

API documentation
http://definitelytyped.org/docs/es6-promises--es6-promises/classes/promise.html
https://tc39.github.io/ecma262/#sec-promise-constructor
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
https://promisesaplus.com/
https://developers.google.com/web/fundamentals/primers/promises#promise-api-reference
https://github.com/tildeio/rsvp.js

Note:
In principal there are 3 ways to extract and type the functions, procedures and variables from any API :
- find an API declaration file and convert this programmatically into object Pascal syntax
- find a reference to a global object representing the API methods (if available)
- read through the API specs 

The first option would involve finding a relevant API declaration file (f.i. this one) and convert this into pascal syntax. The results of using ts2pas (see posts) have been not very 'promising' 

The second option, when using promises in a browser environment, is to simply forget about typing, but have a reference to the global window object, and reference the promise object from there :

var window  external 'window':  variant;
...
var promise := window.Promise.resolve('something');
promise.then(function(x:variant):variant
  begin
    writeln(x);
  end);

The post above has used the last option (read the API docs)
Surprisingly, it is actually somewhat difficult to find the latest/most comprehensive/most agreed upon spec. The promise api has gone through various iterations, starting from an idea to external libraries (Q, when, rsvp.js) and finally becoming part of the javascript language in Ecmascript 6. Even then there have been some changes .

Please let me know if you know of relevant use cases for this API requiring other changes to the ECMA.Promise unit

 

Share this post


Link to post
Share on other sites

Full kudo's to @warleyalex, he was the first one to come up with a working promise unit (see link in post)

My posts are usually a personal discovery journey, so I started with the api docs and went from there. There are a couple of differences I've noticed, probably because of different source material or for other reasons. I've asked him. 

 

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
Sign in to follow this  

×