如何从一开始就有条件地重启承诺链?

时间:2017-07-15 22:19:58

标签: javascript node.js mongodb promise mongodb-query

我正在尝试实施一个简单的抽奖系统,在那里我进行GET /测试,返回一个随机用户,他(1)以前没有赢得抽奖,(2)在过去一小时内注册。

在Mongo中,可能存在与用户相关联的多个文档,因为用户可以注册多个主题。例如{id: 1, name: 'John', subject: 'math',...}{id: 1, name: 'John', subject: 'english',...}。如果约翰因为数学抽奖而被选中,那么他将没有资格参加随后的所有抽奖活动,因此他无法多次获胜。从本质上讲,抽奖获奖者的身份必须是独一无二的。

我的问题是:我如何做检查John先前是否获胜的逻辑?如果John已经赢了,我想从顶部重新启动承诺链并再次Math.random,直到选出一个独特的赢家。如果没有获胜者符合条件,那么我想返回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 个答案:

答案 0 :(得分:0)

简而言之,在这种情况下,您实际上并不需要这样做。但是有更长的解释。

如果您的MongoDB版本支持它,那么您可以在初始查询条件之后使用$sample聚合管道,以获得“随机”选择。

当然,在任何情况下,如果某人因为已经“赢了”而没有资格,那么只需将其标记为这样,直接在另一组表格结果中。但这里“排除”的一般情况是简单地修改查询以将“获胜者”排除在可能的结果之外。

但是,我实际上至少会在“现代”的意义上演示“打破一个循环”,即使你实际上并不需要在这里实际需要做什么,这实际上是修改了要排除的查询。

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()
  }
})()

当然,无论采用哪种方法,结果都是随机的,并且之前的“获胜者”在实际查询本身中被排除在选择之外。这里的“循环中断”仅用于保持输出结果,直到不再有可能的赢家为止。

关于“循环破坏”方法的说明

现代node.js环境中的一般建议是内置async/await/yield功能,现在包含在v8.x.x版本中默认打开。这些版本将于今年10月(写作时)达到长期支持(LTS)并按照我自己的个人“三个月规则”进行,然后任何新作品都应基于当时最新的作品。

此处的备用案例通过async.await作为单独的库依赖项呈现。或者使用“Bluebird”Promise.coroutine作为单独的库依赖项,后一种情况是您可以交替使用Promise.try,但如果您要包含一个库来获取该函数,那么您可能会以及使用实现更现代语法方法的其他函数。

所以“虽然”(不打算)演示“打破一个承诺/回调”循环,真正应该从这里拿走的主要事情是不同的查询过程,它实际上是在选择随机获胜者之前,试图在“循环”中实施的“排除”。

实际情况是数据确定最佳。但是整个例子至少表明了可以应用“选择”和“循环中断”的方式。