draft redaction support, no local echo yet
This commit is contained in:
parent
b31dc38af1
commit
edaac9f436
4 changed files with 115 additions and 20 deletions
|
@ -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"));
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Reference in a new issue