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.
|
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.
|
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:
|
What is happening below is:
|
||||||
- in the sync loop:
|
- in the sync loop:
|
||||||
- we first open a readonly txn on inboundGroupSessions, which we don't use in the example below
|
- 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);
|
const key = await ssssKeyFromCredential(type, credential, this._storage, this._platform, this._olm);
|
||||||
// and create session backup, which needs to read from accountData
|
// 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,
|
this._storage.storeNames.accountData,
|
||||||
]);
|
]);
|
||||||
await this._createSessionBackup(key, readTxn);
|
await this._createSessionBackup(key, readTxn);
|
||||||
// only after having read a secret, write the key
|
// only after having read a secret, write the key
|
||||||
// as we only find out if it was good if the MAC verification succeeds
|
// 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,
|
this._storage.storeNames.session,
|
||||||
]);
|
]);
|
||||||
try {
|
try {
|
||||||
|
@ -249,7 +249,7 @@ export class Session {
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
async load(log) {
|
async load(log) {
|
||||||
const txn = this._storage.readTxn([
|
const txn = await this._storage.readTxn([
|
||||||
this._storage.storeNames.session,
|
this._storage.storeNames.session,
|
||||||
this._storage.storeNames.roomSummary,
|
this._storage.storeNames.roomSummary,
|
||||||
this._storage.storeNames.roomMembers,
|
this._storage.storeNames.roomMembers,
|
||||||
|
@ -301,7 +301,7 @@ export class Session {
|
||||||
async start(lastVersionResponse, log) {
|
async start(lastVersionResponse, log) {
|
||||||
if (lastVersionResponse) {
|
if (lastVersionResponse) {
|
||||||
// store /versions response
|
// store /versions response
|
||||||
const txn = this._storage.readWriteTxn([
|
const txn = await this._storage.readWriteTxn([
|
||||||
this._storage.storeNames.session
|
this._storage.storeNames.session
|
||||||
]);
|
]);
|
||||||
txn.session.set("serverVersions", lastVersionResponse);
|
txn.session.set("serverVersions", lastVersionResponse);
|
||||||
|
@ -310,7 +310,7 @@ export class Session {
|
||||||
}
|
}
|
||||||
// enable session backup, this requests the latest backup version
|
// enable session backup, this requests the latest backup version
|
||||||
if (!this._sessionBackup) {
|
if (!this._sessionBackup) {
|
||||||
const txn = this._storage.readTxn([
|
const txn = await this._storage.readTxn([
|
||||||
this._storage.storeNames.session,
|
this._storage.storeNames.session,
|
||||||
this._storage.storeNames.accountData,
|
this._storage.storeNames.accountData,
|
||||||
]);
|
]);
|
||||||
|
@ -323,7 +323,7 @@ export class Session {
|
||||||
this._hasSecretStorageKey.set(!!ssssKey);
|
this._hasSecretStorageKey.set(!!ssssKey);
|
||||||
}
|
}
|
||||||
// restore unfinished operations, like sending out room keys
|
// restore unfinished operations, like sending out room keys
|
||||||
const opsTxn = this._storage.readWriteTxn([
|
const opsTxn = await this._storage.readWriteTxn([
|
||||||
this._storage.storeNames.operations
|
this._storage.storeNames.operations
|
||||||
]);
|
]);
|
||||||
const operations = await opsTxn.operations.getAll();
|
const operations = await opsTxn.operations.getAll();
|
||||||
|
|
|
@ -201,7 +201,7 @@ export class Sync {
|
||||||
return rs.room.afterPrepareSync(rs.preparation, log);
|
return rs.room.afterPrepareSync(rs.preparation, log);
|
||||||
})));
|
})));
|
||||||
await log.wrap("write", async log => {
|
await log.wrap("write", async log => {
|
||||||
const syncTxn = this._openSyncTxn();
|
const syncTxn = await this._openSyncTxn();
|
||||||
try {
|
try {
|
||||||
sessionState.changes = await log.wrap("session", log => this._session.writeSync(
|
sessionState.changes = await log.wrap("session", log => this._session.writeSync(
|
||||||
response, syncFilterId, sessionState.preparation, syncTxn, log));
|
response, syncFilterId, sessionState.preparation, syncTxn, log));
|
||||||
|
@ -253,7 +253,7 @@ export class Sync {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _prepareSessionAndRooms(sessionState, roomStates, response, log) {
|
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(
|
sessionState.preparation = await log.wrap("session", log => this._session.prepareSync(
|
||||||
response, sessionState.lock, prepareTxn, log));
|
response, sessionState.lock, prepareTxn, log));
|
||||||
|
|
||||||
|
|
|
@ -45,7 +45,7 @@ export class Account {
|
||||||
}
|
}
|
||||||
const pickledAccount = account.pickle(pickleKey);
|
const pickledAccount = account.pickle(pickleKey);
|
||||||
const areDeviceKeysUploaded = false;
|
const areDeviceKeysUploaded = false;
|
||||||
const txn = storage.readWriteTxn([
|
const txn = await storage.readWriteTxn([
|
||||||
storage.storeNames.session
|
storage.storeNames.session
|
||||||
]);
|
]);
|
||||||
try {
|
try {
|
||||||
|
@ -225,7 +225,7 @@ export class Account {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _updateSessionStorage(storage, callback) {
|
async _updateSessionStorage(storage, callback) {
|
||||||
const txn = storage.readWriteTxn([
|
const txn = await storage.readWriteTxn([
|
||||||
storage.storeNames.session
|
storage.storeNames.session
|
||||||
]);
|
]);
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -75,7 +75,7 @@ export class DeviceTracker {
|
||||||
}
|
}
|
||||||
const memberList = await room.loadMemberList(log);
|
const memberList = await room.loadMemberList(log);
|
||||||
try {
|
try {
|
||||||
const txn = this._storage.readWriteTxn([
|
const txn = await this._storage.readWriteTxn([
|
||||||
this._storage.storeNames.roomSummary,
|
this._storage.storeNames.roomSummary,
|
||||||
this._storage.storeNames.userIdentities,
|
this._storage.storeNames.userIdentities,
|
||||||
]);
|
]);
|
||||||
|
@ -157,7 +157,7 @@ export class DeviceTracker {
|
||||||
}, {log}).response();
|
}, {log}).response();
|
||||||
|
|
||||||
const verifiedKeysPerUser = log.wrap("verify", log => this._filterVerifiedDeviceKeys(deviceKeyResponse["device_keys"], log));
|
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.userIdentities,
|
||||||
this._storage.storeNames.deviceIdentities,
|
this._storage.storeNames.deviceIdentities,
|
||||||
]);
|
]);
|
||||||
|
@ -271,7 +271,7 @@ export class DeviceTracker {
|
||||||
* @return {[type]} [description]
|
* @return {[type]} [description]
|
||||||
*/
|
*/
|
||||||
async devicesForTrackedRoom(roomId, hsApi, log) {
|
async devicesForTrackedRoom(roomId, hsApi, log) {
|
||||||
const txn = this._storage.readTxn([
|
const txn = await this._storage.readTxn([
|
||||||
this._storage.storeNames.roomMembers,
|
this._storage.storeNames.roomMembers,
|
||||||
this._storage.storeNames.userIdentities,
|
this._storage.storeNames.userIdentities,
|
||||||
]);
|
]);
|
||||||
|
@ -287,7 +287,7 @@ export class DeviceTracker {
|
||||||
}
|
}
|
||||||
|
|
||||||
async devicesForRoomMembers(roomId, userIds, hsApi, log) {
|
async devicesForRoomMembers(roomId, userIds, hsApi, log) {
|
||||||
const txn = this._storage.readTxn([
|
const txn = await this._storage.readTxn([
|
||||||
this._storage.storeNames.userIdentities,
|
this._storage.storeNames.userIdentities,
|
||||||
]);
|
]);
|
||||||
return await this._devicesForUserIds(roomId, userIds, txn, hsApi, log);
|
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);
|
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,
|
this._storage.storeNames.deviceIdentities,
|
||||||
]);
|
]);
|
||||||
const devicesPerUser = await Promise.all(upToDateIdentities.map(identity => {
|
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 events = entries.filter(e => e.isEncrypted && !e.isDecrypted && e.event).map(e => e.event);
|
||||||
const eventsBySession = groupEventsBySession(events);
|
const eventsBySession = groupEventsBySession(events);
|
||||||
const groups = Array.from(eventsBySession.values());
|
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 => {
|
const hasSessions = await Promise.all(groups.map(async group => {
|
||||||
return this._megolmDecryption.hasSession(this._room.id, group.senderKey, group.sessionId, txn);
|
return this._megolmDecryption.hasSession(this._room.id, group.senderKey, group.sessionId, txn);
|
||||||
}));
|
}));
|
||||||
|
@ -164,7 +164,7 @@ export class RoomEncryption {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// now check which sessions have been received already
|
// 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]) => {
|
await Promise.all(Array.from(eventsBySession).map(async ([key, group]) => {
|
||||||
if (await this._megolmDecryption.hasSession(this._room.id, group.senderKey, group.sessionId, txn)) {
|
if (await this._megolmDecryption.hasSession(this._room.id, group.senderKey, group.sessionId, txn)) {
|
||||||
eventsBySession.delete(key);
|
eventsBySession.delete(key);
|
||||||
|
@ -211,7 +211,7 @@ export class RoomEncryption {
|
||||||
if (roomKey) {
|
if (roomKey) {
|
||||||
let keyIsBestOne = false;
|
let keyIsBestOne = false;
|
||||||
try {
|
try {
|
||||||
const txn = this._storage.readWriteTxn([this._storage.storeNames.inboundGroupSessions]);
|
const txn = await this._storage.readWriteTxn([this._storage.storeNames.inboundGroupSessions]);
|
||||||
try {
|
try {
|
||||||
keyIsBestOne = await this._megolmDecryption.writeRoomKey(roomKey, txn);
|
keyIsBestOne = await this._megolmDecryption.writeRoomKey(roomKey, txn);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -281,7 +281,7 @@ export class RoomEncryption {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _shareNewRoomKey(roomKeyMessage, hsApi, log) {
|
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;
|
let operation;
|
||||||
try {
|
try {
|
||||||
operation = this._writeRoomKeyShareOperation(roomKeyMessage, null, writeOpTxn);
|
operation = this._writeRoomKeyShareOperation(roomKeyMessage, null, writeOpTxn);
|
||||||
|
@ -319,7 +319,7 @@ export class RoomEncryption {
|
||||||
this._isFlushingRoomKeyShares = true;
|
this._isFlushingRoomKeyShares = true;
|
||||||
try {
|
try {
|
||||||
if (!operations) {
|
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);
|
operations = await txn.operations.getAllByTypeAndScope("share_room_key", this._room.id);
|
||||||
}
|
}
|
||||||
for (const operation of operations) {
|
for (const operation of operations) {
|
||||||
|
@ -355,7 +355,7 @@ export class RoomEncryption {
|
||||||
devices = await this._deviceTracker.devicesForTrackedRoom(this._room.id, hsApi, log);
|
devices = await this._deviceTracker.devicesForTrackedRoom(this._room.id, hsApi, log);
|
||||||
const userIds = Array.from(devices.reduce((set, device) => set.add(device.userId), new Set()));
|
const userIds = Array.from(devices.reduce((set, device) => set.add(device.userId), new Set()));
|
||||||
operation.userIds = userIds;
|
operation.userIds = userIds;
|
||||||
const userIdsTxn = this._storage.readWriteTxn([this._storage.storeNames.operations]);
|
const userIdsTxn = await this._storage.readWriteTxn([this._storage.storeNames.operations]);
|
||||||
try {
|
try {
|
||||||
userIdsTxn.operations.update(operation);
|
userIdsTxn.operations.update(operation);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -371,7 +371,7 @@ export class RoomEncryption {
|
||||||
"m.room_key", operation.roomKeyMessage, devices, hsApi, log));
|
"m.room_key", operation.roomKeyMessage, devices, hsApi, log));
|
||||||
await log.wrap("send", log => this._sendMessagesToDevices(ENCRYPTED_TYPE, messages, 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 {
|
try {
|
||||||
removeTxn.operations.remove(operation.id);
|
removeTxn.operations.remove(operation.id);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -46,7 +46,7 @@ export class Encryption {
|
||||||
async ensureOutboundSession(roomId, encryptionParams) {
|
async ensureOutboundSession(roomId, encryptionParams) {
|
||||||
let session = new this._olm.OutboundGroupSession();
|
let session = new this._olm.OutboundGroupSession();
|
||||||
try {
|
try {
|
||||||
const txn = this._storage.readWriteTxn([
|
const txn = await this._storage.readWriteTxn([
|
||||||
this._storage.storeNames.inboundGroupSessions,
|
this._storage.storeNames.inboundGroupSessions,
|
||||||
this._storage.storeNames.outboundGroupSessions,
|
this._storage.storeNames.outboundGroupSessions,
|
||||||
]);
|
]);
|
||||||
|
@ -104,7 +104,7 @@ export class Encryption {
|
||||||
async encrypt(roomId, type, content, encryptionParams) {
|
async encrypt(roomId, type, content, encryptionParams) {
|
||||||
let session = new this._olm.OutboundGroupSession();
|
let session = new this._olm.OutboundGroupSession();
|
||||||
try {
|
try {
|
||||||
const txn = this._storage.readWriteTxn([
|
const txn = await this._storage.readWriteTxn([
|
||||||
this._storage.storeNames.inboundGroupSessions,
|
this._storage.storeNames.inboundGroupSessions,
|
||||||
this._storage.storeNames.outboundGroupSessions,
|
this._storage.storeNames.outboundGroupSessions,
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -101,7 +101,7 @@ export class Encryption {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _findExistingSessions(devices) {
|
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 => {
|
const sessionIdsForDevice = await Promise.all(devices.map(async device => {
|
||||||
return await txn.olmSessions.getSessionIds(device.curve25519Key);
|
return await txn.olmSessions.getSessionIds(device.curve25519Key);
|
||||||
}));
|
}));
|
||||||
|
@ -215,7 +215,7 @@ export class Encryption {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _loadSessions(encryptionTargets) {
|
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
|
// given we run loading in parallel, there might still be some
|
||||||
// storage requests that will finish later once one has failed.
|
// storage requests that will finish later once one has failed.
|
||||||
// those should not allocate a session anymore.
|
// those should not allocate a session anymore.
|
||||||
|
@ -241,7 +241,7 @@ export class Encryption {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _storeSessions(encryptionTargets, timestamp) {
|
async _storeSessions(encryptionTargets, timestamp) {
|
||||||
const txn = this._storage.readWriteTxn([this._storage.storeNames.olmSessions]);
|
const txn = await this._storage.readWriteTxn([this._storage.storeNames.olmSessions]);
|
||||||
try {
|
try {
|
||||||
for (const target of encryptionTargets) {
|
for (const target of encryptionTargets) {
|
||||||
const sessionEntry = createSessionEntry(
|
const sessionEntry = createSessionEntry(
|
||||||
|
|
|
@ -79,7 +79,7 @@ export class Room extends EventEmitter {
|
||||||
if (!this._roomEncryption) {
|
if (!this._roomEncryption) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const txn = this._storage.readTxn([
|
const txn = await this._storage.readTxn([
|
||||||
this._storage.storeNames.timelineEvents,
|
this._storage.storeNames.timelineEvents,
|
||||||
this._storage.storeNames.inboundGroupSessions,
|
this._storage.storeNames.inboundGroupSessions,
|
||||||
]);
|
]);
|
||||||
|
@ -118,7 +118,7 @@ export class Room extends EventEmitter {
|
||||||
_decryptEntries(source, entries, inboundSessionTxn = null) {
|
_decryptEntries(source, entries, inboundSessionTxn = null) {
|
||||||
const request = new DecryptionRequest(async r => {
|
const request = new DecryptionRequest(async r => {
|
||||||
if (!inboundSessionTxn) {
|
if (!inboundSessionTxn) {
|
||||||
inboundSessionTxn = this._storage.readTxn([this._storage.storeNames.inboundGroupSessions]);
|
inboundSessionTxn = await this._storage.readTxn([this._storage.storeNames.inboundGroupSessions]);
|
||||||
}
|
}
|
||||||
if (r.cancelled) return;
|
if (r.cancelled) return;
|
||||||
const events = entries.filter(entry => {
|
const events = entries.filter(entry => {
|
||||||
|
@ -135,7 +135,7 @@ export class Room extends EventEmitter {
|
||||||
// read to fetch devices if timeline is open
|
// read to fetch devices if timeline is open
|
||||||
stores.push(this._storage.storeNames.deviceIdentities);
|
stores.push(this._storage.storeNames.deviceIdentities);
|
||||||
}
|
}
|
||||||
const writeTxn = this._storage.readWriteTxn(stores);
|
const writeTxn = await this._storage.readWriteTxn(stores);
|
||||||
let decryption;
|
let decryption;
|
||||||
try {
|
try {
|
||||||
decryption = await changes.write(writeTxn);
|
decryption = await changes.write(writeTxn);
|
||||||
|
@ -472,7 +472,7 @@ export class Room extends EventEmitter {
|
||||||
}
|
}
|
||||||
}, {log}).response();
|
}, {log}).response();
|
||||||
|
|
||||||
const txn = this._storage.readWriteTxn([
|
const txn = await this._storage.readWriteTxn([
|
||||||
this._storage.storeNames.pendingEvents,
|
this._storage.storeNames.pendingEvents,
|
||||||
this._storage.storeNames.timelineEvents,
|
this._storage.storeNames.timelineEvents,
|
||||||
this._storage.storeNames.timelineFragments,
|
this._storage.storeNames.timelineFragments,
|
||||||
|
@ -584,7 +584,7 @@ export class Room extends EventEmitter {
|
||||||
async _getLastEventId() {
|
async _getLastEventId() {
|
||||||
const lastKey = this._syncWriter.lastMessageKey;
|
const lastKey = this._syncWriter.lastMessageKey;
|
||||||
if (lastKey) {
|
if (lastKey) {
|
||||||
const txn = this._storage.readTxn([
|
const txn = await this._storage.readTxn([
|
||||||
this._storage.storeNames.timelineEvents,
|
this._storage.storeNames.timelineEvents,
|
||||||
]);
|
]);
|
||||||
const eventEntry = await txn.timelineEvents.get(this._roomId, lastKey);
|
const eventEntry = await txn.timelineEvents.get(this._roomId, lastKey);
|
||||||
|
@ -607,7 +607,7 @@ export class Room extends EventEmitter {
|
||||||
if (this.isUnread || this.notificationCount) {
|
if (this.isUnread || this.notificationCount) {
|
||||||
return await this._platform.logger.wrapOrRun(log, "clearUnread", async log => {
|
return await this._platform.logger.wrapOrRun(log, "clearUnread", async log => {
|
||||||
log.set("id", this.id);
|
log.set("id", this.id);
|
||||||
const txn = this._storage.readWriteTxn([
|
const txn = await this._storage.readWriteTxn([
|
||||||
this._storage.storeNames.roomSummary,
|
this._storage.storeNames.roomSummary,
|
||||||
]);
|
]);
|
||||||
let data;
|
let data;
|
||||||
|
@ -706,7 +706,7 @@ export class Room extends EventEmitter {
|
||||||
if (this.isEncrypted) {
|
if (this.isEncrypted) {
|
||||||
stores.push(this._storage.storeNames.inboundGroupSessions);
|
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);
|
const storageEntry = await txn.timelineEvents.getByEventId(this._roomId, eventId);
|
||||||
if (storageEntry) {
|
if (storageEntry) {
|
||||||
const entry = new EventEntry(storageEntry, this._fragmentIdComparer);
|
const entry = new EventEntry(storageEntry, this._fragmentIdComparer);
|
||||||
|
|
|
@ -251,7 +251,7 @@ export class RoomSummary {
|
||||||
if (data === this._data) {
|
if (data === this._data) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const txn = storage.readWriteTxn([
|
const txn = await storage.readWriteTxn([
|
||||||
storage.storeNames.roomSummary,
|
storage.storeNames.roomSummary,
|
||||||
]);
|
]);
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
||||||
import {RoomMember} from "./RoomMember.js";
|
import {RoomMember} from "./RoomMember.js";
|
||||||
|
|
||||||
async function loadMembers({roomId, storage}) {
|
async function loadMembers({roomId, storage}) {
|
||||||
const txn = storage.readTxn([
|
const txn = await storage.readTxn([
|
||||||
storage.storeNames.roomMembers,
|
storage.storeNames.roomMembers,
|
||||||
]);
|
]);
|
||||||
const memberDatas = await txn.roomMembers.getAll(roomId);
|
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 memberResponse = await hsApi.members(roomId, {at: syncToken}, {log}).response();
|
||||||
|
|
||||||
const txn = storage.readWriteTxn([
|
const txn = await storage.readWriteTxn([
|
||||||
storage.storeNames.roomSummary,
|
storage.storeNames.roomSummary,
|
||||||
storage.storeNames.roomMembers,
|
storage.storeNames.roomMembers,
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -129,7 +129,7 @@ export class SendQueue {
|
||||||
async _removeEvent(pendingEvent) {
|
async _removeEvent(pendingEvent) {
|
||||||
const idx = this._pendingEvents.array.indexOf(pendingEvent);
|
const idx = this._pendingEvents.array.indexOf(pendingEvent);
|
||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
const txn = this._storage.readWriteTxn([this._storage.storeNames.pendingEvents]);
|
const txn = await this._storage.readWriteTxn([this._storage.storeNames.pendingEvents]);
|
||||||
try {
|
try {
|
||||||
txn.pendingEvents.remove(pendingEvent.roomId, pendingEvent.queueIndex);
|
txn.pendingEvents.remove(pendingEvent.roomId, pendingEvent.queueIndex);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -185,7 +185,7 @@ export class SendQueue {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _tryUpdateEvent(pendingEvent) {
|
async _tryUpdateEvent(pendingEvent) {
|
||||||
const txn = this._storage.readWriteTxn([this._storage.storeNames.pendingEvents]);
|
const txn = await this._storage.readWriteTxn([this._storage.storeNames.pendingEvents]);
|
||||||
try {
|
try {
|
||||||
// pendingEvent might have been removed already here
|
// pendingEvent might have been removed already here
|
||||||
// by a racing remote echo, so check first so we don't recreate it
|
// 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) {
|
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;
|
let pendingEvent;
|
||||||
try {
|
try {
|
||||||
const pendingEventsStore = txn.pendingEvents;
|
const pendingEventsStore = txn.pendingEvents;
|
||||||
|
|
|
@ -46,7 +46,7 @@ export class Timeline {
|
||||||
|
|
||||||
/** @package */
|
/** @package */
|
||||||
async load(user) {
|
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);
|
const memberData = await txn.roomMembers.get(this._roomId, user.id);
|
||||||
this._ownMember = new RoomMember(memberData);
|
this._ownMember = new RoomMember(memberData);
|
||||||
// it should be fine to not update the local entries,
|
// it should be fine to not update the local entries,
|
||||||
|
|
|
@ -108,14 +108,14 @@ export class TimelineReader {
|
||||||
|
|
||||||
readFrom(eventKey, direction, amount) {
|
readFrom(eventKey, direction, amount) {
|
||||||
return new ReaderRequest(async r => {
|
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);
|
return await this._readFrom(eventKey, direction, amount, r, txn);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
readFromEnd(amount, existingTxn = null) {
|
readFromEnd(amount, existingTxn = null) {
|
||||||
return new ReaderRequest(async r => {
|
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);
|
const liveFragment = await txn.timelineFragments.liveFragment(this._roomId);
|
||||||
let entries;
|
let entries;
|
||||||
// room hasn't been synced yet
|
// room hasn't been synced yet
|
||||||
|
|
|
@ -19,7 +19,7 @@ import {keyFromPassphrase} from "./passphrase.js";
|
||||||
import {keyFromRecoveryKey} from "./recoveryKey.js";
|
import {keyFromRecoveryKey} from "./recoveryKey.js";
|
||||||
|
|
||||||
async function readDefaultKeyDescription(storage) {
|
async function readDefaultKeyDescription(storage) {
|
||||||
const txn = storage.readTxn([
|
const txn = await storage.readTxn([
|
||||||
storage.storeNames.accountData
|
storage.storeNames.accountData
|
||||||
]);
|
]);
|
||||||
const defaultKeyEvent = await txn.accountData.get("m.secret_storage.default_key");
|
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 {Transaction} from "./Transaction.js";
|
||||||
import { STORE_NAMES, StorageError } from "../common.js";
|
import { STORE_NAMES, StorageError } from "../common.js";
|
||||||
|
import { reqAsPromise } from "./utils.js";
|
||||||
|
|
||||||
|
const WEBKITEARLYCLOSETXNBUG_BOGUS_KEY = "782rh281re38-boguskey";
|
||||||
|
|
||||||
export class Storage {
|
export class Storage {
|
||||||
constructor(idbDatabase) {
|
constructor(idbDatabase, hasWebkitEarlyCloseTxnBug) {
|
||||||
this._db = idbDatabase;
|
this._db = idbDatabase;
|
||||||
|
this._hasWebkitEarlyCloseTxnBug = hasWebkitEarlyCloseTxnBug;
|
||||||
const nameMap = STORE_NAMES.reduce((nameMap, name) => {
|
const nameMap = STORE_NAMES.reduce((nameMap, name) => {
|
||||||
nameMap[name] = name;
|
nameMap[name] = name;
|
||||||
return nameMap;
|
return nameMap;
|
||||||
|
@ -34,20 +38,30 @@ export class Storage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readTxn(storeNames) {
|
async readTxn(storeNames) {
|
||||||
this._validateStoreNames(storeNames);
|
this._validateStoreNames(storeNames);
|
||||||
try {
|
try {
|
||||||
const txn = this._db.transaction(storeNames, "readonly");
|
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);
|
return new Transaction(txn, storeNames);
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
throw new StorageError("readTxn failed", err);
|
throw new StorageError("readTxn failed", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readWriteTxn(storeNames) {
|
async readWriteTxn(storeNames) {
|
||||||
this._validateStoreNames(storeNames);
|
this._validateStoreNames(storeNames);
|
||||||
try {
|
try {
|
||||||
const txn = this._db.transaction(storeNames, "readwrite");
|
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);
|
return new Transaction(txn, storeNames);
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
throw new StorageError("readWriteTxn failed", err);
|
throw new StorageError("readWriteTxn failed", err);
|
||||||
|
|
|
@ -18,6 +18,7 @@ import {Storage} from "./Storage.js";
|
||||||
import { openDatabase, reqAsPromise } from "./utils.js";
|
import { openDatabase, reqAsPromise } from "./utils.js";
|
||||||
import { exportSession, importSession } from "./export.js";
|
import { exportSession, importSession } from "./export.js";
|
||||||
import { schema } from "./schema.js";
|
import { schema } from "./schema.js";
|
||||||
|
import { detectWebkitEarlyCloseTxnBug } from "./quirks.js";
|
||||||
|
|
||||||
const sessionName = sessionId => `hydrogen_session_${sessionId}`;
|
const sessionName = sessionId => `hydrogen_session_${sessionId}`;
|
||||||
const openDatabaseWithSessionId = sessionId => openDatabase(sessionName(sessionId), createStores, schema.length);
|
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");
|
console.warn("no persisted storage, database can be evicted by browser");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const hasWebkitEarlyCloseTxnBug = await detectWebkitEarlyCloseTxnBug();
|
||||||
const db = await openDatabaseWithSessionId(sessionId);
|
const db = await openDatabaseWithSessionId(sessionId);
|
||||||
return new Storage(db);
|
return new Storage(db, hasWebkitEarlyCloseTxnBug);
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(sessionId) {
|
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