From e0d14207ac38e2d6951cbc711bd1ca2b91351afc Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 4 Mar 2021 19:47:02 +0100 Subject: [PATCH 1/4] make opening a txn async again as we'll need to await a bogus request first thing after opening the txn funny enough, we originally made it sync to accommodate the same bug in safari, but that didn't prevent any microtask being awaited before scheduling a request in the calling code closing the txn. We'll await a bogus request within the transaction class now so it doesn't depend on the calling code --- src/matrix/Session.js | 12 ++++++------ src/matrix/Sync.js | 4 ++-- src/matrix/e2ee/Account.js | 4 ++-- src/matrix/e2ee/DeviceTracker.js | 10 +++++----- src/matrix/e2ee/RoomEncryption.js | 14 +++++++------- src/matrix/e2ee/megolm/Encryption.js | 4 ++-- src/matrix/e2ee/olm/Encryption.js | 6 +++--- src/matrix/room/Room.js | 14 +++++++------- src/matrix/room/RoomSummary.js | 2 +- src/matrix/room/members/load.js | 4 ++-- src/matrix/room/sending/SendQueue.js | 6 +++--- src/matrix/room/timeline/Timeline.js | 2 +- .../room/timeline/persistence/TimelineReader.js | 4 ++-- src/matrix/ssss/index.js | 2 +- src/matrix/storage/idb/Storage.js | 4 ++-- 15 files changed, 46 insertions(+), 46 deletions(-) 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..9c79b92f 100644 --- a/src/matrix/storage/idb/Storage.js +++ b/src/matrix/storage/idb/Storage.js @@ -34,7 +34,7 @@ export class Storage { } } - readTxn(storeNames) { + async readTxn(storeNames) { this._validateStoreNames(storeNames); try { const txn = this._db.transaction(storeNames, "readonly"); @@ -44,7 +44,7 @@ export class Storage { } } - readWriteTxn(storeNames) { + async readWriteTxn(storeNames) { this._validateStoreNames(storeNames); try { const txn = this._db.transaction(storeNames, "readwrite"); From 932d26ed8c44ef7a36c519e16487394ed5c4f4d2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 4 Mar 2021 19:49:13 +0100 Subject: [PATCH 2/4] detect the webkit bug, and await a bogus request when opening a txn --- src/matrix/storage/idb/Storage.js | 16 ++++++++- src/matrix/storage/idb/StorageFactory.js | 5 ++- src/matrix/storage/idb/quirks.js | 41 ++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 src/matrix/storage/idb/quirks.js 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; +} From ea66b75b86057d820a292a907d82ab4db7ff4341 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 4 Mar 2021 19:50:02 +0100 Subject: [PATCH 3/4] prototype to detect webkit bug --- prototypes/idb-test-safari-close-txn.html | 71 +++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 prototypes/idb-test-safari-close-txn.html 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 @@ + + + + + + From 571bedd76d6625a05988db2c280d0b8d20c376d2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 4 Mar 2021 19:50:23 +0100 Subject: [PATCH 4/4] doc improvement --- doc/INDEXEDDB.md | 2 ++ 1 file changed, 2 insertions(+) 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