Skip to content Skip to sidebar Skip to footer

How Do I Conditionally Restart The Promise Chain From The Beginning?

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. I

Solution 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.

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

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

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

const oneHour = ( 1000 * 60 * 60 );

(asyncfunction() {

  let db;

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

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

    // Clean dataawait collection.remove({});

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

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

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

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

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

    // Loop with random lengthconsole.log("Math random selection");
    while (true) {
      let winners = await collection.find({
        "updated": {
          "$gte": newDate( newDate() - oneHour ),
          "$lt": newDate()
        },
        "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 stateawait collection.updateMany({},{ "$unset": { "isWinner": "" } });

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

    // Wrap in a promise to awaitawaitnewPromise((resolve,reject) => {
      var looping = true;
      whilst(
        () => looping,
        (callback) => {
          collection.find({
            "updated": {
              "$gte": newDate( newDate() - oneHour ),
              "$lt": newDate()
            },
            "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 stateawait collection.updateMany({},{ "$unset": { "isWinner": "" } });

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

    await BPromise.coroutine(function* () {
      while(true) {
        let winners = yield collection.find({
          "updated": {
            "$gte": newDate( newDate() - oneHour ),
            "$lt": newDate()
          },
          "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.

Post a Comment for "How Do I Conditionally Restart The Promise Chain From The Beginning?"