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

Commit 859fdf7

Browse files
authored
Cache localStorage objects for SettingsStore (#8366)
1 parent 65c74bd commit 859fdf7

File tree

3 files changed

+109
-35
lines changed

3 files changed

+109
-35
lines changed
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
Copyright 2019 - 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import SettingsHandler from "./SettingsHandler";
18+
19+
/**
20+
* Abstract settings handler wrapping around localStorage making getValue calls cheaper
21+
* by caching the values and listening for localStorage updates from other tabs.
22+
*/
23+
export default abstract class AbstractLocalStorageSettingsHandler extends SettingsHandler {
24+
private itemCache = new Map<string, any>();
25+
private objectCache = new Map<string, object>();
26+
27+
protected constructor() {
28+
super();
29+
30+
// Listen for storage changes from other tabs to bust the cache
31+
window.addEventListener("storage", (e: StorageEvent) => {
32+
if (e.key === null) {
33+
this.itemCache.clear();
34+
this.objectCache.clear();
35+
} else {
36+
this.itemCache.delete(e.key);
37+
this.objectCache.delete(e.key);
38+
}
39+
});
40+
}
41+
42+
protected getItem(key: string): any {
43+
if (!this.itemCache.has(key)) {
44+
const value = localStorage.getItem(key);
45+
this.itemCache.set(key, value);
46+
return value;
47+
}
48+
49+
return this.itemCache.get(key);
50+
}
51+
52+
protected getObject<T extends object>(key: string): T | null {
53+
if (!this.objectCache.has(key)) {
54+
try {
55+
const value = JSON.parse(localStorage.getItem(key));
56+
this.objectCache.set(key, value);
57+
return value;
58+
} catch (err) {
59+
console.error("Failed to parse localStorage object", err);
60+
return null;
61+
}
62+
}
63+
64+
return this.objectCache.get(key) as T;
65+
}
66+
67+
protected setItem(key: string, value: any): void {
68+
this.itemCache.set(key, value);
69+
localStorage.setItem(key, value);
70+
}
71+
72+
protected setObject(key: string, value: object): void {
73+
this.objectCache.set(key, value);
74+
localStorage.setItem(key, JSON.stringify(value));
75+
}
76+
77+
// handles both items and objects
78+
protected removeItem(key: string): void {
79+
localStorage.removeItem(key);
80+
this.itemCache.delete(key);
81+
this.objectCache.delete(key);
82+
}
83+
84+
public isSupported(): boolean {
85+
return localStorage !== undefined && localStorage !== null;
86+
}
87+
}

src/settings/handlers/DeviceSettingsHandler.ts

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
Copyright 2017 Travis Ralston
33
Copyright 2019 New Vector Ltd.
4-
Copyright 2019 The Matrix.org Foundation C.I.C.
4+
Copyright 2019 - 2022 The Matrix.org Foundation C.I.C.
55
66
Licensed under the Apache License, Version 2.0 (the "License");
77
you may not use this file except in compliance with the License.
@@ -16,17 +16,17 @@ See the License for the specific language governing permissions and
1616
limitations under the License.
1717
*/
1818

19-
import SettingsHandler from "./SettingsHandler";
2019
import { MatrixClientPeg } from "../../MatrixClientPeg";
2120
import { SettingLevel } from "../SettingLevel";
2221
import { CallbackFn, WatchManager } from "../WatchManager";
22+
import AbstractLocalStorageSettingsHandler from "./AbstractLocalStorageSettingsHandler";
2323

2424
/**
2525
* Gets and sets settings at the "device" level for the current device.
2626
* This handler does not make use of the roomId parameter. This handler
2727
* will special-case features to support legacy settings.
2828
*/
29-
export default class DeviceSettingsHandler extends SettingsHandler {
29+
export default class DeviceSettingsHandler extends AbstractLocalStorageSettingsHandler {
3030
/**
3131
* Creates a new device settings handler
3232
* @param {string[]} featureNames The names of known features.
@@ -43,15 +43,15 @@ export default class DeviceSettingsHandler extends SettingsHandler {
4343

4444
// Special case notifications
4545
if (settingName === "notificationsEnabled") {
46-
const value = localStorage.getItem("notifications_enabled");
46+
const value = this.getItem("notifications_enabled");
4747
if (typeof(value) === "string") return value === "true";
4848
return null; // wrong type or otherwise not set
4949
} else if (settingName === "notificationBodyEnabled") {
50-
const value = localStorage.getItem("notifications_body_enabled");
50+
const value = this.getItem("notifications_body_enabled");
5151
if (typeof(value) === "string") return value === "true";
5252
return null; // wrong type or otherwise not set
5353
} else if (settingName === "audioNotificationsEnabled") {
54-
const value = localStorage.getItem("audio_notifications_enabled");
54+
const value = this.getItem("audio_notifications_enabled");
5555
if (typeof(value) === "string") return value === "true";
5656
return null; // wrong type or otherwise not set
5757
}
@@ -68,15 +68,15 @@ export default class DeviceSettingsHandler extends SettingsHandler {
6868

6969
// Special case notifications
7070
if (settingName === "notificationsEnabled") {
71-
localStorage.setItem("notifications_enabled", newValue);
71+
this.setItem("notifications_enabled", newValue);
7272
this.watchers.notifyUpdate(settingName, null, SettingLevel.DEVICE, newValue);
7373
return Promise.resolve();
7474
} else if (settingName === "notificationBodyEnabled") {
75-
localStorage.setItem("notifications_body_enabled", newValue);
75+
this.setItem("notifications_body_enabled", newValue);
7676
this.watchers.notifyUpdate(settingName, null, SettingLevel.DEVICE, newValue);
7777
return Promise.resolve();
7878
} else if (settingName === "audioNotificationsEnabled") {
79-
localStorage.setItem("audio_notifications_enabled", newValue);
79+
this.setItem("audio_notifications_enabled", newValue);
8080
this.watchers.notifyUpdate(settingName, null, SettingLevel.DEVICE, newValue);
8181
return Promise.resolve();
8282
}
@@ -87,15 +87,15 @@ export default class DeviceSettingsHandler extends SettingsHandler {
8787

8888
delete settings["useIRCLayout"];
8989
settings["layout"] = newValue;
90-
localStorage.setItem("mx_local_settings", JSON.stringify(settings));
90+
this.setObject("mx_local_settings", settings);
9191

9292
this.watchers.notifyUpdate(settingName, null, SettingLevel.DEVICE, newValue);
9393
return Promise.resolve();
9494
}
9595

9696
const settings = this.getSettings() || {};
9797
settings[settingName] = newValue;
98-
localStorage.setItem("mx_local_settings", JSON.stringify(settings));
98+
this.setObject("mx_local_settings", settings);
9999
this.watchers.notifyUpdate(settingName, null, SettingLevel.DEVICE, newValue);
100100

101101
return Promise.resolve();
@@ -105,10 +105,6 @@ export default class DeviceSettingsHandler extends SettingsHandler {
105105
return true; // It's their device, so they should be able to
106106
}
107107

108-
public isSupported(): boolean {
109-
return localStorage !== undefined && localStorage !== null;
110-
}
111-
112108
public watchSetting(settingName: string, roomId: string, cb: CallbackFn) {
113109
this.watchers.watchSetting(settingName, roomId, cb);
114110
}
@@ -118,9 +114,7 @@ export default class DeviceSettingsHandler extends SettingsHandler {
118114
}
119115

120116
private getSettings(): any { // TODO: [TS] Type return
121-
const value = localStorage.getItem("mx_local_settings");
122-
if (!value) return null;
123-
return JSON.parse(value);
117+
return this.getObject("mx_local_settings");
124118
}
125119

126120
// Note: features intentionally don't use the same key as settings to avoid conflicts
@@ -132,15 +126,15 @@ export default class DeviceSettingsHandler extends SettingsHandler {
132126
return false;
133127
}
134128

135-
const value = localStorage.getItem("mx_labs_feature_" + featureName);
129+
const value = this.getItem("mx_labs_feature_" + featureName);
136130
if (value === "true") return true;
137131
if (value === "false") return false;
138132
// Try to read the next config level for the feature.
139133
return null;
140134
}
141135

142136
private writeFeature(featureName: string, enabled: boolean | null) {
143-
localStorage.setItem("mx_labs_feature_" + featureName, `${enabled}`);
137+
this.setItem("mx_labs_feature_" + featureName, `${enabled}`);
144138
this.watchers.notifyUpdate(featureName, null, SettingLevel.DEVICE, enabled);
145139
}
146140
}

src/settings/handlers/RoomDeviceSettingsHandler.ts

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*
22
Copyright 2017 Travis Ralston
3-
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
3+
Copyright 2019, 2020 - 2022 The Matrix.org Foundation C.I.C.
44
55
Licensed under the Apache License, Version 2.0 (the "License");
66
you may not use this file except in compliance with the License.
@@ -15,15 +15,15 @@ See the License for the specific language governing permissions and
1515
limitations under the License.
1616
*/
1717

18-
import SettingsHandler from "./SettingsHandler";
1918
import { SettingLevel } from "../SettingLevel";
2019
import { WatchManager } from "../WatchManager";
20+
import AbstractLocalStorageSettingsHandler from "./AbstractLocalStorageSettingsHandler";
2121

2222
/**
2323
* Gets and sets settings at the "room-device" level for the current device in a particular
2424
* room.
2525
*/
26-
export default class RoomDeviceSettingsHandler extends SettingsHandler {
26+
export default class RoomDeviceSettingsHandler extends AbstractLocalStorageSettingsHandler {
2727
constructor(public readonly watchers: WatchManager) {
2828
super();
2929
}
@@ -32,7 +32,7 @@ export default class RoomDeviceSettingsHandler extends SettingsHandler {
3232
// Special case blacklist setting to use legacy values
3333
if (settingName === "blacklistUnverifiedDevices") {
3434
const value = this.read("mx_local_settings");
35-
if (value && value['blacklistUnverifiedDevicesPerRoom']) {
35+
if (value?.['blacklistUnverifiedDevicesPerRoom']) {
3636
return value['blacklistUnverifiedDevicesPerRoom'][roomId];
3737
}
3838
}
@@ -49,16 +49,15 @@ export default class RoomDeviceSettingsHandler extends SettingsHandler {
4949
if (!value) value = {};
5050
if (!value["blacklistUnverifiedDevicesPerRoom"]) value["blacklistUnverifiedDevicesPerRoom"] = {};
5151
value["blacklistUnverifiedDevicesPerRoom"][roomId] = newValue;
52-
localStorage.setItem("mx_local_settings", JSON.stringify(value));
52+
this.setObject("mx_local_settings", value);
5353
this.watchers.notifyUpdate(settingName, roomId, SettingLevel.ROOM_DEVICE, newValue);
5454
return Promise.resolve();
5555
}
5656

5757
if (newValue === null) {
58-
localStorage.removeItem(this.getKey(settingName, roomId));
58+
this.removeItem(this.getKey(settingName, roomId));
5959
} else {
60-
newValue = JSON.stringify({ value: newValue });
61-
localStorage.setItem(this.getKey(settingName, roomId), newValue);
60+
this.setObject(this.getKey(settingName, roomId), { value: newValue });
6261
}
6362

6463
this.watchers.notifyUpdate(settingName, roomId, SettingLevel.ROOM_DEVICE, newValue);
@@ -69,14 +68,8 @@ export default class RoomDeviceSettingsHandler extends SettingsHandler {
6968
return true; // It's their device, so they should be able to
7069
}
7170

72-
public isSupported(): boolean {
73-
return localStorage !== undefined && localStorage !== null;
74-
}
75-
7671
private read(key: string): any {
77-
const rawValue = localStorage.getItem(key);
78-
if (!rawValue) return null;
79-
return JSON.parse(rawValue);
72+
return this.getItem(key);
8073
}
8174

8275
private getKey(settingName: string, roomId: string): string {

0 commit comments

Comments
 (0)