Tuesday 1 September 2020

Promise Inheritance and Synchronous Inspection

The other day, taking a look into the advanced Promises provided by bluebirdjs I though about how to implement a very small and basic part of the funcionality, the Synchronous Inspection, that basically stores the value resolved by a Promise and allows accessing to it synchronously (read the doc for a real explanation). There are other ways to implement it, but I decided to use inheritance, and has been interesting enough to post it here.

In a previous post I already made use of Promise inheritance, but I did not need to take into account the resolve and reject callbacks passed to the executor function received by the constructor. In this case I wanted to override those resolve-reject callbacks, so that I would add my additional logic (setting the isFulfilled and value or isRejected and reason values) and then invoke the originals (the parent ones let's say) so that they perform their magic logic of invoking the continuations (the "then"-"catch" handlers). These callbacks are not exposed as methods in the Promise class (one could think of having implemented them as protected methods in another language...), so in order to get hold of them to use them, we create an internal Promise with an executor function that just will give us a reference to those original callbacks, and then we call the original executor function passing to it resolve-reject callbacks that perform our extra logic and then invoke the originals. Our "then" overriden method invokes the internal Promise parent method, so that all the logic performed by the Promise class to set the continuations is run.

The explanation above is really confusing, so you better just check the code of my SyncInspectPromise class:

 


class SyncInspectPromise extends Promise{
    constructor(executorFn){
        //compiler forces me to do a super call
        super(() => {});

        this._isFulfilled = false;
        this._value = null; //resolution result
        
        this._isRejected = false;
        this._reason = null; //rejection reason
        
        this._isPending = true;
        
        //we need to be able to invoke the original resFn, rejFn functions after performing our additional logic
        let origResFn, origRejFn;
        this.internalPr = new Promise((resFn, rejFn) => {
            origResFn = resFn;
            origRejFn = rejFn;
        });

        let overriddenResFn = (value) => {
            this._isPending = false;
            this._isFulfilled = true;
            this._value = value;
            origResFn(value);
        };

        let overriddenRejFn = (reason) => {
            this._isPending = false;
            this._isRejected = true;
            this._reason = reason;
            origRejFn(reason);
        };

        executorFn(overriddenResFn, overriddenRejFn);
    }

    isFulfilled(){
        return this._isFulfilled;
    }

    getValue(){
        return this.isFulfilled() 
            ? this._value
            : (() => {throw new Error("Unfulfilled Promise");})(); //emulate "throw expressions"
    }


    isRejected(){
        return this._isRejected;
    }

    getReason(){
        return this.isRejected()
            ? this._reason
            : (() => {throw new Error("Unrejected Promise");})(); //emulate "throw expressions"
    }

    isPending(){
        return this._isPending;
    }

    then(fn){
        //we set the continuation to the internal Promise, so that invoking the original res function
        //will invoke the continuation
        return this.internalPr.then(fn);
    }

    catch(fn){
        //we set the continuation to the internal Promise, so that invoking the original rej function
        //will invoke the continuation
        return this.internalPr.catch(fn);
    }

    finally(fn){
        return this.internalPr.finally(fn);
    }
}


And we can use it like this:

 

function sleep(ms){
    let resolveFn;
    let pr = new Promise(res => resolveFn = res);
    setTimeout(() => resolveFn(), ms);
    return pr;
}

function printValueIfFulfilled(pr){
    if (pr.isFulfilled()){
        console.log("Promise resolved to: " + pr.getValue());
    }
    else{
        console.log("Promise NOT resolved yet");
    }
}

(async () => {
    let pr1 = new SyncInspectPromise(res => {
        console.log("starting query");
        setTimeout(() => {
            console.log("finishing query");
            res("hi");
        }, 3000);
    });
    console.log("isPending: " + pr1.isPending());

    //this fn runs in 1 seconds (while the async fn takes 3 seconds) so it won't be fulfilled at that point)
    setTimeout(() => printValueIfFulfilled(pr1), 1000);

    let result = await pr1;
    console.log("result value: " + result);
    
    printValueIfFulfilled(pr1);

})();

//output:
// starting query
// isPending: true
// Promise NOT resolved yet
// finishing query
// result value: hi
// Promise resolved to: hi

As usual I've uploaded it into a gist.

No comments:

Post a Comment