mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-16 22:27:26 +01:00
Handle simultaneous quest accept/reject (#12090)
* Implement atomic quest accept/reject * Persist quest.members early to avoid simultaneous handling of accept/reject * Fix quest accept test (missing expectation) * PR fixes
This commit is contained in:
@@ -112,7 +112,7 @@ describe('POST /groups/:groupId/quests/accept', () => {
|
||||
|
||||
await Promise.all([partyMembers[0].sync(), questingGroup.sync()]);
|
||||
expect(leader.party.quest.RSVPNeeded).to.equal(false);
|
||||
expect(questingGroup.quest.members[partyMembers[0]._id]);
|
||||
expect(questingGroup.quest.members[partyMembers[0]._id]).to.equal(true);
|
||||
});
|
||||
|
||||
it('does not begin the quest if pending invitations remain', async () => {
|
||||
|
||||
@@ -193,12 +193,14 @@ api.acceptQuest = {
|
||||
if (group.quest.active) throw new NotAuthorized(res.t('questAlreadyStartedFriendly'));
|
||||
if (group.quest.members[user._id]) throw new BadRequest(res.t('questAlreadyAccepted'));
|
||||
|
||||
const acceptedSuccessfully = await group.handleQuestInvitation(user, true);
|
||||
if (!acceptedSuccessfully) {
|
||||
throw new NotAuthorized(res.t('questAlreadyAccepted'));
|
||||
}
|
||||
|
||||
user.party.quest.RSVPNeeded = false;
|
||||
await user.save();
|
||||
|
||||
group.markModified('quest');
|
||||
group.quest.members[user._id] = true;
|
||||
|
||||
if (canStartQuestAutomatically(group)) {
|
||||
await group.startQuest(user);
|
||||
}
|
||||
@@ -252,13 +254,15 @@ api.rejectQuest = {
|
||||
if (group.quest.members[user._id]) throw new BadRequest(res.t('questAlreadyAccepted'));
|
||||
if (group.quest.members[user._id] === false) throw new BadRequest(res.t('questAlreadyRejected'));
|
||||
|
||||
const rejectedSuccessfully = await group.handleQuestInvitation(user, false);
|
||||
if (!rejectedSuccessfully) {
|
||||
throw new NotAuthorized(res.t('questAlreadyRejected'));
|
||||
}
|
||||
|
||||
user.party.quest = Group.cleanQuestUser(user.party.quest.progress);
|
||||
user.markModified('party.quest');
|
||||
await user.save();
|
||||
|
||||
group.quest.members[user._id] = false;
|
||||
group.markModified('quest.members');
|
||||
|
||||
if (canStartQuestAutomatically(group)) {
|
||||
await group.startQuest(user);
|
||||
}
|
||||
|
||||
@@ -651,6 +651,30 @@ schema.methods.sendChat = function sendChat (options = {}) {
|
||||
return newChatMessage;
|
||||
};
|
||||
|
||||
schema.methods.handleQuestInvitation = async function handleQuestInvitation (user, accept) {
|
||||
if (!user) throw new InternalServerError('Must provide user to handle quest invitation');
|
||||
if (accept !== true && accept !== false) throw new InternalServerError('Must provide accept param handle quest invitation');
|
||||
|
||||
// Handle quest invitation atomically (update only current member when still undecided)
|
||||
// to prevent multiple concurrent requests overriding updates
|
||||
// see https://github.com/HabitRPG/habitica/issues/11398
|
||||
const Group = this.constructor;
|
||||
const result = await Group.update(
|
||||
{
|
||||
_id: this._id,
|
||||
[`quest.members.${user._id}`]: { $type: 10 }, // match BSON Type Null (type number 10)
|
||||
},
|
||||
{ $set: { [`quest.members.${user._id}`]: accept } },
|
||||
).exec();
|
||||
|
||||
if (result.nModified) {
|
||||
// update also current instance so future operations will work correctly
|
||||
this.quest.members[user._id] = accept;
|
||||
}
|
||||
|
||||
return Boolean(result.nModified);
|
||||
};
|
||||
|
||||
schema.methods.startQuest = async function startQuest (user) {
|
||||
// not using i18n strings because these errors are meant
|
||||
// for devs who forgot to pass some parameters
|
||||
@@ -680,6 +704,11 @@ schema.methods.startQuest = async function startQuest (user) {
|
||||
|
||||
// Changes quest.members to only include participating members
|
||||
this.quest.members = _.pickBy(this.quest.members, _.identity);
|
||||
|
||||
// Persist quest.members early to avoid simultaneous handling of accept/reject
|
||||
// while processing the rest of this script
|
||||
await this.update({ $set: { 'quest.members': this.quest.members } }).exec();
|
||||
|
||||
const nonUserQuestMembers = _.keys(this.quest.members);
|
||||
removeFromArray(nonUserQuestMembers, user._id);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user