@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
14
14
limitations under the License.
15
15
*/
16
16
17
+ import AwaitLock from "await-lock" ;
17
18
import { UnstableValue } from "matrix-events-sdk" ;
18
19
19
20
import { MatrixClient } from "../client" ;
@@ -71,6 +72,16 @@ export enum PolicyScope {
71
72
* our data structures.
72
73
*/
73
74
export class IgnoredInvites {
75
+ // A lock around method `getOrCreateTargetRoom`.
76
+ // Used to ensure that only one async task of this class
77
+ // is creating a new target room and modifying the
78
+ // `target` property of account key `IGNORE_INVITES_POLICIES`.
79
+ private _getOrCreateTargetRoomLock = new AwaitLock ( ) ;
80
+
81
+ // A lock around method `withIgnoreInvitesPoliciesLock`.
82
+ // Used to ensure that only one async task of this class is
83
+ // modifying `IGNORE_INVITES_POLICIES` at any point in time.
84
+ private _withIgnoreInvitesPoliciesLock = new AwaitLock ( ) ;
74
85
constructor (
75
86
private readonly client : MatrixClient ,
76
87
) {
@@ -83,6 +94,12 @@ export class IgnoredInvites {
83
94
* @param entity The entity covered by this rule. Globs are supported.
84
95
* @param reason A human-readable reason for introducing this new rule.
85
96
* @return The event id for the new rule.
97
+ *
98
+ * # Safety
99
+ *
100
+ * This method will rewrite the `Policies` object in the user's account data.
101
+ * This rewrite is inherently racy and could overwrite or be overwritten by
102
+ * other concurrent rewrites of the same object.
86
103
*/
87
104
public async addRule ( scope : PolicyScope , entity : string , reason : string ) : Promise < string > {
88
105
const target = await this . getOrCreateTargetRoom ( ) ;
@@ -119,11 +136,10 @@ export class IgnoredInvites {
119
136
*/
120
137
public async addSource ( roomId : string ) : Promise < boolean > {
121
138
// We attempt to join the room *before* calling
122
- // `await this.getOrCreateSourceRooms ()` to decrease the duration
139
+ // `await this.getSourceRooms ()` to decrease the duration
123
140
// of the racy section.
124
141
await this . client . joinRoom ( roomId ) ;
125
- // Race starts.
126
- const sources = ( await this . getOrCreateSourceRooms ( ) )
142
+ const sources = this . getSourceRooms ( )
127
143
. map ( room => room . roomId ) ;
128
144
if ( sources . includes ( roomId ) ) {
129
145
return false ;
@@ -133,7 +149,6 @@ export class IgnoredInvites {
133
149
ignoreInvitesPolicies . sources = sources ;
134
150
} ) ;
135
151
136
- // Race ends.
137
152
return true ;
138
153
}
139
154
@@ -144,10 +159,10 @@ export class IgnoredInvites {
144
159
* @param roomId The room to which the user is invited.
145
160
* @returns A rule matching the entity, if any was found, `null` otherwise.
146
161
*/
147
- public async getRuleForInvite ( { sender, roomId } : {
162
+ public getRuleForInvite ( { sender, roomId } : {
148
163
sender : string ;
149
164
roomId : string ;
150
- } ) : Promise < Readonly < MatrixEvent | null > > {
165
+ } ) : Readonly < MatrixEvent | null > {
151
166
// In this implementation, we perform a very naive lookup:
152
167
// - search in each policy room;
153
168
// - turn each (potentially glob) rule entity into a regexp.
@@ -158,7 +173,7 @@ export class IgnoredInvites {
158
173
// - match several entities per go;
159
174
// - pre-compile each rule entity into a regexp;
160
175
// - pre-compile entire rooms into a single regexp.
161
- const policyRooms = await this . getOrCreateSourceRooms ( ) ;
176
+ const policyRooms = this . getSourceRooms ( ) ;
162
177
const senderServer = sender . split ( ":" ) [ 1 ] ;
163
178
const roomServer = roomId . split ( ":" ) [ 1 ] ;
164
179
for ( const room of policyRooms ) {
@@ -227,21 +242,30 @@ export class IgnoredInvites {
227
242
const room = this . client . getRoom ( target ) ;
228
243
if ( room ) {
229
244
return room ;
230
- } else {
231
- target = null ;
232
245
}
233
246
}
234
- // We need to create our own policy room for ignoring invites.
235
- target = ( await this . client . createRoom ( {
236
- name : "Individual Policy Room" ,
237
- preset : Preset . PrivateChat ,
238
- } ) ) . room_id ;
239
- await this . withIgnoreInvitesPolicies ( ignoreInvitesPolicies => {
240
- ignoreInvitesPolicies . target = target ;
241
- } ) ;
247
+ try {
248
+ // We need to create our own policy room for ignoring invites.
249
+ await this . _getOrCreateTargetRoomLock . acquireAsync ( ) ;
250
+ target = ( await this . client . createRoom ( {
251
+ name : "Individual Policy Room" ,
252
+ preset : Preset . PrivateChat ,
253
+ } ) ) . room_id ;
254
+ await this . withIgnoreInvitesPolicies ( ignoreInvitesPolicies => {
255
+ ignoreInvitesPolicies . target = target ;
256
+ if ( ! ( "sources" in ignoreInvitesPolicies ) ) {
257
+ // `[target]` is a reasonable default for `sources`.
258
+ ignoreInvitesPolicies . sources = [ target ] ;
259
+ }
260
+ } ) ;
242
261
243
- // Since we have just called `createRoom`, `getRoom` should not be `null`.
244
- return this . client . getRoom ( target ) ! ;
262
+ // Since we have just called `createRoom`, `getRoom` should not be `null`.
263
+ // Note that this is unavoidably racy, e.g. another client could have left
264
+ // the room during the call to `this.withIgnoreInvitesPolicies`.
265
+ return this . client . getRoom ( target ) ! ;
266
+ } finally {
267
+ this . _getOrCreateTargetRoomLock . release ( ) ;
268
+ }
245
269
}
246
270
247
271
/**
@@ -258,40 +282,21 @@ export class IgnoredInvites {
258
282
* This rewrite is inherently racy and could overwrite or be overwritten by
259
283
* other concurrent rewrites of the same object.
260
284
*/
261
- public async getOrCreateSourceRooms ( ) : Promise < Room [ ] > {
285
+ public getSourceRooms ( ) : Room [ ] {
262
286
const ignoreInvitesPolicies = this . getIgnoreInvitesPolicies ( ) ;
263
287
let sources = ignoreInvitesPolicies . sources ;
264
288
265
289
// Validate `sources`. If it is invalid, trash out the current `sources`
266
290
// and create a new list of sources from `target`.
267
- let hasChanges = false ;
268
291
if ( ! Array . isArray ( sources ) ) {
269
292
// `sources` could not be an array.
270
- hasChanges = true ;
271
293
sources = [ ] ;
272
294
}
273
- let sourceRooms : Room [ ] = sources
295
+ const sourceRooms : Room [ ] = sources
274
296
// `sources` could contain non-string / invalid room ids
275
297
. filter ( roomId => typeof roomId === "string" )
276
298
. map ( roomId => this . client . getRoom ( roomId ) )
277
299
. filter ( room => ! ! room ) ;
278
- if ( sourceRooms . length != sources . length ) {
279
- hasChanges = true ;
280
- }
281
- if ( sourceRooms . length == 0 ) {
282
- // `sources` could be empty (possibly because we've removed
283
- // invalid content)
284
- const target = await this . getOrCreateTargetRoom ( ) ;
285
- hasChanges = true ;
286
- sourceRooms = [ target ] ;
287
- }
288
- if ( hasChanges ) {
289
- // Reload `policies`/`ignoreInvitesPolicies` in case it has been changed
290
- // during or by our call to `this.getTargetRoom()`.
291
- await this . withIgnoreInvitesPolicies ( ignoreInvitesPolicies => {
292
- ignoreInvitesPolicies . sources = sources ;
293
- } ) ;
294
- }
295
300
return sourceRooms ;
296
301
}
297
302
@@ -313,10 +318,15 @@ export class IgnoredInvites {
313
318
* Modify in place the `IGNORE_INVITES_POLICIES` object from account data.
314
319
*/
315
320
private async withIgnoreInvitesPolicies ( cb : ( ignoreInvitesPolicies : { [ key : string ] : any } ) => void ) {
316
- const { policies, ignoreInvitesPolicies } = this . getPoliciesAndIgnoreInvitesPolicies ( ) ;
317
- cb ( ignoreInvitesPolicies ) ;
318
- policies [ IGNORE_INVITES_ACCOUNT_EVENT_KEY . name ] = ignoreInvitesPolicies ;
319
- await this . client . setAccountData ( POLICIES_ACCOUNT_EVENT_TYPE . name , policies ) ;
321
+ await this . _withIgnoreInvitesPoliciesLock . acquireAsync ( ) ;
322
+ try {
323
+ const { policies, ignoreInvitesPolicies } = this . getPoliciesAndIgnoreInvitesPolicies ( ) ;
324
+ cb ( ignoreInvitesPolicies ) ;
325
+ policies [ IGNORE_INVITES_ACCOUNT_EVENT_KEY . name ] = ignoreInvitesPolicies ;
326
+ await this . client . setAccountData ( POLICIES_ACCOUNT_EVENT_TYPE . name , policies ) ;
327
+ } finally {
328
+ this . _withIgnoreInvitesPoliciesLock . release ( ) ;
329
+ }
320
330
}
321
331
322
332
/**
0 commit comments