diff --git a/src/matrix/storage/idb/Storage.js b/src/matrix/storage/idb/Storage.js index 9c79b92f..93d55f34 100644 --- a/src/matrix/storage/idb/Storage.js +++ b/src/matrix/storage/idb/Storage.js @@ -16,10 +16,14 @@ limitations under the License. import {Transaction} from "./Transaction.js"; import { STORE_NAMES, StorageError } from "../common.js"; +import { reqAsPromise } from "./utils.js"; + +const WEBKITEARLYCLOSETXNBUG_BOGUS_KEY = "782rh281re38-boguskey"; export class Storage { - constructor(idbDatabase) { + constructor(idbDatabase, hasWebkitEarlyCloseTxnBug) { this._db = idbDatabase; + this._hasWebkitEarlyCloseTxnBug = hasWebkitEarlyCloseTxnBug; const nameMap = STORE_NAMES.reduce((nameMap, name) => { nameMap[name] = name; return nameMap; @@ -38,6 +42,11 @@ export class Storage { this._validateStoreNames(storeNames); try { const txn = this._db.transaction(storeNames, "readonly"); + // https://bugs.webkit.org/show_bug.cgi?id=222746 workaround, + // await a bogus idb request on the new txn so it doesn't close early if we await a microtask first + if (this._hasWebkitEarlyCloseTxnBug) { + await reqAsPromise(txn.objectStore(storeNames[0]).get(WEBKITEARLYCLOSETXNBUG_BOGUS_KEY)); + } return new Transaction(txn, storeNames); } catch(err) { throw new StorageError("readTxn failed", err); @@ -48,6 +57,11 @@ export class Storage { this._validateStoreNames(storeNames); try { const txn = this._db.transaction(storeNames, "readwrite"); + // https://bugs.webkit.org/show_bug.cgi?id=222746 workaround, + // await a bogus idb request on the new txn so it doesn't close early if we await a microtask first + if (this._hasWebkitEarlyCloseTxnBug) { + await reqAsPromise(txn.objectStore(storeNames[0]).get(WEBKITEARLYCLOSETXNBUG_BOGUS_KEY)); + } return new Transaction(txn, storeNames); } catch(err) { throw new StorageError("readWriteTxn failed", err); diff --git a/src/matrix/storage/idb/StorageFactory.js b/src/matrix/storage/idb/StorageFactory.js index b4411469..13860f67 100644 --- a/src/matrix/storage/idb/StorageFactory.js +++ b/src/matrix/storage/idb/StorageFactory.js @@ -18,6 +18,7 @@ import {Storage} from "./Storage.js"; import { openDatabase, reqAsPromise } from "./utils.js"; import { exportSession, importSession } from "./export.js"; import { schema } from "./schema.js"; +import { detectWebkitEarlyCloseTxnBug } from "./quirks.js"; const sessionName = sessionId => `hydrogen_session_${sessionId}`; const openDatabaseWithSessionId = sessionId => openDatabase(sessionName(sessionId), createStores, schema.length); @@ -50,8 +51,10 @@ export class StorageFactory { console.warn("no persisted storage, database can be evicted by browser"); } }); + + const hasWebkitEarlyCloseTxnBug = await detectWebkitEarlyCloseTxnBug(); const db = await openDatabaseWithSessionId(sessionId); - return new Storage(db); + return new Storage(db, hasWebkitEarlyCloseTxnBug); } delete(sessionId) { diff --git a/src/matrix/storage/idb/quirks.js b/src/matrix/storage/idb/quirks.js new file mode 100644 index 00000000..b37750f4 --- /dev/null +++ b/src/matrix/storage/idb/quirks.js @@ -0,0 +1,41 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + + +import {openDatabase, txnAsPromise, reqAsPromise} from "./utils.js"; + +// filed as https://bugs.webkit.org/show_bug.cgi?id=222746 +export async function detectWebkitEarlyCloseTxnBug() { + const dbName = "hydrogen_webkit_test_inactive_txn_bug"; + try { + const db = await openDatabase(dbName, db => { + db.createObjectStore("test", {keyPath: "key"}); + }, 1); + const readTxn = db.transaction(["test"], "readonly"); + await reqAsPromise(readTxn.objectStore("test").get("somekey")); + // schedule a macro task in between the two txns + await new Promise(r => setTimeout(r, 0)); + const writeTxn = db.transaction(["test"], "readwrite"); + await Promise.resolve(); + writeTxn.objectStore("test").add({key: "somekey", value: "foo"}); + await txnAsPromise(writeTxn); + } catch (err) { + if (err.name === "TransactionInactiveError") { + return true; + } + } + return false; +}