wrap everything that can throw a idb DOMException in StorageError

as lumia gives very cryptic errors without a stacktrace.
This commit is contained in:
Bruno Windels 2019-06-26 22:00:50 +02:00
parent 0fd52be710
commit bbb5e35bcb
9 changed files with 148 additions and 58 deletions

View file

@ -5,9 +5,6 @@ export class HomeServerError extends Error {
} }
} }
export class StorageError extends Error {
}
export class RequestAbortError extends Error { export class RequestAbortError extends Error {
} }

View file

@ -4,3 +4,17 @@ export const STORE_MAP = Object.freeze(STORE_NAMES.reduce((nameMap, name) => {
nameMap[name] = name; nameMap[name] = name;
return nameMap; return nameMap;
}, {})); }, {}));
export class StorageError extends Error {
constructor(message, cause) {
let fullMessage = message;
if (cause) {
fullMessage += ": ";
if (cause.name) {
fullMessage += `(${cause.name}) `;
}
fullMessage += cause.message;
}
super(fullMessage);
}
}

View file

@ -1,5 +1,5 @@
import Transaction from "./transaction.js"; import Transaction from "./transaction.js";
import { STORE_NAMES } from "../common.js"; import { STORE_NAMES, StorageError } from "../common.js";
export default class Storage { export default class Storage {
constructor(idbDatabase) { constructor(idbDatabase) {
@ -14,19 +14,27 @@ export default class Storage {
_validateStoreNames(storeNames) { _validateStoreNames(storeNames) {
const idx = storeNames.findIndex(name => !STORE_NAMES.includes(name)); const idx = storeNames.findIndex(name => !STORE_NAMES.includes(name));
if (idx !== -1) { if (idx !== -1) {
throw new Error(`Tried to open a transaction for unknown store ${storeNames[idx]}`); throw new StorageError(`Tried top, a transaction unknown store ${storeNames[idx]}`);
} }
} }
async readTxn(storeNames) { async readTxn(storeNames) {
this._validateStoreNames(storeNames); this._validateStoreNames(storeNames);
const txn = this._db.transaction(storeNames, "readonly"); try {
return new Transaction(txn, storeNames); const txn = this._db.transaction(storeNames, "readonly");
return new Transaction(txn, storeNames);
} catch(err) {
throw new StorageError("readTxn failed", err);
}
} }
async readWriteTxn(storeNames) { async readWriteTxn(storeNames) {
this._validateStoreNames(storeNames); this._validateStoreNames(storeNames);
const txn = this._db.transaction(storeNames, "readwrite"); try {
return new Transaction(txn, storeNames); const txn = this._db.transaction(storeNames, "readwrite");
return new Transaction(txn, storeNames);
} catch(err) {
throw new StorageError("readWriteTxn failed", err);
}
} }
} }

View file

@ -1,9 +1,64 @@
import QueryTarget from "./query-target.js"; import QueryTarget from "./query-target.js";
import { reqAsPromise } from "./utils.js"; import { reqAsPromise } from "./utils.js";
import { StorageError } from "../common.js";
class QueryTargetWrapper {
constructor(qt) {
this._qt = qt;
}
openKeyCursor(...params) {
try {
return this._qt.openKeyCursor(...params);
} catch(err) {
throw new StorageError("openKeyCursor failed", err);
}
}
openCursor(...params) {
try {
return this._qt.openCursor(...params);
} catch(err) {
throw new StorageError("openCursor failed", err);
}
}
put(...params) {
try {
return this._qt.put(...params);
} catch(err) {
throw new StorageError("put failed", err);
}
}
add(...params) {
try {
return this._qt.add(...params);
} catch(err) {
throw new StorageError("add failed", err);
}
}
get(...params) {
try {
return this._qt.get(...params);
} catch(err) {
throw new StorageError("get failed", err);
}
}
index(...params) {
try {
return this._qt.index(...params);
} catch(err) {
throw new StorageError("index failed", err);
}
}
}
export default class Store extends QueryTarget { export default class Store extends QueryTarget {
constructor(idbStore) { constructor(idbStore) {
super(idbStore); super(new QueryTargetWrapper(idbStore));
} }
get _idbStore() { get _idbStore() {
@ -11,7 +66,7 @@ export default class Store extends QueryTarget {
} }
index(indexName) { index(indexName) {
return new QueryTarget(this._idbStore.index(indexName)); return new QueryTarget(new QueryTargetWrapper(this._idbStore.index(indexName)));
} }
put(value) { put(value) {
@ -21,4 +76,4 @@ export default class Store extends QueryTarget {
add(value) { add(value) {
return reqAsPromise(this._idbStore.add(value)); return reqAsPromise(this._idbStore.add(value));
} }
} }

View file

@ -1,4 +1,5 @@
import EventKey from "../../../room/timeline/EventKey.js"; import EventKey from "../../../room/timeline/EventKey.js";
import { StorageError } from "../../common.js";
import Platform from "../../../../Platform.js"; import Platform from "../../../../Platform.js";
// storage keys are defined to be unsigned 32bit numbers in WebPlatform.js, which is assumed by idb // storage keys are defined to be unsigned 32bit numbers in WebPlatform.js, which is assumed by idb
@ -30,38 +31,42 @@ class Range {
} }
asIDBKeyRange(roomId) { asIDBKeyRange(roomId) {
// only try {
if (this._only) { // only
return IDBKeyRange.only(encodeKey(roomId, this._only.fragmentId, this._only.eventIndex)); if (this._only) {
} return IDBKeyRange.only(encodeKey(roomId, this._only.fragmentId, this._only.eventIndex));
// lowerBound }
// also bound as we don't want to move into another roomId // lowerBound
if (this._lower && !this._upper) { // also bound as we don't want to move into another roomId
return IDBKeyRange.bound( if (this._lower && !this._upper) {
encodeKey(roomId, this._lower.fragmentId, this._lower.eventIndex), return IDBKeyRange.bound(
encodeKey(roomId, this._lower.fragmentId, Platform.maxStorageKey), encodeKey(roomId, this._lower.fragmentId, this._lower.eventIndex),
this._lowerOpen, encodeKey(roomId, this._lower.fragmentId, Platform.maxStorageKey),
false this._lowerOpen,
); false
} );
// upperBound }
// also bound as we don't want to move into another roomId // upperBound
if (!this._lower && this._upper) { // also bound as we don't want to move into another roomId
return IDBKeyRange.bound( if (!this._lower && this._upper) {
encodeKey(roomId, this._upper.fragmentId, Platform.minStorageKey), return IDBKeyRange.bound(
encodeKey(roomId, this._upper.fragmentId, this._upper.eventIndex), encodeKey(roomId, this._upper.fragmentId, Platform.minStorageKey),
false, encodeKey(roomId, this._upper.fragmentId, this._upper.eventIndex),
this._upperOpen false,
); this._upperOpen
} );
// bound }
if (this._lower && this._upper) { // bound
return IDBKeyRange.bound( if (this._lower && this._upper) {
encodeKey(roomId, this._lower.fragmentId, this._lower.eventIndex), return IDBKeyRange.bound(
encodeKey(roomId, this._upper.fragmentId, this._upper.eventIndex), encodeKey(roomId, this._lower.fragmentId, this._lower.eventIndex),
this._lowerOpen, encodeKey(roomId, this._upper.fragmentId, this._upper.eventIndex),
this._upperOpen this._lowerOpen,
); this._upperOpen
);
}
} catch(err) {
throw new StorageError(`IDBKeyRange failed with data: ` + JSON.stringify(this), err);
} }
} }
} }

View file

@ -1,3 +1,4 @@
import { StorageError } from "../../common.js";
import Platform from "../../../../Platform.js"; import Platform from "../../../../Platform.js";
function encodeKey(roomId, fragmentId) { function encodeKey(roomId, fragmentId) {
@ -12,10 +13,14 @@ export default class RoomFragmentStore {
} }
_allRange(roomId) { _allRange(roomId) {
return IDBKeyRange.bound( try {
encodeKey(roomId, Platform.minStorageKey), return IDBKeyRange.bound(
encodeKey(roomId, Platform.maxStorageKey) encodeKey(roomId, Platform.minStorageKey),
); encodeKey(roomId, Platform.maxStorageKey)
);
} catch (err) {
throw new StorageError(`error from IDBKeyRange with roomId ${roomId}`, err);
}
} }
all(roomId) { all(roomId) {

View file

@ -1,4 +1,5 @@
import {txnAsPromise} from "./utils.js"; import {txnAsPromise} from "./utils.js";
import {StorageError} from "../common.js";
import Store from "./store.js"; import Store from "./store.js";
import SessionStore from "./stores/SessionStore.js"; import SessionStore from "./stores/SessionStore.js";
import RoomSummaryStore from "./stores/RoomSummaryStore.js"; import RoomSummaryStore from "./stores/RoomSummaryStore.js";
@ -21,7 +22,7 @@ export default class Transaction {
_idbStore(name) { _idbStore(name) {
if (!this._allowedStoreNames.includes(name)) { if (!this._allowedStoreNames.includes(name)) {
// more specific error? this is a bug, so maybe not ... // more specific error? this is a bug, so maybe not ...
throw new Error(`Invalid store for transaction: ${name}, only ${this._allowedStoreNames.join(", ")} are allowed.`); throw new StorageError(`Invalid store for transaction: ${name}, only ${this._allowedStoreNames.join(", ")} are allowed.`);
} }
return new Store(this._txn.objectStore(name)); return new Store(this._txn.objectStore(name));
} }

View file

@ -1,3 +1,5 @@
import { StorageError } from "../common.js";
export function openDatabase(name, createObjectStore, version) { export function openDatabase(name, createObjectStore, version) {
const req = window.indexedDB.open(name, version); const req = window.indexedDB.open(name, version);
req.onupgradeneeded = (ev) => { req.onupgradeneeded = (ev) => {
@ -8,17 +10,21 @@ export function openDatabase(name, createObjectStore, version) {
return reqAsPromise(req); return reqAsPromise(req);
} }
function wrapError(err) {
return new StorageError(`wrapped DOMException`, err);
}
export function reqAsPromise(req) { export function reqAsPromise(req) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
req.addEventListener("success", event => resolve(event.target.result)); req.addEventListener("success", event => resolve(event.target.result));
req.addEventListener("error", event => reject(event.target.error)); req.addEventListener("error", event => reject(wrapError(event.target.error)));
}); });
} }
export function txnAsPromise(txn) { export function txnAsPromise(txn) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
txn.addEventListener("complete", resolve); txn.addEventListener("complete", resolve);
txn.addEventListener("abort", reject); txn.addEventListener("abort", e => reject(wrapError(e)));
}); });
} }
@ -42,6 +48,8 @@ export function iterateCursor(cursor, processValue) {
cursor.continue(jumpTo); cursor.continue(jumpTo);
} }
}; };
}).catch(err => {
throw new StorageError("iterateCursor failed", err);
}); });
} }
@ -97,7 +105,7 @@ export async function findStoreValue(db, storeName, toCursor, matchesValue) {
} }
}); });
if (!matched) { if (!matched) {
throw new Error("Value not found"); throw new StorageError("Value not found");
} }
return match; return match;
} }

View file

@ -1,8 +1,4 @@
import { import {RequestAbortError} from "./error.js";
RequestAbortError,
HomeServerError,
StorageError
} from "./error.js";
import EventEmitter from "../EventEmitter.js"; import EventEmitter from "../EventEmitter.js";
const INCREMENTAL_TIMEOUT = 30000; const INCREMENTAL_TIMEOUT = 30000;
@ -111,7 +107,8 @@ export default class Sync extends EventEmitter {
await syncTxn.complete(); await syncTxn.complete();
console.info("syncTxn committed!!"); console.info("syncTxn committed!!");
} catch (err) { } catch (err) {
throw new StorageError("unable to commit sync tranaction", err); console.error("unable to commit sync tranaction", err.message);
throw err;
} }
// emit room related events after txn has been closed // emit room related events after txn has been closed
for(let {room, changes} of roomChanges) { for(let {room, changes} of roomChanges) {