Inheritance Promise and Inspection Synchronously

While exploring the advanced Promise features provided by bluebirdjs, I considered how to implement a simplified version of the Synchronous Inspection functionality. This feature essentially stores a Promise’s resolved value and enables synchronous access to it (refer to the documentation for a detailed explanation). I opted for an inheritance-based approach, which I found intriguing enough to share.

I’ve previously used Promise inheritance in a previous post, but without needing to manipulate the resolve and reject callbacks within the constructor’s executor function. In this case, my goal was to override these callbacks to incorporate additional logic (setting properties like isFulfilled, value, isRejected, and reason) before invoking the original callbacks to trigger subsequent “then” or “catch” handlers.

Since these callbacks aren’t exposed as methods within the Promise class, I devised a workaround. I create an internal Promise with an executor function solely to gain access to the original resolve and reject callbacks. Using these, I call the original executor function with modified resolve-reject callbacks that execute my custom logic before invoking the original callbacks. My overridden “then” method then calls the internal Promise’s parent method, ensuring the execution of the Promise class’s logic for setting up continuations.

To clarify, let’s examine the code for my SyncInspectPromise class:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
 
 `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);
    }
}` 

Here’s how you can use the SyncInspectPromise class:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
 
 `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 always, I’ve uploaded this code to into a gist.

Licensed under CC BY-NC-SA 4.0
Last updated on Apr 02, 2023 20:54 +0100