3

I want a program to run a chain of actions after certain user action is done. However, part of the chain will need to wait for the resolution of previous Promise OR the fact that user has done some action. Is it possible to make Promise work this way?

I am imagining the ideal program script is like:

var coreTrigger = Promise.any([normalAsyncRequest, userAction]);
coreTrigger.then(res=>{
  // the followup action
});

...

// somewhere far away, or in developer console
userAction.done(); // I want this can be one possible path to trigger the followup action
COY
  • 684
  • 3
  • 10
  • Some suggestion you can get from this tutorial on [js promise](https://www.yogihosting.com/javascript-promise/). – yogihosting Aug 01 '20 at 12:24
  • @yogihosting seems you are giving me an elementary introduction to Promise and that is not what I need...I am experienced to Promise (at least I am able to manually interpret source code written in Promises). I am querying whether I overlook some brilliant techniques that make Promise also listen to extra calls. – COY Aug 01 '20 at 12:29
  • 1
    You probably want [`Promise.race`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race) instead of [`Promise.any`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/any) for that, but yes it would work. You can do with `const userAction = { then(onFulfill) { this.done = onFulfill; }};` but that's a hack, I would recommend to construct a `new Promise` properly for the user having made the particular action, using the `resolve()` callback. – Bergi Aug 01 '20 at 12:54
  • if you want to call handling after both input promises are complete try `Promise.all` instead. also I believe async await syntax could help for more complex examples like `await promise1; await new Promise(res => button.addEventlistener('click', res)) ....`; in this case click will only be counted after `promise1` is resolved – Andrei Aug 01 '20 at 12:59
  • Thank you both Bergi and Andrei! Nevermind about any/race/all/allSettled, I know how to choose between these correctly. But wow @Bergi that's magic! Can you give a brief explanation what is happening with `userAction` above? I want to further stabilize the hack by understanding more deeply first. – COY Aug 01 '20 at 13:05
  • 1
    @COY It's a thenable that will get resolved to a promise, by passing two callbacks into the `.then()` method that it expects to be called. This only works if the `Promise.resolve(userAction)` is called *before* the `userAction.done()`. – Bergi Aug 01 '20 at 13:11
  • Does this answer your question? [Can an ES6 JavaScript promise be resolved by anything else if it is not resolved by the executor?](https://stackoverflow.com/questions/59559897/can-an-es6-javascript-promise-be-resolved-by-anything-else-if-it-is-not-resolved) – FZs Aug 06 '20 at 16:18
  • @FZs although the core topic is similar, my question is more focused on how to manually intercept the promise rather. In fact my question is already fully resolved with some surplus by the answer I posted... – COY Aug 06 '20 at 16:21
  • @COY OK, I've retracted my vote... – FZs Aug 06 '20 at 16:35

3 Answers3

6

Yes!

function createUserAction() {
    let resolve = undefined;
    const promise = new Promise(r => { resolve = r });

    function done() {
        resolve();
    }

    function wait() {
        return promise;
    }

    return { done, wait }
}

And use it as you've described in your question.

const userAction = createUserAction();
var coreTrigger = Promise.any([normalAsyncRequest, userAction.wait()]);
coreTrigger.then(res=>{
  // the followup action
});

// Somewhere else
userAction.done();
Arash Motamedi
  • 9,284
  • 5
  • 34
  • 43
  • 2
    Your answer tells me the brilliant idea I need exactly, that is, we can pass the "resolve" and "reject" to outside world via the resolver. Thank you! – COY Aug 02 '20 at 05:33
0
  • Example 1: allow external call to resolve the Promise.
  • Example 2: altering the promiseList after creating the comPromise.
  • Example 3 and 4: directly creating comPromise with a promiseList.
async function doAsync(syncMethod, timeout) {
    try {
        if (timeout != null) {
            setTimeout(()=>syncMethod(), timeout)
        } else syncMethod();
    } catch (e) {console.error(e);}
}

class comPromise extends Promise {
    constructor(resolver) {
        var outBox = undefined;
        super((resolve, reject)=>outBox={resolve:resolve, reject:reject});
        this.promiseList = [new Promise(resolver)];
        this.quantifier = 'race';
        this.needReplacement = false;
        this.state = 'pending';
        (async ()=>{
            try {
                while (true) {
                    this.replacer = triggerFactory();
                    var result = await Promise.race([
                        Promise[this.quantifier](this.promiseList),
                        this.replacer.promise
                    ]);
                    if (this.state == 'aborted') {
                        return;
                    } else if (!this.needReplacement) {
                        this.state = 'fulfilled';
                        return outBox.resolve(result);
                    } else {
                        this.needReplacement = false;
                    }
                }
            } catch(e) {return outBox.reject(e);}
        })();
    }
    refreshCondition() {
        this.needReplacement = true;
        this.replacer.fire();
    }
    testQuantifier() {
        console.log(this.quantifier);
        console.log(this.promiseList);
        return Promise[this.quantifier](this.promiseList);
    }
    forceOutcome(action) {
        if (this.state == 'fulfilled') {
            throw Error('Promise is already fulfilled!');
        } else {
            this.needReplacement = false;
            action();
            return this;
        }
    }
    abort() {return this.forceOutcome(()=>{this.state == 'aborted'; this.replacer.fire();});}
    forceResolve(result) {return this.forceOutcome(()=>this.replacer.fire(result));}
    forceReject(error) {return this.forceOutcome(()=>this.replacer.cancel(error));}
    redefine(promiseList, quantifier) {
        this.promiseList = promiseList;
        this.quantifier = quantifier;
        this.refreshCondition();
        return this;
    }
    static composite(promiseList, quantifier) {
        var cp = new comPromise(s=>'never resolve or reject');
        cp.quantifier = quantifier;
        cp.promiseList = promiseList;
        cp.refreshCondition();
        return cp;
    }
    static all(promiseList) {return this.composite(promiseList, 'all');}
    static allSettled(promiseList) {return this.composite(promiseList, 'allSettled');}
    static any(promiseList) {return this.composite(promiseList, 'any');}
    static race(promiseList) {return this.composite(promiseList, 'race');}
}

var trigs = {};
function triggerFactory(name) {
    console.debug('new trigger requested:', name);
    var resolveCall = undefined;
    var rejectCall = undefined;
    var trigger = {
        fired: false,
        fire(result) {
            if (this.fired) {throw Error('Trigger is already fired!');}
            this.fired = true;
            this.result = result;
            resolveCall(result);
        },
        cancel(e) {rejectCall(e);},
        promise: new Promise((s,f) => [resolveCall,rejectCall] = [s,f])
    };
    if (name != null) trigs[name] = trigger;
    return trigger;
}

// Example 1
var manualTrigger = Promise.any([new Promise(()=>'never resolve or reject'), triggerFactory('manual').promise]);
manualTrigger.then(res=>{
    console.log('Core trigger done!', res);
});
trigs['manual'].fire('manualTrigger resolved by manual trigger');

// Example 2
var cpNever = new comPromise(()=>'never resolve or reject');
cpNever.then(x=>console.log(x));
doAsync(()=>{
    cpNever.promiseList.push(new Promise(s=>s('cpNever resolved by this new promise')));
    cpNever.quantifier = 'any';
    cpNever.refreshCondition();
}, 1000);

// Example 3
var allAB = comPromise.all([triggerFactory('A').promise, triggerFactory('B').promise]);
allAB.then(x=>console.log('All A and B fulfilled',x)).catch(e=>console.error(e));
doAsync(()=>{
    trigs['A'].fire('result of A');
    trigs['B'].fire('result of B');
}, 2000);

// Example 4
var anyCD = comPromise.any([triggerFactory('C').promise, triggerFactory('D').promise]);
anyCD.then(x=>console.log('Any of C and D fulfilled',x)).catch(e=>console.error(e));
doAsync(()=>{
    trigs['C'].cancel('cancelling C');
    trigs['D'].fire('result of D');
}, 3000);

EDIT

  • finally, it works now!
  • thanks to the answer by Arash Motamedi, I have polished the trigger factory definition to make it looks more stable
  • after some study, further polished the class and added basic force actions
COY
  • 684
  • 3
  • 10
  • I would suggest you to not subclass `Promise`. This `comPromise` thing currently bears resemblance to the [`Promise` constructor antipattern](https://stackoverflow.com/q/23803743/1048572?What-is-the-promise-construction-antipattern-and-how-to-avoid-it) it should be a completely separate class (or even simple factory function) instead. – Bergi Aug 01 '20 at 17:07
  • "*TODO: understand what has caused the immediate call (without setTimeout) to fail*" - the problem is that `manualTrigger.then` is getting invoked asynchronously during the promise resolution procedure, so when you use it immediately the `fire` trigger is not yet installed. (But you'd never do that, right, because when the information is already available you wouldn't construct the compromise at all?) As I said, it's a hack. Use a proper `const manualTrigger = new Promise(resolve => setTimeout(() => resolve(111), 0));` – Bergi Aug 01 '20 at 17:10
  • @Bergi. Thanks again. Fortunately about the second comment, issues are resolved and the code now at least works for all "resolve" cases (yet to complete the rejection handling). About the first comment, my view is, though I understand the best way to construct promise is to simply make a call to an async function, it is sometimes unavoidable we have to go deep into the promise object. This comPromise extension is to make such "exceptional need" more straightforward to handle. – COY Aug 02 '20 at 08:14
0

It seems you can add the trigger inside your promise with an async/await function :

Here is an exemple with a click trigger :

async function displaySomething() {
  let myPromise = new Promise((resolve) => {
    let button = document.getElementById("button");
    button.addEventListener('click', (e) => {
      e.preventDefault();
      resolve("all is find !")
    })
  })
  const myString = await myPromise;
  console.log(myString)
}

displaySomething();