Skip to content

Commit 2d9c938

Browse files
authored
Support cancelling events whilst they are in status = ENCRYPTING (#2095)
1 parent bd47667 commit 2d9c938

File tree

5 files changed

+107
-15
lines changed

5 files changed

+107
-15
lines changed

spec/test-utils.js

+2
Original file line numberDiff line numberDiff line change
@@ -365,3 +365,5 @@ export function setHttpResponses(
365365
.respond(200, response.data);
366366
});
367367
}
368+
369+
export const emitPromise = (e, k) => new Promise(r => e.once(k, r));

spec/unit/matrix-client.spec.js

+81-1
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ import {
1111
UNSTABLE_MSC3089_TREE_SUBTYPE,
1212
} from "../../src/@types/event";
1313
import { MEGOLM_ALGORITHM } from "../../src/crypto/olmlib";
14-
import { MatrixEvent } from "../../src/models/event";
14+
import { EventStatus, MatrixEvent } from "../../src/models/event";
1515
import { Preset } from "../../src/@types/partials";
16+
import * as testUtils from "../test-utils";
1617

1718
jest.useFakeTimers();
1819

@@ -867,4 +868,83 @@ describe("MatrixClient", function() {
867868
await client.redactEvent(roomId, eventId, txnId, { reason });
868869
});
869870
});
871+
872+
describe("cancelPendingEvent", () => {
873+
const roomId = "!room:server";
874+
const txnId = "m12345";
875+
876+
const mockRoom = {
877+
getMyMembership: () => "join",
878+
updatePendingEvent: (event, status) => event.setStatus(status),
879+
currentState: {
880+
getStateEvents: (eventType, stateKey) => {
881+
if (eventType === EventType.RoomCreate) {
882+
expect(stateKey).toEqual("");
883+
return new MatrixEvent({
884+
content: {
885+
[RoomCreateTypeField]: RoomType.Space,
886+
},
887+
});
888+
} else if (eventType === EventType.RoomEncryption) {
889+
expect(stateKey).toEqual("");
890+
return new MatrixEvent({ content: {} });
891+
} else {
892+
throw new Error("Unexpected event type or state key");
893+
}
894+
},
895+
},
896+
};
897+
898+
let event;
899+
beforeEach(async () => {
900+
event = new MatrixEvent({
901+
event_id: "~" + roomId + ":" + txnId,
902+
user_id: client.credentials.userId,
903+
sender: client.credentials.userId,
904+
room_id: roomId,
905+
origin_server_ts: new Date().getTime(),
906+
});
907+
event.setTxnId(txnId);
908+
909+
client.getRoom = (getRoomId) => {
910+
expect(getRoomId).toEqual(roomId);
911+
return mockRoom;
912+
};
913+
client.crypto = { // mock crypto
914+
encryptEvent: (event, room) => new Promise(() => {}),
915+
};
916+
});
917+
918+
function assertCancelled() {
919+
expect(event.status).toBe(EventStatus.CANCELLED);
920+
expect(client.scheduler.removeEventFromQueue(event)).toBeFalsy();
921+
expect(httpLookups.filter(h => h.path.includes("/send/")).length).toBe(0);
922+
}
923+
924+
it("should cancel an event which is queued", () => {
925+
event.setStatus(EventStatus.QUEUED);
926+
client.scheduler.queueEvent(event);
927+
client.cancelPendingEvent(event);
928+
assertCancelled();
929+
});
930+
931+
it("should cancel an event which is encrypting", async () => {
932+
client.encryptAndSendEvent(null, event);
933+
await testUtils.emitPromise(event, "Event.status");
934+
client.cancelPendingEvent(event);
935+
assertCancelled();
936+
});
937+
938+
it("should cancel an event which is not sent", () => {
939+
event.setStatus(EventStatus.NOT_SENT);
940+
client.cancelPendingEvent(event);
941+
assertCancelled();
942+
});
943+
944+
it("should error when given any other event status", () => {
945+
event.setStatus(EventStatus.SENDING);
946+
expect(() => client.cancelPendingEvent(event)).toThrow("cannot cancel an event with status sending");
947+
expect(event.status).toBe(EventStatus.SENDING);
948+
});
949+
});
870950
});

src/client.ts

+22-9
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ limitations under the License.
2121

2222
import { EventEmitter } from "events";
2323

24-
import { ISyncStateData, SyncApi } from "./sync";
24+
import { ISyncStateData, SyncApi, SyncState } from "./sync";
2525
import { EventStatus, IContent, IDecryptOptions, IEvent, MatrixEvent } from "./models/event";
2626
import { StubStore } from "./store/stub";
2727
import { createNewMatrixCall, MatrixCall } from "./webrtc/call";
@@ -96,7 +96,6 @@ import {
9696
IRecoveryKey,
9797
ISecretStorageKeyInfo,
9898
} from "./crypto/api";
99-
import { SyncState } from "./sync";
10099
import { EventTimelineSet } from "./models/event-timeline-set";
101100
import { VerificationRequest } from "./crypto/verification/request/VerificationRequest";
102101
import { VerificationBase as Verification } from "./crypto/verification/Base";
@@ -816,6 +815,7 @@ export class MatrixClient extends EventEmitter {
816815
protected exportedOlmDeviceToImport: IOlmDevice;
817816
protected txnCtr = 0;
818817
protected mediaHandler = new MediaHandler();
818+
protected pendingEventEncryption = new Map<string, Promise<void>>();
819819

820820
constructor(opts: IMatrixClientCreateOpts) {
821821
super();
@@ -870,7 +870,7 @@ export class MatrixClient extends EventEmitter {
870870

871871
this.scheduler = opts.scheduler;
872872
if (this.scheduler) {
873-
this.scheduler.setProcessFunction(async (eventToSend) => {
873+
this.scheduler.setProcessFunction(async (eventToSend: MatrixEvent) => {
874874
const room = this.getRoom(eventToSend.getRoomId());
875875
if (eventToSend.status !== EventStatus.SENDING) {
876876
this.updatePendingEventStatus(room, eventToSend, EventStatus.SENDING);
@@ -3399,15 +3399,18 @@ export class MatrixClient extends EventEmitter {
33993399
* Cancel a queued or unsent event.
34003400
*
34013401
* @param {MatrixEvent} event Event to cancel
3402-
* @throws Error if the event is not in QUEUED or NOT_SENT state
3402+
* @throws Error if the event is not in QUEUED, NOT_SENT or ENCRYPTING state
34033403
*/
34043404
public cancelPendingEvent(event: MatrixEvent) {
3405-
if ([EventStatus.QUEUED, EventStatus.NOT_SENT].indexOf(event.status) < 0) {
3405+
if (![EventStatus.QUEUED, EventStatus.NOT_SENT, EventStatus.ENCRYPTING].includes(event.status)) {
34063406
throw new Error("cannot cancel an event with status " + event.status);
34073407
}
34083408

3409-
// first tell the scheduler to forget about it, if it's queued
3410-
if (this.scheduler) {
3409+
// if the event is currently being encrypted then
3410+
if (event.status === EventStatus.ENCRYPTING) {
3411+
this.pendingEventEncryption.delete(event.getId());
3412+
} else if (this.scheduler && event.status === EventStatus.QUEUED) {
3413+
// tell the scheduler to forget about it, if it's queued
34113414
this.scheduler.removeEventFromQueue(event);
34123415
}
34133416

@@ -3669,16 +3672,26 @@ export class MatrixClient extends EventEmitter {
36693672
* @private
36703673
*/
36713674
private encryptAndSendEvent(room: Room, event: MatrixEvent, callback?: Callback): Promise<ISendEventResponse> {
3675+
let cancelled = false;
36723676
// Add an extra Promise.resolve() to turn synchronous exceptions into promise rejections,
36733677
// so that we can handle synchronous and asynchronous exceptions with the
36743678
// same code path.
36753679
return Promise.resolve().then(() => {
36763680
const encryptionPromise = this.encryptEventIfNeeded(event, room);
3677-
if (!encryptionPromise) return null;
3681+
if (!encryptionPromise) return null; // doesn't need encryption
36783682

3683+
this.pendingEventEncryption.set(event.getId(), encryptionPromise);
36793684
this.updatePendingEventStatus(room, event, EventStatus.ENCRYPTING);
3680-
return encryptionPromise.then(() => this.updatePendingEventStatus(room, event, EventStatus.SENDING));
3685+
return encryptionPromise.then(() => {
3686+
if (!this.pendingEventEncryption.has(event.getId())) {
3687+
// cancelled via MatrixClient::cancelPendingEvent
3688+
cancelled = true;
3689+
return;
3690+
}
3691+
this.updatePendingEventStatus(room, event, EventStatus.SENDING);
3692+
});
36813693
}).then(() => {
3694+
if (cancelled) return {} as ISendEventResponse;
36823695
let promise: Promise<ISendEventResponse>;
36833696
if (this.scheduler) {
36843697
// if this returns a promise then the scheduler has control now and will

src/crypto/index.ts

-3
Original file line numberDiff line numberDiff line change
@@ -2728,7 +2728,6 @@ export class Crypto extends EventEmitter {
27282728
}
27292729
}
27302730

2731-
/* eslint-disable valid-jsdoc */ //https://github.com/eslint/eslint/issues/7307
27322731
/**
27332732
* Encrypt an event according to the configuration of the room.
27342733
*
@@ -2739,8 +2738,6 @@ export class Crypto extends EventEmitter {
27392738
* @return {Promise?} Promise which resolves when the event has been
27402739
* encrypted, or null if nothing was needed
27412740
*/
2742-
/* eslint-enable valid-jsdoc */
2743-
// TODO this return type lies
27442741
public async encryptEvent(event: MatrixEvent, room: Room): Promise<void> {
27452742
if (!room) {
27462743
throw new Error("Cannot send encrypted messages in unknown rooms");

src/models/room.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -2302,12 +2302,12 @@ function pendingEventsKey(roomId: string): string {
23022302
return `mx_pending_events_${roomId}`;
23032303
}
23042304

2305-
/* a map from current event status to a list of allowed next statuses
2306-
*/
2305+
// a map from current event status to a list of allowed next statuses
23072306
const ALLOWED_TRANSITIONS: Record<EventStatus, EventStatus[]> = {
23082307
[EventStatus.ENCRYPTING]: [
23092308
EventStatus.SENDING,
23102309
EventStatus.NOT_SENT,
2310+
EventStatus.CANCELLED,
23112311
],
23122312
[EventStatus.SENDING]: [
23132313
EventStatus.ENCRYPTING,

0 commit comments

Comments
 (0)