diff --git a/doc/INDEXEDDB.md b/doc/INDEXEDDB.md index ebfa6580..317aeb4c 100644 --- a/doc/INDEXEDDB.md +++ b/doc/INDEXEDDB.md @@ -23,6 +23,8 @@ without waiting for any *micro*tasks. See comments about Safari at https://githu Another failure mode perceived in Hydrogen on Safari is that when the (readonly) prepareTxn in sync wasn't awaited to be completed before opening and using the syncTxn. I haven't found any documentation online about this at all. Awaiting prepareTxn.complete() fixed the issue below. It's strange though the put does not fail. +## Diagnose of problem + What is happening below is: - in the sync loop: - we first open a readonly txn on inboundGroupSessions, which we don't use in the example below diff --git a/prototypes/idb-test-safari-close-txn.html b/prototypes/idb-test-safari-close-txn.html new file mode 100644 index 00000000..2d50b831 --- /dev/null +++ b/prototypes/idb-test-safari-close-txn.html @@ -0,0 +1,71 @@ + + + + + + diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 4a825507..f88d1b38 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -185,13 +185,13 @@ export class Session { } const key = await ssssKeyFromCredential(type, credential, this._storage, this._platform, this._olm); // and create session backup, which needs to read from accountData - const readTxn = this._storage.readTxn([ + const readTxn = await this._storage.readTxn([ this._storage.storeNames.accountData, ]); await this._createSessionBackup(key, readTxn); // only after having read a secret, write the key // as we only find out if it was good if the MAC verification succeeds - const writeTxn = this._storage.readWriteTxn([ + const writeTxn = await this._storage.readWriteTxn([ this._storage.storeNames.session, ]); try { @@ -249,7 +249,7 @@ export class Session { /** @internal */ async load(log) { - const txn = this._storage.readTxn([ + const txn = await this._storage.readTxn([ this._storage.storeNames.session, this._storage.storeNames.roomSummary, this._storage.storeNames.roomMembers, @@ -301,7 +301,7 @@ export class Session { async start(lastVersionResponse, log) { if (lastVersionResponse) { // store /versions response - const txn = this._storage.readWriteTxn([ + const txn = await this._storage.readWriteTxn([ this._storage.storeNames.session ]); txn.session.set("serverVersions", lastVersionResponse); @@ -310,7 +310,7 @@ export class Session { } // enable session backup, this requests the latest backup version if (!this._sessionBackup) { - const txn = this._storage.readTxn([ + const txn = await this._storage.readTxn([ this._storage.storeNames.session, this._storage.storeNames.accountData, ]); @@ -323,7 +323,7 @@ export class Session { this._hasSecretStorageKey.set(!!ssssKey); } // restore unfinished operations, like sending out room keys - const opsTxn = this._storage.readWriteTxn([ + const opsTxn = await this._storage.readWriteTxn([ this._storage.storeNames.operations ]); const operations = await opsTxn.operations.getAll(); diff --git a/src/matrix/Sync.js b/src/matrix/Sync.js index 4154f959..73ff0207 100644 --- a/src/matrix/Sync.js +++ b/src/matrix/Sync.js @@ -201,7 +201,7 @@ export class Sync { return rs.room.afterPrepareSync(rs.preparation, log); }))); await log.wrap("write", async log => { - const syncTxn = this._openSyncTxn(); + const syncTxn = await this._openSyncTxn(); try { sessionState.changes = await log.wrap("session", log => this._session.writeSync( response, syncFilterId, sessionState.preparation, syncTxn, log)); @@ -253,7 +253,7 @@ export class Sync { } async _prepareSessionAndRooms(sessionState, roomStates, response, log) { - const prepareTxn = this._openPrepareSyncTxn(); + const prepareTxn = await this._openPrepareSyncTxn(); sessionState.preparation = await log.wrap("session", log => this._session.prepareSync( response, sessionState.lock, prepareTxn, log)); diff --git a/src/matrix/e2ee/Account.js b/src/matrix/e2ee/Account.js index 693d61f8..825333b1 100644 --- a/src/matrix/e2ee/Account.js +++ b/src/matrix/e2ee/Account.js @@ -45,7 +45,7 @@ export class Account { } const pickledAccount = account.pickle(pickleKey); const areDeviceKeysUploaded = false; - const txn = storage.readWriteTxn([ + const txn = await storage.readWriteTxn([ storage.storeNames.session ]); try { @@ -225,7 +225,7 @@ export class Account { } async _updateSessionStorage(storage, callback) { - const txn = storage.readWriteTxn([ + const txn = await storage.readWriteTxn([ storage.storeNames.session ]); try { diff --git a/src/matrix/e2ee/DeviceTracker.js b/src/matrix/e2ee/DeviceTracker.js index a740636c..ed55b79a 100644 --- a/src/matrix/e2ee/DeviceTracker.js +++ b/src/matrix/e2ee/DeviceTracker.js @@ -75,7 +75,7 @@ export class DeviceTracker { } const memberList = await room.loadMemberList(log); try { - const txn = this._storage.readWriteTxn([ + const txn = await this._storage.readWriteTxn([ this._storage.storeNames.roomSummary, this._storage.storeNames.userIdentities, ]); @@ -157,7 +157,7 @@ export class DeviceTracker { }, {log}).response(); const verifiedKeysPerUser = log.wrap("verify", log => this._filterVerifiedDeviceKeys(deviceKeyResponse["device_keys"], log)); - const txn = this._storage.readWriteTxn([ + const txn = await this._storage.readWriteTxn([ this._storage.storeNames.userIdentities, this._storage.storeNames.deviceIdentities, ]); @@ -271,7 +271,7 @@ export class DeviceTracker { * @return {[type]} [description] */ async devicesForTrackedRoom(roomId, hsApi, log) { - const txn = this._storage.readTxn([ + const txn = await this._storage.readTxn([ this._storage.storeNames.roomMembers, this._storage.storeNames.userIdentities, ]); @@ -287,7 +287,7 @@ export class DeviceTracker { } async devicesForRoomMembers(roomId, userIds, hsApi, log) { - const txn = this._storage.readTxn([ + const txn = await this._storage.readTxn([ this._storage.storeNames.userIdentities, ]); return await this._devicesForUserIds(roomId, userIds, txn, hsApi, log); @@ -319,7 +319,7 @@ export class DeviceTracker { queriedDevices = await this._queryKeys(outdatedIdentities.map(i => i.userId), hsApi, log); } - const deviceTxn = this._storage.readTxn([ + const deviceTxn = await this._storage.readTxn([ this._storage.storeNames.deviceIdentities, ]); const devicesPerUser = await Promise.all(upToDateIdentities.map(identity => { diff --git a/src/matrix/e2ee/RoomEncryption.js b/src/matrix/e2ee/RoomEncryption.js index 51accdf3..3ca2e220 100644 --- a/src/matrix/e2ee/RoomEncryption.js +++ b/src/matrix/e2ee/RoomEncryption.js @@ -59,7 +59,7 @@ export class RoomEncryption { const events = entries.filter(e => e.isEncrypted && !e.isDecrypted && e.event).map(e => e.event); const eventsBySession = groupEventsBySession(events); const groups = Array.from(eventsBySession.values()); - const txn = this._storage.readTxn([this._storage.storeNames.inboundGroupSessions]); + const txn = await this._storage.readTxn([this._storage.storeNames.inboundGroupSessions]); const hasSessions = await Promise.all(groups.map(async group => { return this._megolmDecryption.hasSession(this._room.id, group.senderKey, group.sessionId, txn); })); @@ -164,7 +164,7 @@ export class RoomEncryption { return; } // now check which sessions have been received already - const txn = this._storage.readTxn([this._storage.storeNames.inboundGroupSessions]); + const txn = await this._storage.readTxn([this._storage.storeNames.inboundGroupSessions]); await Promise.all(Array.from(eventsBySession).map(async ([key, group]) => { if (await this._megolmDecryption.hasSession(this._room.id, group.senderKey, group.sessionId, txn)) { eventsBySession.delete(key); @@ -211,7 +211,7 @@ export class RoomEncryption { if (roomKey) { let keyIsBestOne = false; try { - const txn = this._storage.readWriteTxn([this._storage.storeNames.inboundGroupSessions]); + const txn = await this._storage.readWriteTxn([this._storage.storeNames.inboundGroupSessions]); try { keyIsBestOne = await this._megolmDecryption.writeRoomKey(roomKey, txn); } catch (err) { @@ -281,7 +281,7 @@ export class RoomEncryption { } async _shareNewRoomKey(roomKeyMessage, hsApi, log) { - let writeOpTxn = this._storage.readWriteTxn([this._storage.storeNames.operations]); + let writeOpTxn = await this._storage.readWriteTxn([this._storage.storeNames.operations]); let operation; try { operation = this._writeRoomKeyShareOperation(roomKeyMessage, null, writeOpTxn); @@ -319,7 +319,7 @@ export class RoomEncryption { this._isFlushingRoomKeyShares = true; try { if (!operations) { - const txn = this._storage.readTxn([this._storage.storeNames.operations]); + const txn = await this._storage.readTxn([this._storage.storeNames.operations]); operations = await txn.operations.getAllByTypeAndScope("share_room_key", this._room.id); } for (const operation of operations) { @@ -355,7 +355,7 @@ export class RoomEncryption { devices = await this._deviceTracker.devicesForTrackedRoom(this._room.id, hsApi, log); const userIds = Array.from(devices.reduce((set, device) => set.add(device.userId), new Set())); operation.userIds = userIds; - const userIdsTxn = this._storage.readWriteTxn([this._storage.storeNames.operations]); + const userIdsTxn = await this._storage.readWriteTxn([this._storage.storeNames.operations]); try { userIdsTxn.operations.update(operation); } catch (err) { @@ -371,7 +371,7 @@ export class RoomEncryption { "m.room_key", operation.roomKeyMessage, devices, hsApi, log)); await log.wrap("send", log => this._sendMessagesToDevices(ENCRYPTED_TYPE, messages, hsApi, log)); - const removeTxn = this._storage.readWriteTxn([this._storage.storeNames.operations]); + const removeTxn = await this._storage.readWriteTxn([this._storage.storeNames.operations]); try { removeTxn.operations.remove(operation.id); } catch (err) { diff --git a/src/matrix/e2ee/megolm/Encryption.js b/src/matrix/e2ee/megolm/Encryption.js index 4cf3791c..838d6082 100644 --- a/src/matrix/e2ee/megolm/Encryption.js +++ b/src/matrix/e2ee/megolm/Encryption.js @@ -46,7 +46,7 @@ export class Encryption { async ensureOutboundSession(roomId, encryptionParams) { let session = new this._olm.OutboundGroupSession(); try { - const txn = this._storage.readWriteTxn([ + const txn = await this._storage.readWriteTxn([ this._storage.storeNames.inboundGroupSessions, this._storage.storeNames.outboundGroupSessions, ]); @@ -104,7 +104,7 @@ export class Encryption { async encrypt(roomId, type, content, encryptionParams) { let session = new this._olm.OutboundGroupSession(); try { - const txn = this._storage.readWriteTxn([ + const txn = await this._storage.readWriteTxn([ this._storage.storeNames.inboundGroupSessions, this._storage.storeNames.outboundGroupSessions, ]); diff --git a/src/matrix/e2ee/olm/Encryption.js b/src/matrix/e2ee/olm/Encryption.js index c26c0239..7fe4c318 100644 --- a/src/matrix/e2ee/olm/Encryption.js +++ b/src/matrix/e2ee/olm/Encryption.js @@ -101,7 +101,7 @@ export class Encryption { } async _findExistingSessions(devices) { - const txn = this._storage.readTxn([this._storage.storeNames.olmSessions]); + const txn = await this._storage.readTxn([this._storage.storeNames.olmSessions]); const sessionIdsForDevice = await Promise.all(devices.map(async device => { return await txn.olmSessions.getSessionIds(device.curve25519Key); })); @@ -215,7 +215,7 @@ export class Encryption { } async _loadSessions(encryptionTargets) { - const txn = this._storage.readTxn([this._storage.storeNames.olmSessions]); + const txn = await this._storage.readTxn([this._storage.storeNames.olmSessions]); // given we run loading in parallel, there might still be some // storage requests that will finish later once one has failed. // those should not allocate a session anymore. @@ -241,7 +241,7 @@ export class Encryption { } async _storeSessions(encryptionTargets, timestamp) { - const txn = this._storage.readWriteTxn([this._storage.storeNames.olmSessions]); + const txn = await this._storage.readWriteTxn([this._storage.storeNames.olmSessions]); try { for (const target of encryptionTargets) { const sessionEntry = createSessionEntry( diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index 900384e0..5bab0a40 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -79,7 +79,7 @@ export class Room extends EventEmitter { if (!this._roomEncryption) { return; } - const txn = this._storage.readTxn([ + const txn = await this._storage.readTxn([ this._storage.storeNames.timelineEvents, this._storage.storeNames.inboundGroupSessions, ]); @@ -118,7 +118,7 @@ export class Room extends EventEmitter { _decryptEntries(source, entries, inboundSessionTxn = null) { const request = new DecryptionRequest(async r => { if (!inboundSessionTxn) { - inboundSessionTxn = this._storage.readTxn([this._storage.storeNames.inboundGroupSessions]); + inboundSessionTxn = await this._storage.readTxn([this._storage.storeNames.inboundGroupSessions]); } if (r.cancelled) return; const events = entries.filter(entry => { @@ -135,7 +135,7 @@ export class Room extends EventEmitter { // read to fetch devices if timeline is open stores.push(this._storage.storeNames.deviceIdentities); } - const writeTxn = this._storage.readWriteTxn(stores); + const writeTxn = await this._storage.readWriteTxn(stores); let decryption; try { decryption = await changes.write(writeTxn); @@ -472,7 +472,7 @@ export class Room extends EventEmitter { } }, {log}).response(); - const txn = this._storage.readWriteTxn([ + const txn = await this._storage.readWriteTxn([ this._storage.storeNames.pendingEvents, this._storage.storeNames.timelineEvents, this._storage.storeNames.timelineFragments, @@ -584,7 +584,7 @@ export class Room extends EventEmitter { async _getLastEventId() { const lastKey = this._syncWriter.lastMessageKey; if (lastKey) { - const txn = this._storage.readTxn([ + const txn = await this._storage.readTxn([ this._storage.storeNames.timelineEvents, ]); const eventEntry = await txn.timelineEvents.get(this._roomId, lastKey); @@ -607,7 +607,7 @@ export class Room extends EventEmitter { if (this.isUnread || this.notificationCount) { return await this._platform.logger.wrapOrRun(log, "clearUnread", async log => { log.set("id", this.id); - const txn = this._storage.readWriteTxn([ + const txn = await this._storage.readWriteTxn([ this._storage.storeNames.roomSummary, ]); let data; @@ -706,7 +706,7 @@ export class Room extends EventEmitter { if (this.isEncrypted) { stores.push(this._storage.storeNames.inboundGroupSessions); } - const txn = this._storage.readTxn(stores); + const txn = await this._storage.readTxn(stores); const storageEntry = await txn.timelineEvents.getByEventId(this._roomId, eventId); if (storageEntry) { const entry = new EventEntry(storageEntry, this._fragmentIdComparer); diff --git a/src/matrix/room/RoomSummary.js b/src/matrix/room/RoomSummary.js index 8c7c8576..759b275a 100644 --- a/src/matrix/room/RoomSummary.js +++ b/src/matrix/room/RoomSummary.js @@ -251,7 +251,7 @@ export class RoomSummary { if (data === this._data) { return false; } - const txn = storage.readWriteTxn([ + const txn = await storage.readWriteTxn([ storage.storeNames.roomSummary, ]); try { diff --git a/src/matrix/room/members/load.js b/src/matrix/room/members/load.js index 2b8853de..52ae58c4 100644 --- a/src/matrix/room/members/load.js +++ b/src/matrix/room/members/load.js @@ -18,7 +18,7 @@ limitations under the License. import {RoomMember} from "./RoomMember.js"; async function loadMembers({roomId, storage}) { - const txn = storage.readTxn([ + const txn = await storage.readTxn([ storage.storeNames.roomMembers, ]); const memberDatas = await txn.roomMembers.getAll(roomId); @@ -33,7 +33,7 @@ async function fetchMembers({summary, syncToken, roomId, hsApi, storage, setChan const memberResponse = await hsApi.members(roomId, {at: syncToken}, {log}).response(); - const txn = storage.readWriteTxn([ + const txn = await storage.readWriteTxn([ storage.storeNames.roomSummary, storage.storeNames.roomMembers, ]); diff --git a/src/matrix/room/sending/SendQueue.js b/src/matrix/room/sending/SendQueue.js index 5b0bf7f9..2d74d93e 100644 --- a/src/matrix/room/sending/SendQueue.js +++ b/src/matrix/room/sending/SendQueue.js @@ -129,7 +129,7 @@ export class SendQueue { async _removeEvent(pendingEvent) { const idx = this._pendingEvents.array.indexOf(pendingEvent); if (idx !== -1) { - const txn = this._storage.readWriteTxn([this._storage.storeNames.pendingEvents]); + const txn = await this._storage.readWriteTxn([this._storage.storeNames.pendingEvents]); try { txn.pendingEvents.remove(pendingEvent.roomId, pendingEvent.queueIndex); } catch (err) { @@ -185,7 +185,7 @@ export class SendQueue { } async _tryUpdateEvent(pendingEvent) { - const txn = this._storage.readWriteTxn([this._storage.storeNames.pendingEvents]); + const txn = await this._storage.readWriteTxn([this._storage.storeNames.pendingEvents]); try { // pendingEvent might have been removed already here // by a racing remote echo, so check first so we don't recreate it @@ -200,7 +200,7 @@ export class SendQueue { } async _createAndStoreEvent(eventType, content, attachments) { - const txn = this._storage.readWriteTxn([this._storage.storeNames.pendingEvents]); + const txn = await this._storage.readWriteTxn([this._storage.storeNames.pendingEvents]); let pendingEvent; try { const pendingEventsStore = txn.pendingEvents; diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 6389461a..9abd5b88 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -46,7 +46,7 @@ export class Timeline { /** @package */ async load(user) { - const txn = this._storage.readTxn(this._timelineReader.readTxnStores.concat(this._storage.storeNames.roomMembers)); + const txn = await this._storage.readTxn(this._timelineReader.readTxnStores.concat(this._storage.storeNames.roomMembers)); const memberData = await txn.roomMembers.get(this._roomId, user.id); this._ownMember = new RoomMember(memberData); // it should be fine to not update the local entries, diff --git a/src/matrix/room/timeline/persistence/TimelineReader.js b/src/matrix/room/timeline/persistence/TimelineReader.js index b4e18cf4..dc759cc5 100644 --- a/src/matrix/room/timeline/persistence/TimelineReader.js +++ b/src/matrix/room/timeline/persistence/TimelineReader.js @@ -108,14 +108,14 @@ export class TimelineReader { readFrom(eventKey, direction, amount) { return new ReaderRequest(async r => { - const txn = this._storage.readTxn(this.readTxnStores); + const txn = await this._storage.readTxn(this.readTxnStores); return await this._readFrom(eventKey, direction, amount, r, txn); }); } readFromEnd(amount, existingTxn = null) { return new ReaderRequest(async r => { - const txn = existingTxn || this._storage.readTxn(this.readTxnStores); + const txn = existingTxn || await this._storage.readTxn(this.readTxnStores); const liveFragment = await txn.timelineFragments.liveFragment(this._roomId); let entries; // room hasn't been synced yet diff --git a/src/matrix/ssss/index.js b/src/matrix/ssss/index.js index 66ea3ea0..34ee63b8 100644 --- a/src/matrix/ssss/index.js +++ b/src/matrix/ssss/index.js @@ -19,7 +19,7 @@ import {keyFromPassphrase} from "./passphrase.js"; import {keyFromRecoveryKey} from "./recoveryKey.js"; async function readDefaultKeyDescription(storage) { - const txn = storage.readTxn([ + const txn = await storage.readTxn([ storage.storeNames.accountData ]); const defaultKeyEvent = await txn.accountData.get("m.secret_storage.default_key"); diff --git a/src/matrix/storage/idb/Storage.js b/src/matrix/storage/idb/Storage.js index 03c2c8ef..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; @@ -34,20 +38,30 @@ export class Storage { } } - readTxn(storeNames) { + async readTxn(storeNames) { 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); } } - readWriteTxn(storeNames) { + async readWriteTxn(storeNames) { 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; +}