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

Commit 09cc554

Browse files
committed
Invent an AsyncStore and use it for room lists
This is to get around the problem of a slow dispatch loop. Instead of slowing the whole app down to deal with room lists, we'll just raise events to say we're ready. Based upon the EventEmitter class.
1 parent eb36878 commit 09cc554

File tree

7 files changed

+144
-35
lines changed

7 files changed

+144
-35
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
},
5656
"dependencies": {
5757
"@babel/runtime": "^7.8.3",
58+
"await-lock": "^2.0.1",
5859
"blueimp-canvas-to-blob": "^3.5.0",
5960
"browser-encrypt-attachment": "^0.3.0",
6061
"browser-request": "^0.3.3",

src/components/views/rooms/RoomList2.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { _t } from "../../../languageHandler";
2121
import { Layout } from '../../../resizer/distributors/roomsublist2';
2222
import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
2323
import { ResizeNotifier } from "../../../utils/ResizeNotifier";
24-
import RoomListStore from "../../../stores/room-list/RoomListStore2";
24+
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore2";
2525

2626
interface IProps {
2727
onKeyDown: (ev: React.KeyboardEvent) => void;
@@ -56,8 +56,8 @@ export default class RoomList2 extends React.Component<IProps, IState> {
5656
}
5757

5858
public componentDidMount(): void {
59-
RoomListStore.instance.addListener(() => {
60-
console.log(RoomListStore.instance.orderedLists);
59+
RoomListStore.instance.on(LISTS_UPDATE_EVENT, (store) => {
60+
console.log("new lists", store.orderedLists);
6161
});
6262
}
6363

src/stores/AsyncStore.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
Copyright 2020 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 { EventEmitter } from 'events';
18+
import AwaitLock from 'await-lock';
19+
import { ActionPayload } from "../dispatcher-types";
20+
import { Dispatcher } from "flux";
21+
22+
/**
23+
* The event/channel to listen for in an AsyncStore.
24+
*/
25+
export const UPDATE_EVENT = "update";
26+
27+
/**
28+
* Represents a minimal store which works similar to Flux stores. Instead
29+
* of everything needing to happen in a dispatch cycle, everything can
30+
* happen async to that cycle.
31+
*
32+
* The store's core principle is Object.assign(), therefore it is recommended
33+
* to break out your state to be as safe as possible. The state mutations are
34+
* also locked, preventing concurrent writes.
35+
*
36+
* All updates to the store happen on the UPDATE_EVENT event channel with the
37+
* one argument being the instance of the store.
38+
*
39+
* To update the state, use updateState() and preferably await the result to
40+
* help prevent lock conflicts.
41+
*/
42+
export abstract class AsyncStore<T extends Object> extends EventEmitter {
43+
private storeState: T = <T>{};
44+
private lock = new AwaitLock();
45+
private readonly dispatcherRef: string;
46+
47+
/**
48+
* Creates a new AsyncStore using the given dispatcher.
49+
* @param {Dispatcher<ActionPayload>} dispatcher The dispatcher to rely upon.
50+
*/
51+
protected constructor(private dispatcher: Dispatcher<ActionPayload>) {
52+
super();
53+
54+
this.dispatcherRef = dispatcher.register(this.onDispatch.bind(this));
55+
}
56+
57+
/**
58+
* The current state of the store. Cannot be mutated.
59+
*/
60+
protected get state(): T {
61+
return Object.freeze(this.storeState);
62+
}
63+
64+
/**
65+
* Stops the store's listening functions, such as the listener to the dispatcher.
66+
*/
67+
protected stop() {
68+
if (this.dispatcherRef) this.dispatcher.unregister(this.dispatcherRef);
69+
}
70+
71+
/**
72+
* Updates the state of the store.
73+
* @param {T|*} newState The state to update in the store using Object.assign()
74+
*/
75+
protected async updateState(newState: T | Object) {
76+
await this.lock.acquireAsync();
77+
try {
78+
this.storeState = Object.assign(<T>{}, this.storeState, newState);
79+
this.emit(UPDATE_EVENT, this);
80+
} finally {
81+
await this.lock.release();
82+
}
83+
}
84+
85+
/**
86+
* Resets the store's to the provided state or an empty object.
87+
* @param {T|*} newState The new state of the store.
88+
* @param {boolean} quiet If true, the function will not raise an UPDATE_EVENT.
89+
*/
90+
protected async reset(newState: T | Object = null, quiet = false) {
91+
await this.lock.acquireAsync();
92+
try {
93+
this.storeState = <T>(newState || {});
94+
if (!quiet) this.emit(UPDATE_EVENT, this);
95+
} finally {
96+
await this.lock.release();
97+
}
98+
}
99+
100+
/**
101+
* Called when the dispatcher broadcasts a dispatch event.
102+
* @param {ActionPayload} payload The event being dispatched.
103+
*/
104+
protected abstract onDispatch(payload: ActionPayload);
105+
}

src/stores/room-list/RoomListStore2.ts

Lines changed: 26 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,14 @@ See the License for the specific language governing permissions and
1515
limitations under the License.
1616
*/
1717

18-
import {Store} from 'flux/utils';
19-
import {Room} from "matrix-js-sdk/src/models/room";
20-
import {MatrixClient} from "matrix-js-sdk/src/client";
18+
import { MatrixClient } from "matrix-js-sdk/src/client";
2119
import { ActionPayload, defaultDispatcher } from "../../dispatcher-types";
2220
import SettingsStore from "../../settings/SettingsStore";
23-
import { OrderedDefaultTagIDs, DefaultTagID, TagID } from "./models";
21+
import { DefaultTagID, OrderedDefaultTagIDs, TagID } from "./models";
2422
import { IAlgorithm, ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/IAlgorithm";
2523
import TagOrderStore from "../TagOrderStore";
2624
import { getAlgorithmInstance } from "./algorithms";
25+
import { AsyncStore } from "../AsyncStore";
2726

2827
interface IState {
2928
tagsEnabled?: boolean;
@@ -32,8 +31,13 @@ interface IState {
3231
preferredAlgorithm?: ListAlgorithm;
3332
}
3433

35-
class _RoomListStore extends Store<ActionPayload> {
36-
private state: IState = {};
34+
/**
35+
* The event/channel which is called when the room lists have been changed. Raised
36+
* with one argument: the instance of the store.
37+
*/
38+
export const LISTS_UPDATE_EVENT = "lists_update";
39+
40+
class _RoomListStore extends AsyncStore<ActionPayload> {
3741
private matrixClient: MatrixClient;
3842
private initialListsGenerated = false;
3943
private enabled = false;
@@ -49,7 +53,6 @@ class _RoomListStore extends Store<ActionPayload> {
4953
super(defaultDispatcher);
5054

5155
this.checkEnabled();
52-
this.reset();
5356
for (const settingName of this.watchedSettings) SettingsStore.monitorSetting(settingName, null);
5457
}
5558

@@ -66,41 +69,35 @@ class _RoomListStore extends Store<ActionPayload> {
6669
}
6770
}
6871

69-
private reset(): void {
70-
// We don't call setState() because it'll cause changes to emitted which could
71-
// crash the app during logout/signin/etc.
72-
this.state = {};
73-
}
74-
75-
private readAndCacheSettingsFromStore() {
72+
private async readAndCacheSettingsFromStore() {
7673
const tagsEnabled = SettingsStore.isFeatureEnabled("feature_custom_tags");
7774
const orderByImportance = SettingsStore.getValue("RoomList.orderByImportance");
7875
const orderAlphabetically = SettingsStore.getValue("RoomList.orderAlphabetically");
79-
this.setState({
76+
await this.updateState({
8077
tagsEnabled,
8178
preferredSort: orderAlphabetically ? SortAlgorithm.Alphabetic : SortAlgorithm.Recent,
8279
preferredAlgorithm: orderByImportance ? ListAlgorithm.Importance : ListAlgorithm.Natural,
8380
});
8481
this.setAlgorithmClass();
8582
}
8683

87-
protected __onDispatch(payload: ActionPayload): void {
84+
protected async onDispatch(payload: ActionPayload) {
8885
if (payload.action === 'MatrixActions.sync') {
8986
// Filter out anything that isn't the first PREPARED sync.
9087
if (!(payload.prevState === 'PREPARED' && payload.state !== 'PREPARED')) {
9188
return;
9289
}
9390

91+
// TODO: Remove this once the RoomListStore becomes default
9492
this.checkEnabled();
9593
if (!this.enabled) return;
9694

9795
this.matrixClient = payload.matrixClient;
9896

9997
// Update any settings here, as some may have happened before we were logically ready.
100-
this.readAndCacheSettingsFromStore();
101-
102-
// noinspection JSIgnoredPromiseFromCall
103-
this.regenerateAllLists();
98+
console.log("Regenerating room lists: Startup");
99+
await this.readAndCacheSettingsFromStore();
100+
await this.regenerateAllLists();
104101
}
105102

106103
// TODO: Remove this once the RoomListStore becomes default
@@ -109,7 +106,7 @@ class _RoomListStore extends Store<ActionPayload> {
109106
if (payload.action === 'on_client_not_viable' || payload.action === 'on_logged_out') {
110107
// Reset state without causing updates as the client will have been destroyed
111108
// and downstream code will throw NPE errors.
112-
this.reset();
109+
this.reset(null, true);
113110
this.matrixClient = null;
114111
this.initialListsGenerated = false; // we'll want to regenerate them
115112
}
@@ -120,14 +117,15 @@ class _RoomListStore extends Store<ActionPayload> {
120117

121118
if (payload.action === 'setting_updated') {
122119
if (this.watchedSettings.includes(payload.settingName)) {
123-
this.readAndCacheSettingsFromStore();
120+
console.log("Regenerating room lists: Settings changed");
121+
await this.readAndCacheSettingsFromStore();
124122

125-
// noinspection JSIgnoredPromiseFromCall
126-
this.regenerateAllLists(); // regenerate the lists now
123+
await this.regenerateAllLists(); // regenerate the lists now
127124
}
128125
} else if (payload.action === 'MatrixActions.Room.receipt') {
129126
// First see if the receipt event is for our own user. If it was, trigger
130127
// a room update (we probably read the room on a different device).
128+
// noinspection JSObjectNullOrUndefined - this.matrixClient can't be null by this point in the lifecycle
131129
const myUserId = this.matrixClient.getUserId();
132130
for (const eventId of Object.keys(payload.event.getContent())) {
133131
const receiptUsers = Object.keys(payload.event.getContent()[eventId]['m.read'] || {});
@@ -167,19 +165,18 @@ class _RoomListStore extends Store<ActionPayload> {
167165
}
168166
}
169167

170-
private setState(newState: IState) {
168+
protected async updateState(newState: IState) {
171169
if (!this.enabled) return;
172170

173-
this.state = Object.assign(this.state, newState);
174-
this.__emitChange();
171+
await super.updateState(newState);
175172
}
176173

177174
private setAlgorithmClass() {
178175
this.algorithm = getAlgorithmInstance(this.state.preferredAlgorithm);
179176
}
180177

181178
private async regenerateAllLists() {
182-
console.log("REGEN");
179+
console.warn("Regenerating all room lists");
183180
const tags: ITagSortingMap = {};
184181
for (const tagId of OrderedDefaultTagIDs) {
185182
tags[tagId] = this.getSortAlgorithmFor(tagId);
@@ -196,7 +193,7 @@ class _RoomListStore extends Store<ActionPayload> {
196193

197194
this.initialListsGenerated = true;
198195

199-
// TODO: How do we asynchronously update the store's state? or do we just give in and make it all sync?
196+
this.emit(LISTS_UPDATE_EVENT, this);
200197
}
201198
}
202199

src/stores/room-list/RoomListStoreTempProxy.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import { Room } from "matrix-js-sdk/src/models/room";
1919
import SettingsStore from "../../settings/SettingsStore";
2020
import RoomListStore from "./RoomListStore2";
2121
import OldRoomListStore from "../RoomListStore";
22+
import { ITagMap } from "./algorithms/IAlgorithm";
23+
import { UPDATE_EVENT } from "../AsyncStore";
2224

2325
/**
2426
* Temporary RoomListStore proxy. Should be replaced with RoomListStore2 when
@@ -33,13 +35,13 @@ export class RoomListStoreTempProxy {
3335

3436
public static addListener(handler: () => void) {
3537
if (RoomListStoreTempProxy.isUsingNewStore()) {
36-
return RoomListStore.instance.addListener(handler);
38+
return RoomListStore.instance.on(UPDATE_EVENT, handler);
3739
} else {
3840
return OldRoomListStore.addListener(handler);
3941
}
4042
}
4143

42-
public static getRoomLists(): {[tagId in TagID]: Room[]} {
44+
public static getRoomLists(): ITagMap {
4345
if (RoomListStoreTempProxy.isUsingNewStore()) {
4446
return RoomListStore.instance.orderedLists;
4547
} else {

src/stores/room-list/algorithms/ChaoticAlgorithm.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ export class ChaoticAlgorithm implements IAlgorithm {
6565
}
6666

6767
// TODO: Remove logging
68-
console.log('setting known rooms - regen in progress');
6968
console.log({alg: this.representativeAlgorithm});
7069

7170
// Step through each room and determine which tags it should be in.

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1827,6 +1827,11 @@ autoprefixer@^9.0.0:
18271827
postcss "^7.0.27"
18281828
postcss-value-parser "^4.0.3"
18291829

1830+
await-lock@^2.0.1:
1831+
version "2.0.1"
1832+
resolved "https://registry.yarnpkg.com/await-lock/-/await-lock-2.0.1.tgz#b3f65fdf66e08f7538260f79b46c15bcfc18cadd"
1833+
integrity sha512-ntLi9fzlMT/vWjC1wwVI11/cSRJ3nTS35qVekNc9WnaoMOP2eWH0RvIqwLQkDjX4a4YynsKEv+Ere2VONp9wxg==
1834+
18301835
aws-sign2@~0.7.0:
18311836
version "0.7.0"
18321837
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"

0 commit comments

Comments
 (0)