Retaining Data across multiple promise chains

I am learning promises, so I decided to play around with extending them. Everything is working except I am having a hard time figuring out how to make a value persist across all functions.

My goal is to find the count of the called functions from this promise, but each call creates a new one and I am having trouble finding a way to pass the value in. I tried adding a constructor which will have a value passed in, but it doesn't seem to work as I expect. I assume this is due to me misunderstanding the scope of "this".

to sum it up, each of my functions "init", "add" and "commit" should all up front add 1 to the "total number of steps" variables, which right now in this example is "i". With that I want to be able to say, I am on step 1 of 3, step 2 of 3 etc...

class Repo {
    constructor(setup) {
        this.s = {};
        this.s._progress = { "total":0, "count":0 };
        this.s._logging = { "enabled":true, "location":"console", "pretty":true, "verbose":false, "tabCount":0 };
        this.s._remoteInfo = { "name":"", "url":"" };
        this.s._localInfo = { "path":"" };

        this.s._logReset = () => {
            this.s._logging.tabCount = 0;
        };

        this.s._log = (message, tabCount) => {
            if(this.s._logging.enabled) {
                let tabs = '';

                if(this.s._logging.pretty) {
                    for(let i = 0; i < tabCount; i++) { tabs = tabs + '\t' };
                }

                if(this.s._logging.location == 'console') { console.log(tabs, message); }
                else {
                    //TODO: implement the file location to output
                }
            }
        };

        this.s._progressReset = () => {
            this.s._progress.total = 0;
            this.s._progress.count = 0;
        };

        this.s._addProgressTotal = () => {
            this.s._progress.total++;
            console.log(this.s._progress.total)
        }

        this.s._addProgress = () => {
            this.s._progress.count++;
            console.log('Progress is ' + this.s._progress.count + ' out of ' + this.s._progress.total)
        }
    }

    //Starts the promise chain and passes in the settings to be used.
    start() {
        this.s._logReset();
        this.s._progressReset();

        return new RepoPromise((resolve, reject) => {
            this.s._log('Start Log: <time>',0)
            resolve(this.s);
        });
    }
}

class RepoPromise extends Promise {
    constructor(executor, val) {
        let e = executor || function (res, rej) { res('')};
        super((resolve, reject) => {
            return e(resolve, reject);
        });

        this.i = val || 0;
    }

    end() {
        const returnValue = super.then((s) => {
            return new RepoPromise((resolve, reject) => {
                s._log('End Log: <time>',0)
                resolve(s);
            }, this.i);
        });
        return returnValue;
    }

    init() {
        //I know I need to add 1 to "i" here, but it won't work
        const returnValue = super.then((s) => {
            return new RepoPromise((resolve, reject) => {
                s._log('git init',1);
                s._addProgress();
                resolve(s, '')
            }, ++this.i);
        });
        return returnValue;
    };

    add() {
        //I know I need to add 1 to "i" here, but it won't work
        const returnValue = super.then((s) => {
            return new RepoPromise((resolve, reject) => {
                setTimeout(() => {
                    s._log('git add',1);
                    s._addProgress();
                    resolve(s,'');
                    //reject('Add Failed')
                }, Math.random() * (10000 - 1000) + 1000);
            },++this.i);
        });
        return returnValue;
    }

    commit() {
        //I know I need to add 1 to "i" here, but it won't work
        const returnValue = super.then((s) => {
            return new RepoPromise((resolve, reject) => {
                setTimeout(() => {
                    s._log('git commit -m "message"',1);
                    s._addProgress();
                    resolve(s, 'Finished');
                }, Math.random() * (5000 - 1000) + 1000);
            }, ++this.i);
        });
        return returnValue;
    }

    then(onFulfilled, onRejected) {
        const returnValue = super.then(onFulfilled, onRejected);
        return returnValue;
    }
}

usage:

var p = new Repo('')
.start()
    .init()
    .add()
    .commit()
.end()
.catch(
    x => {console.log('it broke: ' + x)}
);

Answers:

Answer

As you've pointed out, there isn't one promise in the chain, every then and catch returns a new promise. So don't try to retain the state in the RepoPromise, retain it in the object that you pass through the chain as the resolution value: s.

Re your second parameter to RepoPromise constructor: You can't reliably do that, because you aren't in control of every time that constructor is called. Remember, that constructor is called when you call then or catch. Which is another reason to pass the value around on s instead. :-) Just for completeness, here's an illustration of the fact that the constructor gets called within Promise:

class MyPromise extends Promise {
  constructor(...args) {
    super(...args);
    console.log("MyPromise constructor called");
  }
}

MyPromise.resolve()
  .then(val => val)
  .then(val => val)
  .then(val => val);


A couple of side notes:

  1. This:

    super((resolve, reject) => {
        return e(resolve, reject);
    });
    

    can be written simply as:

    super(e);
    
  2. This doesn't do anything and can just be removed:

    then(onFulfilled, onRejected) {
        const returnValue = super.then(onFulfilled, onRejected);
        return returnValue;
    }
    

I was a bit dense understanding the question, but now I get it: You want to increase s._progress.total for each call to init/add/commit, and increase s._progress.count when each then/catch callback is called.

Here's a simplified exmaple that just uses then and catch rather than adding init, add, and commit but you can easily apply the pattern to add those if you like.

The solution was to keep the status tracker (s) on the promise object, and insert ourselves into the various ways new promises are created (then, catch) so we copy the tracker from the old promise to the new one. We share the tracker across all of these promises, e.g., the tracker from the root promise tracks everything from there forward. See comments:

"use strict";

// For tracking our status
class Status {
    constructor(total = 0, count = 0) {
        this.id = ++Status.id;
        this.total = total;
        this.count = count;
    }
    addCall() {
        ++this.total;
        return this;
    }
    addProgress() {
        ++this.count;
        return this;
    }
    toString() {
        return `[S${this.id}]: Total: ${this.total}, Count: ${this.count}`;
    }
}
Status.id = 0;

// The promise subclass
class RepoPromise extends Promise {
    constructor(executor) {
        super(executor);
        this.s = new Status();
    }
    // Utility method to wrap `then`/`catch` callbacks so we hook into when they're called
    _wrapCallbacks(...callbacks) {
        return callbacks.filter(c => c).map(c => value => this._handleCallback(c, value));
    }
    // Utility method for when the callback should be called: We track that we've seen
    // the call then execute the callback
    _handleCallback(callback, value) {
        this.s.addProgress();
        console.log("Progress: " + this.s);
        return callback(value);
    }
    // Standard `then`, but overridden so we track what's going on, including copying
    // our status object to the new promise before returning it
    then(onResolved, onRejected) {
        this.s.addCall();
        console.log("Added: " + this.s);
        const newPromise = super.then(...this._wrapCallbacks(onResolved, onRejected));
        newPromise.s = this.s;
        return newPromise;
    }
    // Standard `catch`, doing the same things as `then`
    catch(onRejected) {
        this.s.addCall();
        console.log("Added: " + this.s);
        const newPromise = super.catch(...this._wrapCallbacks(onRejected));
        newPromise.s = this.s;
        return newPromise;
    }
}

// Create a promise we'll resolve after a random timeout
function delayedGratification() {
    return new Promise(resolve => {
        setTimeout(_ => {
            resolve();
        }, Math.random() * 1000);
    });
}

// Run! Note we follow both kinds of paths: Chain and diverge:
const rp = RepoPromise.resolve();
rp.then(delayedGratification)   // First chain
  .then(delayedGratification)
  .then(delayedGratification);
rp.catch(delayedGratification)  // Second chain
  .then(delayedGratification);
.as-console-wrapper {
  max-height: 100% !important;
}

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.