draft redaction support, no local echo yet

This commit is contained in:
Bruno Windels 2021-05-19 16:41:07 +02:00
parent b31dc38af1
commit edaac9f436
4 changed files with 115 additions and 20 deletions

View file

@ -19,4 +19,19 @@ export function makeTxnId() {
const n = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); const n = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
const str = n.toString(16); const str = n.toString(16);
return "t" + "0".repeat(14 - str.length) + str; return "t" + "0".repeat(14 - str.length) + str;
}
export function isTxnId(txnId) {
return txnId.startsWith("t") && txnId.length === 15;
}
export function tests() {
return {
"isTxnId succeeds on result of makeTxnId": assert => {
assert(isTxnId(makeTxnId()));
},
"isTxnId fails on event id": assert => {
assert(!isTxnId("$yS_n5n3cIO2aTtek0_2ZSlv-7g4YYR2zKrk2mFCW_rm"));
},
}
} }

View file

@ -121,6 +121,10 @@ export class HomeServerApi {
return this._put(`/rooms/${encodeURIComponent(roomId)}/send/${encodeURIComponent(eventType)}/${encodeURIComponent(txnId)}`, {}, content, options); return this._put(`/rooms/${encodeURIComponent(roomId)}/send/${encodeURIComponent(eventType)}/${encodeURIComponent(txnId)}`, {}, content, options);
} }
redact(roomId, eventId, txnId, content, options = null) {
return this._put(`/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}/${encodeURIComponent(txnId)}`, {}, content, options);
}
receipt(roomId, receiptType, eventId, options = null) { receipt(roomId, receiptType, eventId, options = null) {
return this._post(`/rooms/${encodeURIComponent(roomId)}/receipt/${encodeURIComponent(receiptType)}/${encodeURIComponent(eventId)}`, return this._post(`/rooms/${encodeURIComponent(roomId)}/receipt/${encodeURIComponent(receiptType)}/${encodeURIComponent(eventId)}`,
{}, {}, options); {}, {}, options);

View file

@ -15,6 +15,8 @@ limitations under the License.
*/ */
import {createEnum} from "../../../utils/enum.js"; import {createEnum} from "../../../utils/enum.js";
import {AbortError} from "../../../utils/error.js"; import {AbortError} from "../../../utils/error.js";
import {isTxnId} from "../../common.js";
import {REDACTION_TYPE} from "./SendQueue.js";
export const SendStatus = createEnum( export const SendStatus = createEnum(
"Waiting", "Waiting",
@ -134,7 +136,7 @@ export class PendingEvent {
this._data.needsUpload = false; this._data.needsUpload = false;
} }
abort() { async abort() {
if (!this._aborted) { if (!this._aborted) {
this._aborted = true; this._aborted = true;
if (this._attachments) { if (this._attachments) {
@ -143,7 +145,7 @@ export class PendingEvent {
} }
} }
this._sendRequest?.abort(); this._sendRequest?.abort();
this._removeFromQueueCallback(); await this._removeFromQueueCallback();
} }
} }
@ -156,15 +158,27 @@ export class PendingEvent {
this._emitUpdate("status"); this._emitUpdate("status");
const eventType = this._data.encryptedEventType || this._data.eventType; const eventType = this._data.encryptedEventType || this._data.eventType;
const content = this._data.encryptedContent || this._data.content; const content = this._data.encryptedContent || this._data.content;
this._sendRequest = hsApi.send( if (eventType === REDACTION_TYPE) {
this.roomId, // TODO: should we double check here that this._data.redacts is not a txnId here anymore?
eventType, this._sendRequest = hsApi.redact(
this.txnId, this.roomId,
content, this._data.redacts,
{log} this.txnId,
); content,
{log}
);
} else {
this._sendRequest = hsApi.send(
this.roomId,
eventType,
this.txnId,
content,
{log}
);
}
const response = await this._sendRequest.response(); const response = await this._sendRequest.response();
this._sendRequest = null; this._sendRequest = null;
// both /send and /redact have the same response format
this._data.remoteId = response.event_id; this._data.remoteId = response.event_id;
log.set("id", this._data.remoteId); log.set("id", this._data.remoteId);
this._status = SendStatus.Sent; this._status = SendStatus.Sent;
@ -178,4 +192,16 @@ export class PendingEvent {
} }
} }
} }
get relatedTxnId() {
if (isTxnId(this._data.redacts)) {
return this._data.redacts;
}
}
setRelatedEventId(eventId) {
if (this._data.redacts) {
this._data.redacts = eventId;
}
}
} }

View file

@ -17,7 +17,9 @@ limitations under the License.
import {SortedArray} from "../../../observable/list/SortedArray.js"; import {SortedArray} from "../../../observable/list/SortedArray.js";
import {ConnectionError} from "../../error.js"; import {ConnectionError} from "../../error.js";
import {PendingEvent} from "./PendingEvent.js"; import {PendingEvent} from "./PendingEvent.js";
import {makeTxnId} from "../../common.js"; import {makeTxnId, isTxnId} from "../../common.js";
export const REDACTION_TYPE = "m.room.redaction";
export class SendQueue { export class SendQueue {
constructor({roomId, storage, hsApi, pendingEvents}) { constructor({roomId, storage, hsApi, pendingEvents}) {
@ -101,8 +103,24 @@ export class SendQueue {
} }
if (pendingEvent.needsSending) { if (pendingEvent.needsSending) {
await pendingEvent.send(this._hsApi, log); await pendingEvent.send(this._hsApi, log);
// we now have a remoteId, but this pending event may be removed at any point in the future
await this._tryUpdateEvent(pendingEvent); // once the remote echo comes in. So if we have any related events that need to resolve
// the relatedTxnId to a related event id, they need to do so now.
// We ensure this by writing the new remote id for the pending event and all related events
// with unresolved relatedTxnId in the queue in one transaction.
const relatedEvents = this._pendingEvents.array.find(pe => pe.relatedTxnId === pendingEvent.txnId);
const txn = await this._storage.readWriteTxn([this._storage.storeNames.pendingEvents]);
try {
await this._tryUpdateEventWithTxn(pendingEvent, txn);
for (const relatedPE of relatedEvents) {
relatedPE.setRelatedEventId(pendingEvent.remoteId);
await this._tryUpdateEventWithTxn(relatedPE, txn);
}
} catch (err) {
txn.abort();
throw err;
}
await txn.complete();
} }
} }
@ -168,7 +186,12 @@ export class SendQueue {
} }
async enqueueEvent(eventType, content, attachments, log) { async enqueueEvent(eventType, content, attachments, log) {
const pendingEvent = await this._createAndStoreEvent(eventType, content, attachments); await this._enqueueEvent(eventType, content, attachments, null, log);
}
async _enqueueEvent(eventType, content, attachments, redacts, log) {
const pendingEvent = await this._createAndStoreEvent(eventType, content, redacts, attachments);
this._pendingEvents.set(pendingEvent); this._pendingEvents.set(pendingEvent);
log.set("queueIndex", pendingEvent.queueIndex); log.set("queueIndex", pendingEvent.queueIndex);
log.set("pendingEvents", this._pendingEvents.length); log.set("pendingEvents", this._pendingEvents.length);
@ -180,6 +203,27 @@ export class SendQueue {
} }
} }
async enqueueRedaction(eventIdOrTxnId, reason, log) {
if (isTxnId(eventIdOrTxnId)) {
const txnId = eventIdOrTxnId;
const pe = this._pendingEvents.array.find(pe => pe.txnId === txnId);
if (pe && !pe.remoteId && pe.status !== SendStatus.Sending) {
// haven't started sending this event yet,
// just remove it from the queue
await pe.abort();
return;
} else if (!pe) {
// we don't have the pending event anymore,
// the remote echo must have arrived in the meantime.
// we could look for it in the timeline, but for now
// we don't do anything as this race is quite unlikely
// and a bit complicated to fix.
return;
}
}
await this._enqueueEvent(REDACTION_TYPE, {reason}, null, eventIdOrTxnId, log);
}
get pendingEvents() { get pendingEvents() {
return this._pendingEvents; return this._pendingEvents;
} }
@ -187,11 +231,7 @@ export class SendQueue {
async _tryUpdateEvent(pendingEvent) { async _tryUpdateEvent(pendingEvent) {
const txn = await this._storage.readWriteTxn([this._storage.storeNames.pendingEvents]); const txn = await this._storage.readWriteTxn([this._storage.storeNames.pendingEvents]);
try { try {
// pendingEvent might have been removed already here this._tryUpdateEventWithTxn(pendingEvent, txn);
// by a racing remote echo, so check first so we don't recreate it
if (await txn.pendingEvents.exists(pendingEvent.roomId, pendingEvent.queueIndex)) {
txn.pendingEvents.update(pendingEvent.data);
}
} catch (err) { } catch (err) {
txn.abort(); txn.abort();
throw err; throw err;
@ -199,20 +239,30 @@ export class SendQueue {
await txn.complete(); await txn.complete();
} }
async _createAndStoreEvent(eventType, content, attachments) { async _tryUpdateEventWithTxn(pendingEvent, txn) {
// pendingEvent might have been removed already here
// by a racing remote echo, so check first so we don't recreate it
if (await txn.pendingEvents.exists(pendingEvent.roomId, pendingEvent.queueIndex)) {
txn.pendingEvents.update(pendingEvent.data);
}
}
async _createAndStoreEvent(eventType, content, redacts, attachments) {
const txn = await this._storage.readWriteTxn([this._storage.storeNames.pendingEvents]); const txn = await this._storage.readWriteTxn([this._storage.storeNames.pendingEvents]);
let pendingEvent; let pendingEvent;
try { try {
const pendingEventsStore = txn.pendingEvents; const pendingEventsStore = txn.pendingEvents;
const maxQueueIndex = await pendingEventsStore.getMaxQueueIndex(this._roomId) || 0; const maxQueueIndex = await pendingEventsStore.getMaxQueueIndex(this._roomId) || 0;
const queueIndex = maxQueueIndex + 1; const queueIndex = maxQueueIndex + 1;
const needsEncryption = eventType !== REDACTION_TYPE && !!this._roomEncryption;
pendingEvent = this._createPendingEvent({ pendingEvent = this._createPendingEvent({
roomId: this._roomId, roomId: this._roomId,
queueIndex, queueIndex,
eventType, eventType,
content, content,
redacts,
txnId: makeTxnId(), txnId: makeTxnId(),
needsEncryption: !!this._roomEncryption, needsEncryption,
needsUpload: !!attachments needsUpload: !!attachments
}, attachments); }, attachments);
pendingEventsStore.add(pendingEvent.data); pendingEventsStore.add(pendingEvent.data);