Merge pull request #259 from vector-im/bwindels/fix-258
Await bogus idb request on webkit browsers to prevent transaction closing early
This commit is contained in:
commit
4b075e582e
19 changed files with 179 additions and 48 deletions
|
@ -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
|
||||
|
|
71
prototypes/idb-test-safari-close-txn.html
Normal file
71
prototypes/idb-test-safari-close-txn.html
Normal file
|
@ -0,0 +1,71 @@
|
|||
<html>
|
||||
<head><meta charset="utf-8"></head>
|
||||
<body>
|
||||
<script type="text/javascript">
|
||||
|
||||
const log = (...params) => {
|
||||
document.write(params.join(" ")+"<br>");
|
||||
};
|
||||
|
||||
function reqAsPromise(req) {
|
||||
return new Promise((resolve, reject) => {
|
||||
req.onsuccess = () => resolve(req.result);
|
||||
req.onerror = (err) => reject(err);
|
||||
});
|
||||
}
|
||||
|
||||
function txnAsPromise(txn) {
|
||||
return new Promise((resolve, reject) => {
|
||||
txn.addEventListener("complete", resolve);
|
||||
txn.addEventListener("abort", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function openDatabase(name, createObjectStore, version) {
|
||||
const req = indexedDB.open(name, version);
|
||||
req.onupgradeneeded = (ev) => {
|
||||
const db = ev.target.result;
|
||||
const txn = ev.target.transaction;
|
||||
const oldVersion = ev.oldVersion;
|
||||
createObjectStore(db, txn, oldVersion, version);
|
||||
};
|
||||
return reqAsPromise(req);
|
||||
}
|
||||
|
||||
async function detectWebkitEarlyCloseTxnBug() {
|
||||
const dbName = "webkit_test_inactive_txn_" + Math.random() * Number.MAX_SAFE_INTEGER;
|
||||
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;
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
indexedDB.deleteDatabase(dbName);
|
||||
} catch (err) {}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
if (await detectWebkitEarlyCloseTxnBug()) {
|
||||
log("the test failed, your browser seems to have the bug");
|
||||
} else {
|
||||
log("the test succeeded, your browser seems fine");
|
||||
}
|
||||
})();
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -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();
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
]);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
]);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
41
src/matrix/storage/idb/quirks.js
Normal file
41
src/matrix/storage/idb/quirks.js
Normal file
|
@ -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;
|
||||
}
|
Reference in a new issue