How do I conditionally restart the promise chain from the beginning?

拟墨画扇 提交于 2020-01-15 11:43:07

问题


I am trying to implement a simple raffling system where I do a GET /test which returns a random user who (1) hasn't won the raffle previously and (2)registered in the past hour.

In Mongo, there may be multiple documents associated with a user since the user can sign up for multiple subjects. For example {id: 1, name: 'John', subject: 'math',...} and {id: 1, name: 'John', subject: 'english',...}. If John gets picked for the raffle for math, then he will be ineligible for all subsequent raffles, so he can't get win multiple times. Essentially, the id of the raffle winner must be unique.

My question is: how do I do the logic for checking if John has won previously? If John has won already, I want to re-start the promise chain from the top and do a Math.random again until a unique winner is picked. If no winner is eligible, then I want to return a res.status(500).

   app.get('/test', function(req, res, next) {
        var currentTimestamp = new Date()
        var oneHourAgo = new Date(currentTimestamp - oneHour)
        var query = { "timeStamp": { $lte: currentTimestamp, $gte: oneHourAgo }, "isWinner": false }
        var winna = {}
        var winnerSelected = false

        var collection = db.collection('entries');

        while (!winnerSelected) {  // how can I do this
            collection.find(query).toArray().then( result => {
                var winner = result[Math.floor(Math.random() * result.length)];
                var query2     = {"id" : winner.id};
                winna['id'] = winner.id
                winna['name'] = winner.name
                winna['subject'] = winner.subject
                winna['timeStamp'] = winner.timeStamp
                winna['isWinner'] = winner.isWinner
                winna['raffleTimestamp'] = winner.raffleTimestamp

                return collection.find(query2).toArray();
            }).then( result => {
                for (var i in result) { // a winner can enter the raffle for multiple subjects, but if he already won once, then I want to redraw by doing a rand function again 
                    if (i.isWinner) {    // until a winner who is eligible is found, or if none are eligible, res.status(500)
                        console.log("i already won")
                        break
                        // how do I make it go back to the beginning of the while loop and pick a new random winner?
                    }
                }

                console.log("unique winner")
                winnerSelected = true // break out of while loop
                var query3 = { id: winna.id, subject: winna.subject }
                var raffleTimestamp = new Date()
                var update = { $set: { isWinner: true, raffleTimestamp: raffleTimestamp } }
                winna['isWinner'] = true
                winna['raffleTimestamp'] = raffleTimestamp
                res.send(winna) // send the winner with the updated fields to clientside
                return collection.updateOne(query3, update); // update the isWinner and raffleTimestamp fields
            }).then( result => {
             res.status(200);
            // res.send(result);
            }).catch( err => {
                console.log(err);
                res.status(500);
            });
        }
    })

回答1:


In brief, you don't actually need to do that in this case. But there is a longer explanation.

If your MongoDB version supports it, then you could simply use the $sample aggregation pipeline after your initial query conditions in order to get the "random" selection.

Of course in any case, if someone is not eligible because they already "won" then simply mark them as such, either directly on in another set of tabulated results. But the general case of "exclusion" here is to simply modify the query to exclude the "winners" from possible results.

However, I will actually demonstrate "breaking a loop" at least in a "modern" sense even though you do not actually need that for what you actually need to do here, which is actually modify the query to exclude instead.

const MongoClient = require('mongodb').MongoClient,
      whilst = require('async').whilst,
      BPromise = require('bluebird');

const users = [
  'Bill',
  'Ted',
  'Fred',
  'Fleur',
  'Ginny',
  'Harry'
];

function log (data) {
  console.log(JSON.stringify(data,undefined,2))
}

const oneHour = ( 1000 * 60 * 60 );

(async function() {

  let db;

  try {
    db = await MongoClient.connect('mongodb://localhost/raffle');

    const collection = db.collection('users');

    // Clean data
    await collection.remove({});

    // Insert some data
    let inserted = await collection.insertMany(
      users.map( name =>
        Object.assign({ name },
          ( name !== 'Harry' )
            ? { updated: new Date() }
            : { updated: new Date( new Date() - (oneHour * 2) ) }
        )
      )
    );
    log(inserted);

    // Loop with aggregate $sample
    console.log("Aggregate $sample");

    while (true) {
      let winner = (await collection.aggregate([
        { "$match": {
          "updated": {
            "$gte": new Date( new Date() - oneHour ),
            "$lt": new Date()
          },
          "isWinner": { "$ne": true }
        }},
        { "$sample": { "size": 1 } }
      ]).toArray())[0];

      if ( winner !== undefined ) {
        log(winner);    // Picked winner
        await collection.update(
          { "_id": winner._id },
          { "$set": { "isWinner": true } }
        );
        continue;
      }
      break;
    }

    // Reset data state
    await collection.updateMany({},{ "$unset": { "isWinner": "" } });

    // Loop with random length
    console.log("Math random selection");
    while (true) {
      let winners = await collection.find({
        "updated": {
          "$gte": new Date( new Date() - oneHour ),
          "$lt": new Date()
        },
        "isWinner": { "$ne": true }
      }).toArray();

      if ( winners.length > 0 ) {
        let winner = winners[Math.floor(Math.random() * winners.length)];
        log(winner);
        await collection.update(
          { "_id": winner._id },
          { "$set": { "isWinner": true } }
        );
        continue;
      }
      break;
    }

    // Reset data state
    await collection.updateMany({},{ "$unset": { "isWinner": "" } });

    // Loop async.whilst
    console.log("async.whilst");

    // Wrap in a promise to await
    await new Promise((resolve,reject) => {
      var looping = true;
      whilst(
        () => looping,
        (callback) => {
          collection.find({
            "updated": {
              "$gte": new Date( new Date() - oneHour ),
              "$lt": new Date()
            },
            "isWinner": { "$ne": true }
          })
          .toArray()
          .then(winners => {
            if ( winners.length > 0 ) {
              let winner = winners[Math.floor(Math.random() * winners.length)];
              log(winner);
              return collection.update(
                { "_id": winner._id },
                { "$set": { "isWinner": true } }
              );
            } else {
              looping = false;
              return
            }
          })
          .then(() => callback())
          .catch(err => callback(err))
        },
        (err) => {
          if (err) reject(err);
          resolve();
        }
      );
    });

    // Reset data state
    await collection.updateMany({},{ "$unset": { "isWinner": "" } });

    // Or synatax for Bluebird coroutine where no async/await
    console.log("Bluebird coroutine");

    await BPromise.coroutine(function* () {
      while(true) {
        let winners = yield collection.find({
          "updated": {
            "$gte": new Date( new Date() - oneHour ),
            "$lt": new Date()
          },
          "isWinner": { "$ne": true }
        }).toArray();

        if ( winners.length > 0 ) {
          let winner = winners[Math.floor(Math.random() * winners.length)];
          log(winner);
          yield collection.update(
            { "_id": winner._id },
            { "$set": { "isWinner": true } }
          );
          continue;
        }
        break;
      }
    })();

  } catch(e) {
    console.error(e)
  } finally {
    db.close()
  }
})()

And of course with either approach the results are random each time and previous "winners" are excluded from selection in the actual query itself. The "loop break" here is merely used to keep outputting results until there can be no more possible winners.


A note on the "loop breaking" methods

The general recommendation in modern node.js environments would to be the built in async/await/yield features now included as turned on by default in v8.x.x releases. These versions will hit Long Term Support (LTS) in October this year ( as of writing ) and going by my own personal "three month rule", then any new works should be based on things that would be current at that point in time.

The alternate cases here are presented via async.await as a separate library dependency. Or otherwise as a separate library dependency using "Bluebird" Promise.coroutine, with the latter case being that you could alternately use Promise.try, but if you are going to include a library to get that function, then you might as well use the other function which implements the more modern syntax approach.

So "whilst" ( pun not intended ) demonstrating "breaking a promise/callback" loop, the main thing that really should be taken away from here is the different query process, which actually does the "exclusion" that was being attempted to be implemented in a "loop" until the random winner was selected.

The actual case is the data determines this best. But the whole example does at least show ways that "both" the selection and the "loop break" can be applied.



来源:https://stackoverflow.com/questions/45123212/how-do-i-conditionally-restart-the-promise-chain-from-the-beginning

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!