diff --git a/src/components/views/toasts/NonUrgentEchoFailureToast.tsx b/src/components/views/toasts/NonUrgentEchoFailureToast.tsx
new file mode 100644
index 0000000000..c9a5037045
--- /dev/null
+++ b/src/components/views/toasts/NonUrgentEchoFailureToast.tsx
@@ -0,0 +1,30 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React from "react";
+import { _t } from "../../../languageHandler";
+
+export default class NonUrgentEchoFailureToast extends React.PureComponent {
+ render() {
+ return (
+
+ {_t("Your server isn't responding to some
requests", {}, {
+ 'a': (sub) =>
{sub}
+ })}
+
+ )
+ }
+}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 6433285d20..a281834628 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -612,6 +612,7 @@
"Headphones": "Headphones",
"Folder": "Folder",
"Pin": "Pin",
+ "Your server isn't responding to some requests": "Your server isn't responding to some requests",
"From %(deviceName)s (%(deviceId)s)": "From %(deviceName)s (%(deviceId)s)",
"Decline (%(counter)s)": "Decline (%(counter)s)",
"Accept to continue:": "Accept to continue:",
diff --git a/src/stores/local-echo/CachedEcho.ts b/src/stores/local-echo/CachedEcho.ts
index caa7ad1d48..2d1f3d8848 100644
--- a/src/stores/local-echo/CachedEcho.ts
+++ b/src/stores/local-echo/CachedEcho.ts
@@ -15,7 +15,7 @@ limitations under the License.
*/
import { EchoContext } from "./EchoContext";
-import { RunFn, TransactionStatus } from "./EchoTransaction";
+import { EchoTransaction, RunFn, TransactionStatus } from "./EchoTransaction";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { EventEmitter } from "events";
@@ -26,10 +26,10 @@ export async function implicitlyReverted() {
export const PROPERTY_UPDATED = "property_updated";
export abstract class CachedEcho extends EventEmitter {
- private cache = new Map();
+ private cache = new Map();
protected matrixClient: MatrixClient;
- protected constructor(protected context: C, private lookupFn: (key: K) => V) {
+ protected constructor(public readonly context: C, private lookupFn: (key: K) => V) {
super();
}
@@ -49,24 +49,39 @@ export abstract class CachedEcho extends EventEmitt
* @returns The value for the key.
*/
public getValue(key: K): V {
- return this.cache.has(key) ? this.cache.get(key) : this.lookupFn(key);
+ return this.cache.has(key) ? this.cache.get(key).val : this.lookupFn(key);
}
- private cacheVal(key: K, val: V) {
- this.cache.set(key, val);
+ private cacheVal(key: K, val: V, txn: EchoTransaction) {
+ this.cache.set(key, {txn, val});
this.emit(PROPERTY_UPDATED, key);
}
private decacheKey(key: K) {
- this.cache.delete(key);
- this.emit(PROPERTY_UPDATED, key);
+ if (this.cache.has(key)) {
+ this.cache.get(key).txn.cancel(); // should be safe to call
+ this.cache.delete(key);
+ this.emit(PROPERTY_UPDATED, key);
+ }
+ }
+
+ protected markEchoReceived(key: K) {
+ this.decacheKey(key);
}
public setValue(auditName: string, key: K, targetVal: V, runFn: RunFn, revertFn: RunFn) {
- this.cacheVal(key, targetVal); // set the cache now as it won't be updated by the .when() ladder below.
- this.context.beginTransaction(auditName, runFn)
- .when(TransactionStatus.Pending, () => this.cacheVal(key, targetVal))
- .whenAnyOf([TransactionStatus.DoneError, TransactionStatus.DoneSuccess], () => this.decacheKey(key))
+ // Cancel any pending transactions for the same key
+ if (this.cache.has(key)) {
+ this.cache.get(key).txn.cancel();
+ }
+
+ const txn = this.context.beginTransaction(auditName, runFn);
+ this.cacheVal(key, targetVal, txn); // set the cache now as it won't be updated by the .when() ladder below.
+
+ txn.when(TransactionStatus.Pending, () => this.cacheVal(key, targetVal, txn))
+ .when(TransactionStatus.DoneError, () => this.decacheKey(key))
.when(TransactionStatus.DoneError, () => revertFn());
+
+ txn.run();
}
}
diff --git a/src/stores/local-echo/EchoContext.ts b/src/stores/local-echo/EchoContext.ts
index 0d5eb961c3..ffad76b4a6 100644
--- a/src/stores/local-echo/EchoContext.ts
+++ b/src/stores/local-echo/EchoContext.ts
@@ -27,12 +27,17 @@ export enum ContextTransactionState {
export abstract class EchoContext extends Whenable implements IDestroyable {
private _transactions: EchoTransaction[] = [];
+ private _state = ContextTransactionState.NotStarted;
public readonly startTime: Date = new Date();
public get transactions(): EchoTransaction[] {
return arrayFastClone(this._transactions);
}
+ public get state(): ContextTransactionState {
+ return this._state;
+ }
+
public beginTransaction(auditName: string, runFn: RunFn): EchoTransaction {
const txn = new EchoTransaction(auditName, runFn);
this._transactions.push(txn);
@@ -48,7 +53,7 @@ export abstract class EchoContext extends Whenable impl
private checkTransactions = () => {
let status = ContextTransactionState.AllSuccessful;
for (const txn of this.transactions) {
- if (txn.status === TransactionStatus.DoneError) {
+ if (txn.status === TransactionStatus.DoneError || txn.didPreviouslyFail) {
status = ContextTransactionState.PendingErrors;
break;
} else if (txn.status === TransactionStatus.Pending) {
@@ -56,6 +61,7 @@ export abstract class EchoContext extends Whenable impl
// no break as we might hit something which broke
}
}
+ this._state = status;
this.notifyCondition(status);
};
@@ -63,6 +69,7 @@ export abstract class EchoContext extends Whenable impl
for (const txn of this.transactions) {
txn.destroy();
}
+ this._transactions = [];
super.destroy();
}
}
diff --git a/src/stores/local-echo/EchoStore.ts b/src/stores/local-echo/EchoStore.ts
index 80c669e5c6..8514bff731 100644
--- a/src/stores/local-echo/EchoStore.ts
+++ b/src/stores/local-echo/EchoStore.ts
@@ -22,12 +22,19 @@ import { RoomEchoContext } from "./RoomEchoContext";
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
import defaultDispatcher from "../../dispatcher/dispatcher";
import { ActionPayload } from "../../dispatcher/payloads";
+import { ContextTransactionState } from "./EchoContext";
+import NonUrgentToastStore, { ToastReference } from "../NonUrgentToastStore";
+import NonUrgentEchoFailureToast from "../../components/views/toasts/NonUrgentEchoFailureToast";
+
+interface IState {
+ toastRef: ToastReference;
+}
type ContextKey = string;
const roomContextKey = (room: Room): ContextKey => `room-${room.roomId}`;
-export class EchoStore extends AsyncStoreWithClient {
+export class EchoStore extends AsyncStoreWithClient {
private static _instance: EchoStore;
private caches = new Map>();
@@ -47,13 +54,35 @@ export class EchoStore extends AsyncStoreWithClient {
if (this.caches.has(roomContextKey(room))) {
return this.caches.get(roomContextKey(room)) as RoomCachedEcho;
}
- const echo = new RoomCachedEcho(new RoomEchoContext(room));
+
+ const context = new RoomEchoContext(room);
+ context.whenAnything(() => this.checkContexts());
+
+ const echo = new RoomCachedEcho(context);
echo.setClient(this.matrixClient);
this.caches.set(roomContextKey(room), echo);
+
return echo;
}
+ private async checkContexts() {
+ let hasOrHadError = false;
+ for (const echo of this.caches.values()) {
+ hasOrHadError = echo.context.state === ContextTransactionState.PendingErrors;
+ if (hasOrHadError) break;
+ }
+
+ if (hasOrHadError && !this.state.toastRef) {
+ const ref = NonUrgentToastStore.instance.addToast(NonUrgentEchoFailureToast);
+ await this.updateState({toastRef: ref});
+ } else if (!hasOrHadError && this.state.toastRef) {
+ NonUrgentToastStore.instance.removeToast(this.state.toastRef);
+ await this.updateState({toastRef: null});
+ }
+ }
+
protected async onReady(): Promise {
+ if (!this.caches) return; // can only happen during initialization
for (const echo of this.caches.values()) {
echo.setClient(this.matrixClient);
}
diff --git a/src/stores/local-echo/EchoTransaction.ts b/src/stores/local-echo/EchoTransaction.ts
index b2125aac08..7993a7838b 100644
--- a/src/stores/local-echo/EchoTransaction.ts
+++ b/src/stores/local-echo/EchoTransaction.ts
@@ -53,6 +53,11 @@ export class EchoTransaction extends Whenable {
.catch(() => this.setStatus(TransactionStatus.DoneError));
}
+ public cancel() {
+ // Success basically means "done"
+ this.setStatus(TransactionStatus.DoneSuccess);
+ }
+
private setStatus(status: TransactionStatus) {
this._status = status;
if (status === TransactionStatus.DoneError) {
diff --git a/src/stores/local-echo/RoomCachedEcho.ts b/src/stores/local-echo/RoomCachedEcho.ts
index 0aec4a4e1c..3ac01d3873 100644
--- a/src/stores/local-echo/RoomCachedEcho.ts
+++ b/src/stores/local-echo/RoomCachedEcho.ts
@@ -60,6 +60,7 @@ export class RoomCachedEcho extends CachedEcho {
- setRoomNotifsState(this.context.room.roomId, v);
+ return setRoomNotifsState(this.context.room.roomId, v);
}, implicitlyReverted);
}
}