Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit afbe3d1

Browse files
authored
Fix account & room settings race condition (#7953)
1 parent b8f37a4 commit afbe3d1

6 files changed

+169
-122
lines changed

src/settings/handlers/AccountSettingsHandler.ts

Lines changed: 62 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ limitations under the License.
1717

1818
import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
1919
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
20+
import { defer } from "matrix-js-sdk/src/utils";
2021

21-
import { MatrixClientPeg } from '../../MatrixClientPeg';
2222
import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler";
2323
import { objectClone, objectKeyChanges } from "../../utils/objects";
2424
import { SettingLevel } from "../SettingLevel";
@@ -30,6 +30,7 @@ const BREADCRUMBS_EVENT_TYPES = [BREADCRUMBS_LEGACY_EVENT_TYPE, BREADCRUMBS_EVEN
3030
const RECENT_EMOJI_EVENT_TYPE = "io.element.recent_emoji";
3131
const INTEG_PROVISIONING_EVENT_TYPE = "im.vector.setting.integration_provisioning";
3232
const ANALYTICS_EVENT_TYPE = "im.vector.analytics";
33+
const DEFAULT_SETTINGS_EVENT_TYPE = "im.vector.web.settings";
3334

3435
/**
3536
* Gets and sets settings at the "account" level for the current user.
@@ -45,10 +46,7 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa
4546
}
4647

4748
public initMatrixClient(oldClient: MatrixClient, newClient: MatrixClient) {
48-
if (oldClient) {
49-
oldClient.removeListener(ClientEvent.AccountData, this.onAccountData);
50-
}
51-
49+
oldClient?.removeListener(ClientEvent.AccountData, this.onAccountData);
5250
newClient.on(ClientEvent.AccountData, this.onAccountData);
5351
}
5452

@@ -62,9 +60,9 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa
6260
}
6361

6462
this.watchers.notifyUpdate("urlPreviewsEnabled", null, SettingLevel.ACCOUNT, val);
65-
} else if (event.getType() === "im.vector.web.settings" || event.getType() === ANALYTICS_EVENT_TYPE) {
63+
} else if (event.getType() === DEFAULT_SETTINGS_EVENT_TYPE || event.getType() === ANALYTICS_EVENT_TYPE) {
6664
// Figure out what changed and fire those updates
67-
const prevContent = prevEvent ? prevEvent.getContent() : {};
65+
const prevContent = prevEvent?.getContent() ?? {};
6866
const changedSettings = objectKeyChanges<Record<string, any>>(prevContent, event.getContent());
6967
for (const settingName of changedSettings) {
7068
const val = event.getContent()[settingName];
@@ -136,72 +134,81 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa
136134
return preferredValue;
137135
}
138136

139-
public async setValue(settingName: string, roomId: string, newValue: any): Promise<void> {
140-
// Special case URL previews
141-
if (settingName === "urlPreviewsEnabled") {
142-
const content = this.getSettings("org.matrix.preview_urls") || {};
143-
content['disable'] = !newValue;
144-
await MatrixClientPeg.get().setAccountData("org.matrix.preview_urls", content);
145-
return;
137+
// helper function to set account data then await it being echoed back
138+
private async setAccountData(
139+
eventType: string,
140+
field: string,
141+
value: any,
142+
legacyEventType?: string,
143+
): Promise<void> {
144+
let content = this.getSettings(eventType);
145+
if (legacyEventType && !content?.[field]) {
146+
content = this.getSettings(legacyEventType);
146147
}
147148

148-
// Special case for breadcrumbs
149-
if (settingName === "breadcrumb_rooms") {
150-
// We read the value first just to make sure we preserve whatever random keys might be present.
151-
let content = this.getSettings(BREADCRUMBS_EVENT_TYPE);
152-
if (!content || !content['recent_rooms']) {
153-
content = this.getSettings(BREADCRUMBS_LEGACY_EVENT_TYPE);
154-
}
155-
if (!content) content = {}; // If we still don't have content, make some
156-
157-
content['recent_rooms'] = newValue;
158-
await MatrixClientPeg.get().setAccountData(BREADCRUMBS_EVENT_TYPE, content);
159-
return;
149+
if (!content) {
150+
content = {};
160151
}
161152

162-
// Special case recent emoji
163-
if (settingName === "recent_emoji") {
164-
const content = this.getSettings(RECENT_EMOJI_EVENT_TYPE) || {};
165-
content["recent_emoji"] = newValue;
166-
await MatrixClientPeg.get().setAccountData(RECENT_EMOJI_EVENT_TYPE, content);
167-
return;
168-
}
153+
content[field] = value;
169154

170-
// Special case integration manager provisioning
171-
if (settingName === "integrationProvisioning") {
172-
const content = this.getSettings(INTEG_PROVISIONING_EVENT_TYPE) || {};
173-
content['enabled'] = newValue;
174-
await MatrixClientPeg.get().setAccountData(INTEG_PROVISIONING_EVENT_TYPE, content);
175-
return;
176-
}
155+
await this.client.setAccountData(eventType, content);
177156

178-
// Special case analytics
179-
if (settingName === "pseudonymousAnalyticsOptIn") {
180-
const content = this.getSettings(ANALYTICS_EVENT_TYPE) || {};
181-
content[settingName] = newValue;
182-
await MatrixClientPeg.get().setAccountData(ANALYTICS_EVENT_TYPE, content);
183-
return;
184-
}
157+
const deferred = defer<void>();
158+
const handler = (event: MatrixEvent) => {
159+
if (event.getType() !== eventType || event.getContent()[field] !== value) return;
160+
this.client.off(ClientEvent.AccountData, handler);
161+
deferred.resolve();
162+
};
163+
this.client.on(ClientEvent.AccountData, handler);
185164

186-
const content = this.getSettings() || {};
187-
content[settingName] = newValue;
188-
await MatrixClientPeg.get().setAccountData("im.vector.web.settings", content);
165+
await deferred.promise;
166+
}
167+
168+
public setValue(settingName: string, roomId: string, newValue: any): Promise<void> {
169+
switch (settingName) {
170+
// Special case URL previews
171+
case "urlPreviewsEnabled":
172+
return this.setAccountData("org.matrix.preview_urls", "disable", !newValue);
173+
174+
// Special case for breadcrumbs
175+
case "breadcrumb_rooms":
176+
return this.setAccountData(
177+
BREADCRUMBS_EVENT_TYPE,
178+
"recent_rooms",
179+
newValue,
180+
BREADCRUMBS_LEGACY_EVENT_TYPE,
181+
);
182+
183+
// Special case recent emoji
184+
case "recent_emoji":
185+
return this.setAccountData(RECENT_EMOJI_EVENT_TYPE, "recent_emoji", newValue);
186+
187+
// Special case integration manager provisioning
188+
case "integrationProvisioning":
189+
return this.setAccountData(INTEG_PROVISIONING_EVENT_TYPE, "enabled", newValue);
190+
191+
// Special case analytics
192+
case "pseudonymousAnalyticsOptIn":
193+
return this.setAccountData(ANALYTICS_EVENT_TYPE, "pseudonymousAnalyticsOptIn", newValue);
194+
195+
default:
196+
return this.setAccountData(DEFAULT_SETTINGS_EVENT_TYPE, settingName, newValue);
197+
}
189198
}
190199

191200
public canSetValue(settingName: string, roomId: string): boolean {
192201
return true; // It's their account, so they should be able to
193202
}
194203

195204
public isSupported(): boolean {
196-
const cli = MatrixClientPeg.get();
197-
return cli !== undefined && cli !== null && !cli.isGuest();
205+
return this.client && !this.client.isGuest();
198206
}
199207

200208
private getSettings(eventType = "im.vector.web.settings"): any { // TODO: [TS] Types on return
201-
const cli = MatrixClientPeg.get();
202-
if (!cli) return null;
209+
if (!this.client) return null;
203210

204-
const event = cli.getAccountData(eventType);
211+
const event = this.client.getAccountData(eventType);
205212
if (!event || !event.getContent()) return null;
206213
return objectClone(event.getContent()); // clone to prevent mutation
207214
}

src/settings/handlers/LocalEchoWrapper.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,31 +40,37 @@ export default class LocalEchoWrapper extends SettingsHandler {
4040
}
4141

4242
public getValue(settingName: string, roomId: string): any {
43-
const cacheRoomId = roomId ? roomId : "UNDEFINED"; // avoid weird keys
43+
const cacheRoomId = roomId ?? "UNDEFINED"; // avoid weird keys
4444
const bySetting = this.cache[settingName];
45-
if (bySetting && bySetting.hasOwnProperty(cacheRoomId)) {
45+
if (bySetting?.hasOwnProperty(cacheRoomId)) {
4646
return bySetting[cacheRoomId];
4747
}
4848

4949
return this.handler.getValue(settingName, roomId);
5050
}
5151

52-
public setValue(settingName: string, roomId: string, newValue: any): Promise<void> {
52+
public async setValue(settingName: string, roomId: string, newValue: any): Promise<void> {
5353
if (!this.cache[settingName]) this.cache[settingName] = {};
5454
const bySetting = this.cache[settingName];
5555

56-
const cacheRoomId = roomId ? roomId : "UNDEFINED"; // avoid weird keys
56+
const cacheRoomId = roomId ?? "UNDEFINED"; // avoid weird keys
5757
bySetting[cacheRoomId] = newValue;
5858

5959
const currentValue = this.handler.getValue(settingName, roomId);
6060
const handlerPromise = this.handler.setValue(settingName, roomId, newValue);
6161
this.handler.watchers?.notifyUpdate(settingName, roomId, this.level, newValue);
62-
return Promise.resolve(handlerPromise).catch(() => {
62+
63+
try {
64+
await handlerPromise;
65+
} catch (e) {
6366
// notify of a rollback
6467
this.handler.watchers?.notifyUpdate(settingName, roomId, this.level, currentValue);
65-
}).finally(() => {
66-
delete bySetting[cacheRoomId];
67-
});
68+
} finally {
69+
// only expire the cache if our value hasn't been overwritten yet
70+
if (bySetting[cacheRoomId] === newValue) {
71+
delete bySetting[cacheRoomId];
72+
}
73+
}
6874
}
6975

7076
public canSetValue(settingName: string, roomId: string): boolean {

src/settings/handlers/MatrixClientBackedSettingsHandler.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ limitations under the License.
1515
*/
1616

1717
import { MatrixClient } from "matrix-js-sdk/src/client";
18-
import { logger } from "matrix-js-sdk/src/logger";
1918

2019
import SettingsHandler from "./SettingsHandler";
2120

@@ -49,7 +48,5 @@ export default abstract class MatrixClientBackedSettingsHandler extends Settings
4948
return MatrixClientBackedSettingsHandler._matrixClient;
5049
}
5150

52-
protected initMatrixClient(oldClient: MatrixClient, newClient: MatrixClient) {
53-
logger.warn("initMatrixClient not overridden");
54-
}
51+
protected abstract initMatrixClient(oldClient: MatrixClient, newClient: MatrixClient);
5552
}

src/settings/handlers/RoomAccountSettingsHandler.ts

Lines changed: 47 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,15 @@ limitations under the License.
1818
import { MatrixClient } from "matrix-js-sdk/src/client";
1919
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
2020
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
21+
import { defer } from "matrix-js-sdk/src/utils";
2122

22-
import { MatrixClientPeg } from '../../MatrixClientPeg';
2323
import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler";
2424
import { objectClone, objectKeyChanges } from "../../utils/objects";
2525
import { SettingLevel } from "../SettingLevel";
2626
import { WatchManager } from "../WatchManager";
2727

2828
const ALLOWED_WIDGETS_EVENT_TYPE = "im.vector.setting.allowed_widgets";
29+
const DEFAULT_SETTINGS_EVENT_TYPE = "im.vector.web.settings";
2930

3031
/**
3132
* Gets and sets settings at the "room-account" level for the current user.
@@ -55,7 +56,7 @@ export default class RoomAccountSettingsHandler extends MatrixClientBackedSettin
5556
}
5657

5758
this.watchers.notifyUpdate("urlPreviewsEnabled", roomId, SettingLevel.ROOM_ACCOUNT, val);
58-
} else if (event.getType() === "im.vector.web.settings") {
59+
} else if (event.getType() === DEFAULT_SETTINGS_EVENT_TYPE) {
5960
// Figure out what changed and fire those updates
6061
const prevContent = prevEvent ? prevEvent.getContent() : {};
6162
const changedSettings = objectKeyChanges<Record<string, any>>(prevContent, event.getContent());
@@ -87,43 +88,62 @@ export default class RoomAccountSettingsHandler extends MatrixClientBackedSettin
8788
return settings[settingName];
8889
}
8990

90-
public async setValue(settingName: string, roomId: string, newValue: any): Promise<void> {
91-
// Special case URL previews
92-
if (settingName === "urlPreviewsEnabled") {
93-
const content = this.getSettings(roomId, "org.matrix.room.preview_urls") || {};
94-
content['disable'] = !newValue;
95-
await MatrixClientPeg.get().setRoomAccountData(roomId, "org.matrix.room.preview_urls", content);
96-
return;
91+
// helper function to send room account data then await it being echoed back
92+
private async setRoomAccountData(
93+
roomId: string,
94+
eventType: string,
95+
field: string | null,
96+
value: any,
97+
): Promise<void> {
98+
let content: ReturnType<RoomAccountSettingsHandler["getSettings"]>;
99+
100+
if (field === null) {
101+
content = value;
102+
} else {
103+
const content = this.getSettings(roomId, eventType) || {};
104+
content[field] = value;
97105
}
98106

99-
// Special case allowed widgets
100-
if (settingName === "allowedWidgets") {
101-
await MatrixClientPeg.get().setRoomAccountData(roomId, ALLOWED_WIDGETS_EVENT_TYPE, newValue);
102-
return;
103-
}
107+
await this.client.setRoomAccountData(roomId, eventType, content);
108+
109+
const deferred = defer<void>();
110+
const handler = (event: MatrixEvent) => {
111+
if (event.getRoomId() !== roomId || event.getType() !== eventType) return;
112+
if (field !== null && event.getContent()[field] !== value) return;
113+
this.client.off(RoomEvent.AccountData, handler);
114+
deferred.resolve();
115+
};
116+
this.client.on(RoomEvent.AccountData, handler);
104117

105-
const content = this.getSettings(roomId) || {};
106-
content[settingName] = newValue;
107-
await MatrixClientPeg.get().setRoomAccountData(roomId, "im.vector.web.settings", content);
118+
await deferred.promise;
108119
}
109120

110-
public canSetValue(settingName: string, roomId: string): boolean {
111-
const room = MatrixClientPeg.get().getRoom(roomId);
121+
public setValue(settingName: string, roomId: string, newValue: any): Promise<void> {
122+
switch (settingName) {
123+
// Special case URL previews
124+
case "urlPreviewsEnabled":
125+
return this.setRoomAccountData(roomId, "org.matrix.room.preview_urls", "disable", !newValue);
126+
127+
// Special case allowed widgets
128+
case "allowedWidgets":
129+
return this.setRoomAccountData(roomId, ALLOWED_WIDGETS_EVENT_TYPE, null, newValue);
112130

131+
default:
132+
return this.setRoomAccountData(roomId, DEFAULT_SETTINGS_EVENT_TYPE, settingName, newValue);
133+
}
134+
}
135+
136+
public canSetValue(settingName: string, roomId: string): boolean {
113137
// If they have the room, they can set their own account data
114-
return room !== undefined && room !== null;
138+
return !!this.client.getRoom(roomId);
115139
}
116140

117141
public isSupported(): boolean {
118-
const cli = MatrixClientPeg.get();
119-
return cli !== undefined && cli !== null && !cli.isGuest();
142+
return this.client && !this.client.isGuest();
120143
}
121144

122-
private getSettings(roomId: string, eventType = "im.vector.web.settings"): any { // TODO: [TS] Type return
123-
const room = MatrixClientPeg.get().getRoom(roomId);
124-
if (!room) return null;
125-
126-
const event = room.getAccountData(eventType);
145+
private getSettings(roomId: string, eventType = DEFAULT_SETTINGS_EVENT_TYPE): any { // TODO: [TS] Type return
146+
const event = this.client.getRoom(roomId)?.getAccountData(eventType);
127147
if (!event || !event.getContent()) return null;
128148
return objectClone(event.getContent()); // clone to prevent mutation
129149
}

0 commit comments

Comments
 (0)