Tuesday 8 September 2020

Promise Inspection Part 2

In my previous post I said that probably there was an easier way than inheritance to implement synchronous inspection in Promises, so that's what this post is about.

This mechanism is used differently from the one shown last week. Rather than receiving an executor function and creating a Promise from it, we receive an existing Promise, and chain to her a new Promise that is expanded with the expected inspection methods (isFulfilled(), isRejected(), getValue(), getReason()). The code is pretty straight forward. The new Promise waits for the original Promise and when this one is completed or rejected it sets her inpection properties accordingly.

An interesting point is that indeed the new Promise does not hold the isFullfilled, value, reason... values (used by the corresponding isFullfilled(), getValue()... expansion methods) as data fields (that hence could be set from outside), but as variables that get trapped by the closures used for each of those expansion methods. This is an old trick used for creating private fields in JavaScript.

Donc, voilĂ  le code:

 

//returns a new Promise expanded with inspection methods
function enableSyncInspect(pr){
    //we trap these variables in the closure making them sort of private, and allow public access only through the inspection methods that we add to the new promise
    let isFulfilled = false;
    let value = null; //resolution result
    
    let isRejected = false;
    let reason = null; //rejection reason
    
    let isPending = true;

    //create a new promise that gets resolved-rejected by the original promise and gets expanded with inspection methods
    let prWrapper = pr.then(_value => {
        isPending = false;
        isFulfilled = true;
        value = _value;
        return _value;
    }, _reason => {
        isPending = false;
        isRejected = true;
        reason = _reason;
        return _reason;
    });

    prWrapper.isFulfilled = () => {
        return isFulfilled;
    }

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

    prWrapper.isRejected = () => {
        return isRejected;
    }

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

    prWrapper.isPending = () => {
        return isPending;
    }

    return prWrapper;
}


That we can use like this:

 

function formatAsync(msg){
    return new Promise((resFn, rejFn) => {
        console.log("starting format");
        setTimeout(() => {
            console.log("finishing format");
            resFn(`[[${msg}]]`);
        }, 2000);
    });
}

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

//async main
(async () => {
    let pr1 = formatAsync("Bonjour");
    let syncInspectPr = enableSyncInspect(pr1);

    console.log("isPending: " + syncInspectPr.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(syncInspectPr), 1000);

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

})();

//Output:
// starting format
// isPending: true
// Promise NOT resolved yet
// finishing format
// result value: [[Bonjour]]
// Promise resolved to: [[Bonjour]]


As usual I've uploaded it into a gist.

No comments:

Post a Comment