@@ -7,6 +7,22 @@ const { mirrorRoomChanges } = require('../postgres/dualWrite');
77const PASSWORD_SALT_ROUNDS = 10 ;
88const STALE_ROOM_LIFETIME_MS = 10 * 60 * 1000 ;
99
10+ const sameUser = ( left , right ) => left && right
11+ && String ( left . id ) === String ( right . id ) ;
12+
13+ const selectRoomAdmin = ( { usersInRoom, owner, admin } ) => {
14+ if ( usersInRoom . length === 0 ) {
15+ return null ;
16+ }
17+
18+ const activeOwner = usersInRoom . find ( ( user ) => sameUser ( user , owner ) ) ;
19+ if ( activeOwner ) {
20+ return activeOwner ;
21+ }
22+
23+ return usersInRoom . find ( ( user ) => sameUser ( user , admin ) ) || usersInRoom [ 0 ] ;
24+ } ;
25+
1026// const lengths = {
1127
1228// };
@@ -87,6 +103,12 @@ const Room = new mongoose.Schema({
87103 of : Boolean ,
88104 default : { } ,
89105 } ,
106+ // Incremented for every join or departure so a conditional departure cannot
107+ // overwrite a reconnect that raced it on another Socket.IO process.
108+ membershipRevision : {
109+ type : Number ,
110+ default : 0 ,
111+ } ,
90112 admin : { type : mongoose . Schema . Types . ObjectId , ref : 'User' } ,
91113 owner : { type : mongoose . Schema . Types . ObjectId , ref : 'User' } ,
92114 type : {
@@ -187,6 +209,7 @@ Room.methods.addUser = async function (user, spectating, updateAdmin) {
187209 }
188210
189211 this . inRoom . set ( user . id . toString ( ) , true ) ;
212+ this . membershipRevision = ( this . membershipRevision || 0 ) + 1 ;
190213
191214 if ( this . waitingForCount === 0 ) {
192215 this . waitingFor . set ( user . id . toString ( ) , true ) ;
@@ -210,6 +233,7 @@ Room.methods.addUser = async function (user, spectating, updateAdmin) {
210233Room . methods . dropUser = async function ( user , updateAdmin ) {
211234 this . inRoom . set ( user . id . toString ( ) , false ) ;
212235 this . waitingFor . set ( user . id . toString ( ) , false ) ;
236+ this . membershipRevision = ( this . membershipRevision || 0 ) + 1 ;
213237
214238 await this . updateAdminIfNeeded ( updateAdmin ) ;
215239
@@ -220,6 +244,70 @@ Room.methods.dropUser = async function (user, updateAdmin) {
220244 return this . save ( ) ;
221245} ;
222246
247+ Room . methods . dropUserAtomically = async function ( user ) {
248+ const userKey = user . id . toString ( ) ;
249+ if ( ! this . inRoom . get ( userKey ) ) {
250+ return null ;
251+ }
252+
253+ const usersInRoom = this . users . filter ( ( candidate ) => (
254+ candidate . id . toString ( ) !== userKey
255+ && this . inRoom . get ( candidate . id . toString ( ) )
256+ ) ) ;
257+ const nextAdmin = selectRoomAdmin ( {
258+ usersInRoom,
259+ owner : this . owner ,
260+ admin : this . admin ,
261+ } ) ;
262+ const adminChanged = ! sameUser ( this . admin , nextAdmin ) ;
263+ const membershipRevision = this . membershipRevision || 0 ;
264+ const condition = {
265+ _id : this . _id ,
266+ [ `inRoom.${ userKey } ` ] : true ,
267+ } ;
268+ if ( this . updatedAt ) {
269+ condition . updatedAt = this . updatedAt ;
270+ }
271+ if ( membershipRevision > 0 ) {
272+ condition . membershipRevision = membershipRevision ;
273+ } else {
274+ condition . $or = [
275+ { membershipRevision : 0 } ,
276+ { membershipRevision : { $exists : false } } ,
277+ ] ;
278+ }
279+
280+ const update = {
281+ $set : {
282+ [ `inRoom.${ userKey } ` ] : false ,
283+ [ `waitingFor.${ userKey } ` ] : false ,
284+ admin : nextAdmin ? nextAdmin . _id || nextAdmin : null ,
285+ } ,
286+ $inc : {
287+ membershipRevision : 1 ,
288+ } ,
289+ } ;
290+ if ( usersInRoom . length === 0 && this . type === 'normal' ) {
291+ update . $set . expireAt = new Date ( Date . now ( ) + STALE_ROOM_LIFETIME_MS ) ;
292+ }
293+
294+ const updatedRoom = await this . constructor . findOneAndUpdate ( condition , update , {
295+ new : true ,
296+ } ) . populate ( 'users' ) . populate ( 'admin' ) . populate ( 'owner' ) ;
297+ if ( ! updatedRoom ) {
298+ return null ;
299+ }
300+
301+ await mirrorRoomChanges ( updatedRoom , {
302+ attempts : [ ] ,
303+ participantUserIds : [ userKey ] ,
304+ syncAllParticipants : false ,
305+ syncRoomOwners : adminChanged ,
306+ } ) ;
307+
308+ return { room : updatedRoom , adminChanged } ;
309+ } ;
310+
223311Room . methods . banUser = async function ( userId ) {
224312 this . banned . set ( userId . toString ( ) , true ) ;
225313 return this . dropUser ( { id : userId } ) ;
@@ -314,22 +402,6 @@ Room.methods.edit = async function (options) {
314402 return this . save ( ) ;
315403} ;
316404
317- const sameUser = ( left , right ) => left && right
318- && String ( left . id ) === String ( right . id ) ;
319-
320- const selectRoomAdmin = ( { usersInRoom, owner, admin } ) => {
321- if ( usersInRoom . length === 0 ) {
322- return null ;
323- }
324-
325- const activeOwner = usersInRoom . find ( ( user ) => sameUser ( user , owner ) ) ;
326- if ( activeOwner ) {
327- return activeOwner ;
328- }
329-
330- return usersInRoom . find ( ( user ) => sameUser ( user , admin ) ) || usersInRoom [ 0 ] ;
331- } ;
332-
333405Room . methods . updateAdminIfNeeded = async function ( cb ) {
334406 const nextAdmin = selectRoomAdmin ( this ) ;
335407 if ( sameUser ( this . admin , nextAdmin ) || ( ! this . admin && ! nextAdmin ) ) {
0 commit comments