pass write errors in a store to the transaction

This commit is contained in:
Bruno Windels 2021-09-17 18:21:48 +02:00
parent aeedb948cc
commit 533b0f40d3
3 changed files with 90 additions and 9 deletions

View file

@ -43,12 +43,16 @@ export class LogItem {
/** logs a reference to a different log item, usually obtained from runDetached.
This is useful if the referenced operation can't be awaited. */
refDetached(logItem, logLevel = null) {
if (!logItem._values.refId) {
logItem.set("refId", this._logger._createRefId());
}
logItem.ensureRefId();
return this.log({ref: logItem._values.refId}, logLevel);
}
ensureRefId() {
if (!this._values.refId) {
this.set("refId", this._logger._createRefId());
}
}
/**
* Creates a new child item and runs it in `callback`.
*/

View file

@ -18,6 +18,7 @@ import {QueryTarget, IDBQuery} from "./QueryTarget";
import {IDBRequestAttemptError} from "./error";
import {reqAsPromise} from "./utils";
import {Transaction} from "./Transaction";
import {LogItem} from "../../../logging/LogItem.js";
const LOG_REQUESTS = false;
@ -148,7 +149,7 @@ export class Store<T> extends QueryTarget<T> {
return new QueryTarget<T>(new QueryTargetWrapper<T>(this._idbStore.index(indexName)));
}
put(value: T): void {
put(value: T, log?: LogItem): void {
// If this request fails, the error will bubble up to the transaction and abort it,
// which is the behaviour we want. Therefore, it is ok to not create a promise for this
// request and await it.
@ -159,16 +160,52 @@ export class Store<T> extends QueryTarget<T> {
//
// Note that this can still throw synchronously, like it does for TransactionInactiveError,
// see https://www.w3.org/TR/IndexedDB-2/#transaction-lifetime-concept
this._idbStore.put(value);
const request = this._idbStore.put(value);
this._prepareErrorLog(request, log, "put", undefined, value);
}
add(value: T): void {
add(value: T, log?: LogItem): void {
// ok to not monitor result of request, see comment in `put`.
this._idbStore.add(value);
const request = this._idbStore.add(value);
this._prepareErrorLog(request, log, "add", undefined, value);
}
delete(keyOrKeyRange: IDBValidKey | IDBKeyRange): Promise<undefined> {
delete(keyOrKeyRange: IDBValidKey | IDBKeyRange, log?: LogItem): void {
// ok to not monitor result of request, see comment in `put`.
return reqAsPromise(this._idbStore.delete(keyOrKeyRange));
const request = this._idbStore.delete(keyOrKeyRange);
this._prepareErrorLog(request, log, "delete", keyOrKeyRange, undefined);
}
private _prepareErrorLog(request: IDBRequest, log: LogItem | undefined, operationName: string, key: IDBValidKey | IDBKeyRange | undefined, value: T | undefined) {
if (log) {
log.ensureRefId();
}
reqAsPromise(request).catch(err => {
try {
if (!key && value) {
key = this._getKey(value);
}
} catch {
key = "getKey failed";
}
this._transaction.addWriteError(err, log, operationName, key);
});
}
private _getKey(value: T): IDBValidKey {
const {keyPath} = this._idbStore;
if (Array.isArray(keyPath)) {
let field: any = value;
for (const part of keyPath) {
if (typeof field === "object") {
field = field[part];
} else {
break;
}
}
return field as IDBValidKey;
} else {
return value[keyPath] as IDBValidKey;
}
}
}

View file

@ -18,6 +18,7 @@ import {StoreNames} from "../common";
import {txnAsPromise} from "./utils";
import {StorageError} from "../common";
import {Store} from "./Store";
import {Storage} from "./Storage";
import {SessionStore} from "./stores/SessionStore";
import {RoomSummaryStore} from "./stores/RoomSummaryStore";
import {InviteStore} from "./stores/InviteStore";
@ -35,13 +36,24 @@ import {OutboundGroupSessionStore} from "./stores/OutboundGroupSessionStore";
import {GroupSessionDecryptionStore} from "./stores/GroupSessionDecryptionStore";
import {OperationStore} from "./stores/OperationStore";
import {AccountDataStore} from "./stores/AccountDataStore";
import {LogItem} from "../../../logging/LogItem.js";
import {BaseLogger} from "../../../logging/BaseLogger.js";
class WriteErrorInfo {
constructor(
public readonly error: StorageError,
public readonly refItem: LogItem | undefined,
public readonly operationName: string,
public readonly key: IDBValidKey | IDBKeyRange | undefined,
) {}
}
export class Transaction {
private _txn: IDBTransaction;
private _allowedStoreNames: StoreNames[];
private _stores: { [storeName in StoreNames]?: any };
private _storage: Storage;
private _writeErrors: WriteErrorInfo[];
constructor(txn: IDBTransaction, allowedStoreNames: StoreNames[], storage: Storage) {
this._txn = txn;
@ -154,5 +166,33 @@ export class Transaction {
abort(): void {
// TODO: should we wrap the exception in a StorageError?
this._txn.abort();
addWriteError(error: StorageError, refItem: LogItem | undefined, operationName: string, key: IDBValidKey | IDBKeyRange | undefined) {
// don't log subsequent `AbortError`s
if (error.errcode !== "AbortError" || this._writeErrors.length === 0) {
this._writeErrors.push(new WriteErrorInfo(error, refItem, operationName, key));
}
}
private _logWriteErrors(parentItem: LogItem | undefined) {
const callback = errorGroupItem => {
// we don't have context when there is no parentItem, so at least log stores
if (!parentItem) {
errorGroupItem.set("allowedStoreNames", this._allowedStoreNames);
}
for (const info of this._writeErrors) {
errorGroupItem.wrap({l: info.operationName, id: info.key}, item => {
if (info.refItem) {
item.refDetached(info.refItem);
}
item.catch(info.error);
});
}
};
const label = `${this._writeErrors.length} storage write operation(s) failed`;
if (parentItem) {
parentItem.wrap(label, callback);
} else {
this.logger.run(label, callback);
}
}
}