1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-26 17:03:12 +03:00

Initial attempt at device tracking -> indexeddb

* Message sending works again, but

 * Marking multiple devices known (ie. 'send anyway') is very slow
   because it writes all device info out each time
 * Support for non-indexedb stores not written yet
 * No migration
This commit is contained in:
David Baker
2018-01-11 18:10:19 +00:00
parent eeb97f5b66
commit 83e4aa2755
5 changed files with 253 additions and 126 deletions

View File

@@ -25,6 +25,7 @@ import Promise from 'bluebird';
import DeviceInfo from './deviceinfo'; import DeviceInfo from './deviceinfo';
import olmlib from './olmlib'; import olmlib from './olmlib';
import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
/* State transition diagram for DeviceList._deviceTrackingStatus /* State transition diagram for DeviceList._deviceTrackingStatus
@@ -58,26 +59,95 @@ const TRACKING_STATUS_UP_TO_DATE = 3;
* @alias module:crypto/DeviceList * @alias module:crypto/DeviceList
*/ */
export default class DeviceList { export default class DeviceList {
constructor(baseApis, sessionStore, olmDevice) { constructor(baseApis, cryptoStore, olmDevice) {
this._sessionStore = sessionStore; this._cryptoStore = cryptoStore;
this._serialiser = new DeviceListUpdateSerialiser(
baseApis, sessionStore, olmDevice, // userId -> {
); // deviceId -> {
// [device info]
// }
// }
this._devices = null;
// which users we are tracking device status for. // which users we are tracking device status for.
// userId -> TRACKING_STATUS_* // userId -> TRACKING_STATUS_*
this._deviceTrackingStatus = sessionStore.getEndToEndDeviceTrackingStatus() || {}; this._deviceTrackingStatus = null; // loaded from storage in load()
// The 'next_batch' sync token at the point the data was writen,
// ie. a token represtenting the point immediately after the
// moment represented by the snapshot in the db.
this._syncToken = null;
this._serialiser = new DeviceListUpdateSerialiser(
baseApis, olmDevice,
);
// userId -> promise
this._keyDownloadsInProgressByUser = {};
// Set whenever changes are made other than setting the sync token
this._dirty = false;
}
/**
* Load the device tracking state from storage
*/
async load() {
await this._cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
this._cryptoStore.getEndToEndDeviceData(txn, (deviceData) => {
this._devices = deviceData ? deviceData.devices : {},
this._deviceTrackingStatus = deviceData ? deviceData.trackingStatus : {};
this._syncToken = deviceData ? deviceData.syncToken : null;
});
},
);
for (const u of Object.keys(this._deviceTrackingStatus)) { for (const u of Object.keys(this._deviceTrackingStatus)) {
// if a download was in progress when we got shut down, it isn't any more. // if a download was in progress when we got shut down, it isn't any more.
if (this._deviceTrackingStatus[u] == TRACKING_STATUS_DOWNLOAD_IN_PROGRESS) { if (this._deviceTrackingStatus[u] == TRACKING_STATUS_DOWNLOAD_IN_PROGRESS) {
this._deviceTrackingStatus[u] = TRACKING_STATUS_PENDING_DOWNLOAD; this._deviceTrackingStatus[u] = TRACKING_STATUS_PENDING_DOWNLOAD;
} }
} }
}
// userId -> promise /**
this._keyDownloadsInProgressByUser = {}; * Save the device tracking state to storage, if any changes are
* pending other than updating the sync token
* Before calling this, the caller must ensure that the state it
* has set this object to is consistent, ie. the appropriate sync
* token has been set with setSyncToken for any device updates that
* have occurred.
*/
async saveIfDirty() {
if (!this._dirty) return;
await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
this._cryptoStore.storeEndToEndDeviceData({
devices: this._devices,
trackingStatus: this._deviceTrackingStatus,
syncToken: this._syncToken,
}, txn);
},
);
this._dirty = false;
}
this.lastKnownSyncToken = null; /**
* Gets the current sync token
*
* @return {string} The sync token
*/
getSyncToken() {
return this._syncToken;
}
/**
* Sets the current sync token
*
* @param {string} st The sync token
*/
setSyncToken(st) {
this._syncToken = st;
} }
/** /**
@@ -152,7 +222,7 @@ export default class DeviceList {
* managed to get a list of devices for this user yet. * managed to get a list of devices for this user yet.
*/ */
getStoredDevicesForUser(userId) { getStoredDevicesForUser(userId) {
const devs = this._sessionStore.getEndToEndDevicesForUser(userId); const devs = this._devices[userId];
if (!devs) { if (!devs) {
return null; return null;
} }
@@ -165,6 +235,22 @@ export default class DeviceList {
return res; return res;
} }
/**
* Get the stored device data for a user, in raw object form
*
* @param {string} userId the user to get data for
*
* @return {Object} userId->deviceId->{object} devices, or null if
* there is no data for this user.
*/
getRawStoredDevicesForUser(userId) {
const devs = this._devices[userId];
if (!devs) {
return null;
}
return devs;
}
/** /**
* Get the stored keys for a single device * Get the stored keys for a single device
* *
@@ -175,7 +261,7 @@ export default class DeviceList {
* if we don't know about this device * if we don't know about this device
*/ */
getStoredDevice(userId, deviceId) { getStoredDevice(userId, deviceId) {
const devs = this._sessionStore.getEndToEndDevicesForUser(userId); const devs = this._devices[userId];
if (!devs || !devs[deviceId]) { if (!devs || !devs[deviceId]) {
return undefined; return undefined;
} }
@@ -200,7 +286,7 @@ export default class DeviceList {
return null; return null;
} }
const devices = this._sessionStore.getEndToEndDevicesForUser(userId); const devices = this._devices[userId];
if (!devices) { if (!devices) {
return null; return null;
} }
@@ -229,6 +315,14 @@ export default class DeviceList {
return null; return null;
} }
/**
* Replaces the list of devices for a user with the given device list
*/
storeDevicesForUser(u, devs) {
this._devices[u] = devs;
this._dirty = true;
}
/** /**
* flag the given user for device-list tracking, if they are not already. * flag the given user for device-list tracking, if they are not already.
* *
@@ -254,8 +348,8 @@ export default class DeviceList {
this._deviceTrackingStatus[userId] = TRACKING_STATUS_PENDING_DOWNLOAD; this._deviceTrackingStatus[userId] = TRACKING_STATUS_PENDING_DOWNLOAD;
} }
// we don't yet persist the tracking status, since there may be a lot // we don't yet persist the tracking status, since there may be a lot
// of calls; instead we wait for the forthcoming // of calls; we save all data together once the sync is done
// refreshOutdatedDeviceLists. this._dirty = true;
} }
/** /**
@@ -273,8 +367,9 @@ export default class DeviceList {
this._deviceTrackingStatus[userId] = TRACKING_STATUS_NOT_TRACKED; this._deviceTrackingStatus[userId] = TRACKING_STATUS_NOT_TRACKED;
} }
// we don't yet persist the tracking status, since there may be a lot // we don't yet persist the tracking status, since there may be a lot
// of calls; instead we wait for the forthcoming // of calls; we save all data together once the sync is done
// refreshOutdatedDeviceLists.
this._dirty = true;
} }
@@ -295,8 +390,9 @@ export default class DeviceList {
this._deviceTrackingStatus[userId] = TRACKING_STATUS_PENDING_DOWNLOAD; this._deviceTrackingStatus[userId] = TRACKING_STATUS_PENDING_DOWNLOAD;
} }
// we don't yet persist the tracking status, since there may be a lot // we don't yet persist the tracking status, since there may be a lot
// of calls; instead we wait for the forthcoming // of calls; we save all data together once the sync is done
// refreshOutdatedDeviceLists.
this._dirty = true;
} }
/** /**
@@ -318,6 +414,8 @@ export default class DeviceList {
* is no need to wait for this (it's mostly for the unit tests). * is no need to wait for this (it's mostly for the unit tests).
*/ */
refreshOutdatedDeviceLists() { refreshOutdatedDeviceLists() {
this.saveIfDirty();
const usersToDownload = []; const usersToDownload = [];
for (const userId of Object.keys(this._deviceTrackingStatus)) { for (const userId of Object.keys(this._deviceTrackingStatus)) {
const stat = this._deviceTrackingStatus[userId]; const stat = this._deviceTrackingStatus[userId];
@@ -326,10 +424,6 @@ export default class DeviceList {
} }
} }
// we didn't persist the tracking status during
// invalidateUserDeviceList, so do it now.
this._persistDeviceTrackingStatus();
return this._doKeyDownload(usersToDownload); return this._doKeyDownload(usersToDownload);
} }
@@ -352,14 +446,14 @@ export default class DeviceList {
} }
const prom = this._serialiser.updateDevicesForUsers( const prom = this._serialiser.updateDevicesForUsers(
users, this.lastKnownSyncToken, this._devices, users, this._syncToken,
).then(() => { ).then((newDevices) => {
finished(true); finished(newDevices);
}, (e) => { }, (e) => {
console.error( console.error(
'Error downloading keys for ' + users + ":", e, 'Error downloading keys for ' + users + ":", e,
); );
finished(false); finished(null);
throw e; throw e;
}); });
@@ -371,7 +465,7 @@ export default class DeviceList {
} }
}); });
const finished = (success) => { const finished = (newDevices) => {
users.forEach((u) => { users.forEach((u) => {
// we may have queued up another download request for this user // we may have queued up another download request for this user
// since we started this request. If that happens, we should // since we started this request. If that happens, we should
@@ -384,25 +478,23 @@ export default class DeviceList {
delete this._keyDownloadsInProgressByUser[u]; delete this._keyDownloadsInProgressByUser[u];
const stat = this._deviceTrackingStatus[u]; const stat = this._deviceTrackingStatus[u];
if (stat == TRACKING_STATUS_DOWNLOAD_IN_PROGRESS) { if (stat == TRACKING_STATUS_DOWNLOAD_IN_PROGRESS) {
if (success) { if (newDevices) {
// we didn't get any new invalidations since this download started: // we didn't get any new invalidations since this download started:
// this user's device list is now up to date. // this user's device list is now up to date.
this._deviceTrackingStatus[u] = TRACKING_STATUS_UP_TO_DATE; this._deviceTrackingStatus[u] = TRACKING_STATUS_UP_TO_DATE;
this._devices[u] = newDevices[u];
this._dirty = true;
console.log("Device list for", u, "now up to date"); console.log("Device list for", u, "now up to date");
} else { } else {
this._deviceTrackingStatus[u] = TRACKING_STATUS_PENDING_DOWNLOAD; this._deviceTrackingStatus[u] = TRACKING_STATUS_PENDING_DOWNLOAD;
} }
} }
}); });
this._persistDeviceTrackingStatus(); this.saveIfDirty();
}; };
return prom; return prom;
} }
_persistDeviceTrackingStatus() {
this._sessionStore.storeEndToEndDeviceTrackingStatus(this._deviceTrackingStatus);
}
} }
/** /**
@@ -415,9 +507,8 @@ export default class DeviceList {
* time (and queuing other requests up). * time (and queuing other requests up).
*/ */
class DeviceListUpdateSerialiser { class DeviceListUpdateSerialiser {
constructor(baseApis, sessionStore, olmDevice) { constructor(baseApis, olmDevice) {
this._baseApis = baseApis; this._baseApis = baseApis;
this._sessionStore = sessionStore;
this._olmDevice = olmDevice; this._olmDevice = olmDevice;
this._downloadInProgress = false; this._downloadInProgress = false;
@@ -431,14 +522,15 @@ class DeviceListUpdateSerialiser {
// non-null indicates that we have users queued for download. // non-null indicates that we have users queued for download.
this._queuedQueryDeferred = null; this._queuedQueryDeferred = null;
// sync token to be used for the next query: essentially the this._devices = null; // the complete device list
// most recent one we know about this._updatedDevices = null; // device list updates we've fetched
this._nextSyncToken = null;
} }
/** /**
* Make a key query request for the given users * Make a key query request for the given users
* *
* @param {object} devices The current device list
*
* @param {String[]} users list of user ids * @param {String[]} users list of user ids
* *
* @param {String} syncToken sync token to pass in the query request, to * @param {String} syncToken sync token to pass in the query request, to
@@ -446,13 +538,12 @@ class DeviceListUpdateSerialiser {
* *
* @return {module:client.Promise} resolves when all the users listed have * @return {module:client.Promise} resolves when all the users listed have
* been updated. rejects if there was a problem updating any of the * been updated. rejects if there was a problem updating any of the
* users. * users. Returns a fresh device list object for the users queried.
*/ */
updateDevicesForUsers(users, syncToken) { updateDevicesForUsers(devices, users, syncToken) {
users.forEach((u) => { users.forEach((u) => {
this._keyDownloadsQueuedByUser[u] = true; this._keyDownloadsQueuedByUser[u] = true;
}); });
this._nextSyncToken = syncToken;
if (!this._queuedQueryDeferred) { if (!this._queuedQueryDeferred) {
this._queuedQueryDeferred = Promise.defer(); this._queuedQueryDeferred = Promise.defer();
@@ -464,11 +555,13 @@ class DeviceListUpdateSerialiser {
return this._queuedQueryDeferred.promise; return this._queuedQueryDeferred.promise;
} }
this._devices = devices;
this._updatedDevices = {};
// start a new download. // start a new download.
return this._doQueuedQueries(); return this._doQueuedQueries(syncToken);
} }
_doQueuedQueries() { _doQueuedQueries(syncToken) {
if (this._downloadInProgress) { if (this._downloadInProgress) {
throw new Error( throw new Error(
"DeviceListUpdateSerialiser._doQueuedQueries called with request active", "DeviceListUpdateSerialiser._doQueuedQueries called with request active",
@@ -484,8 +577,8 @@ class DeviceListUpdateSerialiser {
this._downloadInProgress = true; this._downloadInProgress = true;
const opts = {}; const opts = {};
if (this._nextSyncToken) { if (syncToken) {
opts.token = this._nextSyncToken; opts.token = syncToken;
} }
this._baseApis.downloadKeysForUsers( this._baseApis.downloadKeysForUsers(
@@ -510,11 +603,11 @@ class DeviceListUpdateSerialiser {
console.log('Completed key download for ' + downloadUsers); console.log('Completed key download for ' + downloadUsers);
this._downloadInProgress = false; this._downloadInProgress = false;
deferred.resolve(); deferred.resolve(this._updatedDevices);
// if we have queued users, fire off another request. // if we have queued users, fire off another request.
if (this._queuedQueryDeferred) { if (this._queuedQueryDeferred) {
this._doQueuedQueries(); this._doQueuedQueries(syncToken);
} }
}, (e) => { }, (e) => {
console.warn('Error downloading keys for ' + downloadUsers + ':', e); console.warn('Error downloading keys for ' + downloadUsers + ':', e);
@@ -530,7 +623,7 @@ class DeviceListUpdateSerialiser {
// map from deviceid -> deviceinfo for this user // map from deviceid -> deviceinfo for this user
const userStore = {}; const userStore = {};
const devs = this._sessionStore.getEndToEndDevicesForUser(userId); const devs = this._devices[userId];
if (devs) { if (devs) {
Object.keys(devs).forEach((deviceId) => { Object.keys(devs).forEach((deviceId) => {
const d = DeviceInfo.fromStorage(devs[deviceId], deviceId); const d = DeviceInfo.fromStorage(devs[deviceId], deviceId);
@@ -548,9 +641,7 @@ class DeviceListUpdateSerialiser {
storage[deviceId] = userStore[deviceId].toStorage(); storage[deviceId] = userStore[deviceId].toStorage();
}); });
this._sessionStore.storeEndToEndDevicesForUser( this._updatedDevices[userId] = storage;
userId, storage,
);
} }
} }

View File

@@ -69,7 +69,7 @@ function Crypto(baseApis, sessionStore, userId, deviceId,
this._cryptoStore = cryptoStore; this._cryptoStore = cryptoStore;
this._olmDevice = new OlmDevice(sessionStore, cryptoStore); this._olmDevice = new OlmDevice(sessionStore, cryptoStore);
this._deviceList = new DeviceList(baseApis, sessionStore, this._olmDevice); this._deviceList = new DeviceList(baseApis, cryptoStore, this._olmDevice);
// the last time we did a check for the number of one-time-keys on the // the last time we did a check for the number of one-time-keys on the
// server. // server.
@@ -128,6 +128,7 @@ Crypto.prototype.init = async function() {
} }
await this._olmDevice.init(); await this._olmDevice.init();
await this._deviceList.load();
// build our device keys: these will later be uploaded // build our device keys: these will later be uploaded
this._deviceKeys["ed25519:" + this._deviceId] = this._deviceKeys["ed25519:" + this._deviceId] =
@@ -135,7 +136,7 @@ Crypto.prototype.init = async function() {
this._deviceKeys["curve25519:" + this._deviceId] = this._deviceKeys["curve25519:" + this._deviceId] =
this._olmDevice.deviceCurve25519Key; this._olmDevice.deviceCurve25519Key;
let myDevices = this._sessionStore.getEndToEndDevicesForUser( let myDevices = this._deviceList.getRawStoredDevicesForUser(
this._userId, this._userId,
); );
@@ -153,9 +154,10 @@ Crypto.prototype.init = async function() {
}; };
myDevices[this._deviceId] = deviceInfo; myDevices[this._deviceId] = deviceInfo;
this._sessionStore.storeEndToEndDevicesForUser( this._deviceList.storeDevicesForUser(
this._userId, myDevices, this._userId, myDevices,
); );
this._deviceList.saveIfDirty();
} }
}; };
@@ -456,7 +458,7 @@ Crypto.prototype.getStoredDevice = function(userId, deviceId) {
Crypto.prototype.setDeviceVerification = async function( Crypto.prototype.setDeviceVerification = async function(
userId, deviceId, verified, blocked, known, userId, deviceId, verified, blocked, known,
) { ) {
const devices = this._sessionStore.getEndToEndDevicesForUser(userId); const devices = this._deviceList.getRawStoredDevicesForUser(userId);
if (!devices || !devices[deviceId]) { if (!devices || !devices[deviceId]) {
throw new Error("Unknown device " + userId + ":" + deviceId); throw new Error("Unknown device " + userId + ":" + deviceId);
} }
@@ -484,7 +486,8 @@ Crypto.prototype.setDeviceVerification = async function(
if (dev.verified !== verificationStatus || dev.known !== knownStatus) { if (dev.verified !== verificationStatus || dev.known !== knownStatus) {
dev.verified = verificationStatus; dev.verified = verificationStatus;
dev.known = knownStatus; dev.known = knownStatus;
this._sessionStore.storeEndToEndDevicesForUser(userId, devices); this._deviceList.storeDevicesForUser(userId, devices);
this._deviceList.saveIfDirty();
} }
return DeviceInfo.fromStorage(dev, deviceId); return DeviceInfo.fromStorage(dev, deviceId);
}; };
@@ -812,21 +815,31 @@ Crypto.prototype.decryptEvent = function(event) {
* @param {Object} deviceLists device_lists field from /sync, or response from * @param {Object} deviceLists device_lists field from /sync, or response from
* /keys/changes * /keys/changes
*/ */
Crypto.prototype.handleDeviceListChanges = async function(deviceLists) { Crypto.prototype.handleDeviceListChanges = async function(syncData, syncDeviceLists) {
if (deviceLists.changed && Array.isArray(deviceLists.changed)) { // No point processing device list changes for initial syncs: they'd be meaningless
deviceLists.changed.forEach((u) => { // since the server doesn't know what point we were were at previously. We'll either
this._deviceList.invalidateUserDeviceList(u); // get the complete list of changes for the interval or invalidate everything in
}); // onSyncComplete
} if (!syncData.oldSyncToken) return;
if (deviceLists.left && Array.isArray(deviceLists.left)) { if (syncData.oldSyncToken === this._deviceList.getSyncToken()) {
deviceLists.left.forEach((u) => { // the point the db is at matches where the sync started from, so
this._deviceList.stopTrackingDeviceList(u); // we can safely write the changes
}); this._evalDeviceListChanges(syncDeviceLists);
} else {
// the db is at a different point to where this sync started from, so
// additionally fetch the changes between where the db is and where the
// sync started
console.log(
"Device list sync gap detected - fetching key changes between " +
this._deviceList.getSyncToken() + " and " + syncData.oldSyncToken,
);
const gapDeviceLists = await this._baseApis.getKeyChanges(
this._deviceList.getSyncToken(), syncData.oldSyncToken,
);
this._evalDeviceListChanges(gapDeviceLists);
this._evalDeviceListChanges(syncDeviceLists);
} }
// don't flush the outdated device list yet - we do it once we finish
// processing the sync.
}; };
/** /**
@@ -890,36 +903,15 @@ Crypto.prototype.onSyncCompleted = async function(syncData) {
const nextSyncToken = syncData.nextSyncToken; const nextSyncToken = syncData.nextSyncToken;
if (!syncData.oldSyncToken) { if (!syncData.oldSyncToken) {
console.log("Completed initial sync"); // If we have a stored device sync token, we could request the complete
// list of device changes from the server here to get our device list up
// if we have a deviceSyncToken, we can tell the deviceList to // to date. This case should be relatively rare though (only when you hit
// invalidate devices which have changed since then. // 'clear cache and reload' in practice) so we just invalidate everything.
const oldSyncToken = this._sessionStore.getEndToEndDeviceSyncToken(); console.log("invalidating all device list caches after inital sync");
if (oldSyncToken !== null) { this._deviceList.invalidateAllDeviceLists();
try {
await this._invalidateDeviceListsSince(
oldSyncToken, nextSyncToken,
);
} catch (e) {
// if that failed, we fall back to invalidating everyone.
console.warn("Error fetching changed device list", e);
this._deviceList.invalidateAllDeviceLists();
}
} else {
// otherwise, we have to invalidate all devices for all users we
// are tracking.
console.log("Completed first initialsync; invalidating all " +
"device list caches");
this._deviceList.invalidateAllDeviceLists();
}
} }
this._deviceList.setSyncToken(syncData.nextSyncToken);
// we can now store our sync token so that we can get an update on this._deviceList.saveIfDirty();
// restart rather than having to invalidate everyone.
//
// (we don't really need to do this on every sync - we could just
// do it periodically)
this._sessionStore.storeEndToEndDeviceSyncToken(nextSyncToken);
// catch up on any new devices we got told about during the sync. // catch up on any new devices we got told about during the sync.
this._deviceList.lastKnownSyncToken = nextSyncToken; this._deviceList.lastKnownSyncToken = nextSyncToken;
@@ -936,25 +928,24 @@ Crypto.prototype.onSyncCompleted = async function(syncData) {
}; };
/** /**
* Ask the server which users have new devices since a given token, * Trigger the appropriate invalidations and removes for a given
* and invalidate them * device list
* *
* @param {String} oldSyncToken * @param {Object} deviceLists device_lists field from /sync, or response from
* @param {String} lastKnownSyncToken * /keys/changes
*
* Returns a Promise which resolves once the query is complete. Rejects if the
* keyChange query fails.
*/ */
Crypto.prototype._invalidateDeviceListsSince = async function( Crypto.prototype._evalDeviceListChanges = async function(deviceLists) {
oldSyncToken, lastKnownSyncToken, if (deviceLists.changed && Array.isArray(deviceLists.changed)) {
) { deviceLists.changed.forEach((u) => {
const r = await this._baseApis.getKeyChanges( this._deviceList.invalidateUserDeviceList(u);
oldSyncToken, lastKnownSyncToken, });
); }
console.log("got key changes since", oldSyncToken, ":", r); if (deviceLists.left && Array.isArray(deviceLists.left)) {
deviceLists.left.forEach((u) => {
await this.handleDeviceListChanges(r); this._deviceList.stopTrackingDeviceList(u);
});
}
}; };
/** /**

View File

@@ -1,7 +1,7 @@
import Promise from 'bluebird'; import Promise from 'bluebird';
import utils from '../../utils'; import utils from '../../utils';
export const VERSION = 4; export const VERSION = 5;
/** /**
* Implementation of a CryptoStore which is backed by an existing * Implementation of a CryptoStore which is backed by an existing
@@ -391,6 +391,23 @@ export class Backend {
}); });
} }
getEndToEndDeviceData(txn, func) {
const objectStore = txn.objectStore("device_data");
const getReq = objectStore.get("-");
getReq.onsuccess = function() {
try {
func(getReq.result || null);
} catch (e) {
abortWithException(txn, e);
}
};
}
storeEndToEndDeviceData(deviceData, txn) {
const objectStore = txn.objectStore("device_data");
objectStore.put(deviceData, "-");
}
doTxn(mode, stores, func) { doTxn(mode, stores, func) {
const txn = this._db.transaction(stores, mode); const txn = this._db.transaction(stores, mode);
const promise = promiseifyTxn(txn); const promise = promiseifyTxn(txn);
@@ -423,6 +440,9 @@ export function upgradeDatabase(db, oldVersion) {
keyPath: ["senderCurve25519Key", "sessionId"], keyPath: ["senderCurve25519Key", "sessionId"],
}); });
} }
if (oldVersion < 5) {
db.createObjectStore("device_data");
}
// Expand as needed. // Expand as needed.
} }

View File

@@ -358,6 +358,30 @@ export default class IndexedDBCryptoStore {
); );
} }
// End-to-end device tracking
/**
* Store the state of all tracked devices
* This contains devices for each user, a tracking state for each user
* and a sync token matching the point in time the snapshot represents.
* These all need to be written out in full each time such that the snapshot
* is always consistent, so they are stored in one object.
*
* @param {Object} deviceData
*/
storeEndToEndDeviceData(deviceData, txn) {
this._backendPromise.value().storeEndToEndDeviceData(deviceData, txn);
}
/**
* Get the state of all tracked devices
*
* @param {*} txn An active transaction. See doTxn().
*/
getEndToEndDeviceData(txn, func) {
return this._backendPromise.value().getEndToEndDeviceData(txn, func);
}
/** /**
* Perform a transaction on the crypto store. Any store methods * Perform a transaction on the crypto store. Any store methods
* that require a transaction (txn) object to be passed in may * that require a transaction (txn) object to be passed in may
@@ -389,3 +413,4 @@ export default class IndexedDBCryptoStore {
IndexedDBCryptoStore.STORE_ACCOUNT = 'account'; IndexedDBCryptoStore.STORE_ACCOUNT = 'account';
IndexedDBCryptoStore.STORE_SESSIONS = 'sessions'; IndexedDBCryptoStore.STORE_SESSIONS = 'sessions';
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions'; IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions';
IndexedDBCryptoStore.STORE_DEVICE_DATA = 'device_data';

View File

@@ -621,8 +621,14 @@ SyncApi.prototype._sync = async function(syncOptions) {
await client.store.setSyncData(data); await client.store.setSyncData(data);
} }
const syncEventData = {
oldSyncToken: syncToken,
nextSyncToken: data.next_batch,
catchingUp: this._catchingUp,
};
try { try {
await this._processSyncResponse(syncToken, data, isCachedResponse); await this._processSyncResponse(syncEventData, data, isCachedResponse);
} catch(e) { } catch(e) {
// log the exception with stack if we have it, else fall back // log the exception with stack if we have it, else fall back
// to the plain description // to the plain description
@@ -630,12 +636,6 @@ SyncApi.prototype._sync = async function(syncOptions) {
} }
// emit synced events // emit synced events
const syncEventData = {
oldSyncToken: syncToken,
nextSyncToken: data.next_batch,
catchingUp: this._catchingUp,
};
if (!syncOptions.hasSyncedBefore) { if (!syncOptions.hasSyncedBefore) {
this._updateSyncState("PREPARED", syncEventData); this._updateSyncState("PREPARED", syncEventData);
syncOptions.hasSyncedBefore = true; syncOptions.hasSyncedBefore = true;
@@ -708,7 +708,7 @@ SyncApi.prototype._onSyncError = function(err, syncOptions) {
* @param {bool} isCachedResponse True if this response is from our local cache * @param {bool} isCachedResponse True if this response is from our local cache
*/ */
SyncApi.prototype._processSyncResponse = async function( SyncApi.prototype._processSyncResponse = async function(
syncToken, data, isCachedResponse, syncEventData, data, isCachedResponse,
) { ) {
const client = this.client; const client = this.client;
const self = this; const self = this;
@@ -950,7 +950,7 @@ SyncApi.prototype._processSyncResponse = async function(
self._deregisterStateListeners(room); self._deregisterStateListeners(room);
room.resetLiveTimeline( room.resetLiveTimeline(
joinObj.timeline.prev_batch, joinObj.timeline.prev_batch,
self.opts.canResetEntireTimeline(room.roomId) ? null : syncToken, self.opts.canResetEntireTimeline(room.roomId) ? null : syncEventData.oldSyncToken,
); );
// We have to assume any gap in any timeline is // We have to assume any gap in any timeline is
@@ -1034,7 +1034,7 @@ SyncApi.prototype._processSyncResponse = async function(
// in the timeline relative to ones paginated in by /notifications. // in the timeline relative to ones paginated in by /notifications.
// XXX: we could fix this by making EventTimeline support chronological // XXX: we could fix this by making EventTimeline support chronological
// ordering... but it doesn't, right now. // ordering... but it doesn't, right now.
if (syncToken && this._notifEvents.length) { if (syncEventData.oldSyncToken && this._notifEvents.length) {
this._notifEvents.sort(function(a, b) { this._notifEvents.sort(function(a, b) {
return a.getTs() - b.getTs(); return a.getTs() - b.getTs();
}); });
@@ -1046,7 +1046,7 @@ SyncApi.prototype._processSyncResponse = async function(
// Handle device list updates // Handle device list updates
if (data.device_lists) { if (data.device_lists) {
if (this.opts.crypto) { if (this.opts.crypto) {
await this.opts.crypto.handleDeviceListChanges(data.device_lists); await this.opts.crypto.handleDeviceListChanges(syncEventData, data.device_lists);
} else { } else {
// FIXME if we *don't* have a crypto module, we still need to // FIXME if we *don't* have a crypto module, we still need to
// invalidate the device lists. But that would require a // invalidate the device lists. But that would require a