forked from mystiq/hydrogen-web
pass write errors in a store to the transaction
This commit is contained in:
parent
aeedb948cc
commit
533b0f40d3
3 changed files with 90 additions and 9 deletions
|
@ -43,12 +43,16 @@ export class LogItem {
|
||||||
/** logs a reference to a different log item, usually obtained from runDetached.
|
/** logs a reference to a different log item, usually obtained from runDetached.
|
||||||
This is useful if the referenced operation can't be awaited. */
|
This is useful if the referenced operation can't be awaited. */
|
||||||
refDetached(logItem, logLevel = null) {
|
refDetached(logItem, logLevel = null) {
|
||||||
if (!logItem._values.refId) {
|
logItem.ensureRefId();
|
||||||
logItem.set("refId", this._logger._createRefId());
|
|
||||||
}
|
|
||||||
return this.log({ref: logItem._values.refId}, logLevel);
|
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`.
|
* Creates a new child item and runs it in `callback`.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -18,6 +18,7 @@ import {QueryTarget, IDBQuery} from "./QueryTarget";
|
||||||
import {IDBRequestAttemptError} from "./error";
|
import {IDBRequestAttemptError} from "./error";
|
||||||
import {reqAsPromise} from "./utils";
|
import {reqAsPromise} from "./utils";
|
||||||
import {Transaction} from "./Transaction";
|
import {Transaction} from "./Transaction";
|
||||||
|
import {LogItem} from "../../../logging/LogItem.js";
|
||||||
|
|
||||||
const LOG_REQUESTS = false;
|
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)));
|
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,
|
// 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
|
// which is the behaviour we want. Therefore, it is ok to not create a promise for this
|
||||||
// request and await it.
|
// 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,
|
// Note that this can still throw synchronously, like it does for TransactionInactiveError,
|
||||||
// see https://www.w3.org/TR/IndexedDB-2/#transaction-lifetime-concept
|
// 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`.
|
// 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`.
|
// 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import {StoreNames} from "../common";
|
||||||
import {txnAsPromise} from "./utils";
|
import {txnAsPromise} from "./utils";
|
||||||
import {StorageError} from "../common";
|
import {StorageError} from "../common";
|
||||||
import {Store} from "./Store";
|
import {Store} from "./Store";
|
||||||
|
import {Storage} from "./Storage";
|
||||||
import {SessionStore} from "./stores/SessionStore";
|
import {SessionStore} from "./stores/SessionStore";
|
||||||
import {RoomSummaryStore} from "./stores/RoomSummaryStore";
|
import {RoomSummaryStore} from "./stores/RoomSummaryStore";
|
||||||
import {InviteStore} from "./stores/InviteStore";
|
import {InviteStore} from "./stores/InviteStore";
|
||||||
|
@ -35,13 +36,24 @@ import {OutboundGroupSessionStore} from "./stores/OutboundGroupSessionStore";
|
||||||
import {GroupSessionDecryptionStore} from "./stores/GroupSessionDecryptionStore";
|
import {GroupSessionDecryptionStore} from "./stores/GroupSessionDecryptionStore";
|
||||||
import {OperationStore} from "./stores/OperationStore";
|
import {OperationStore} from "./stores/OperationStore";
|
||||||
import {AccountDataStore} from "./stores/AccountDataStore";
|
import {AccountDataStore} from "./stores/AccountDataStore";
|
||||||
|
import {LogItem} from "../../../logging/LogItem.js";
|
||||||
import {BaseLogger} from "../../../logging/BaseLogger.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 {
|
export class Transaction {
|
||||||
private _txn: IDBTransaction;
|
private _txn: IDBTransaction;
|
||||||
private _allowedStoreNames: StoreNames[];
|
private _allowedStoreNames: StoreNames[];
|
||||||
private _stores: { [storeName in StoreNames]?: any };
|
private _stores: { [storeName in StoreNames]?: any };
|
||||||
private _storage: Storage;
|
private _storage: Storage;
|
||||||
|
private _writeErrors: WriteErrorInfo[];
|
||||||
|
|
||||||
constructor(txn: IDBTransaction, allowedStoreNames: StoreNames[], storage: Storage) {
|
constructor(txn: IDBTransaction, allowedStoreNames: StoreNames[], storage: Storage) {
|
||||||
this._txn = txn;
|
this._txn = txn;
|
||||||
|
@ -154,5 +166,33 @@ export class Transaction {
|
||||||
abort(): void {
|
abort(): void {
|
||||||
// TODO: should we wrap the exception in a StorageError?
|
// TODO: should we wrap the exception in a StorageError?
|
||||||
this._txn.abort();
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue