Store sessions by encodedNumber
Storing multiple sessions in a single indexeddb record is prone to clobbering data due to races between requests to update multiple device sessions for the same number, since you have to read the current state of the device->session map and update it. Splitting the records up makes it so that those updates can be made in parallel. Selecting all the sessions for a given number can still be done efficiently thanks to indexeddb range queries.
This commit is contained in:
parent
37c496f4f0
commit
c26c6fc317
2 changed files with 50 additions and 44 deletions
|
@ -71,7 +71,16 @@
|
||||||
var Model = Backbone.Model.extend({ database: Whisper.Database });
|
var Model = Backbone.Model.extend({ database: Whisper.Database });
|
||||||
var PreKey = Model.extend({ storeName: 'preKeys' });
|
var PreKey = Model.extend({ storeName: 'preKeys' });
|
||||||
var SignedPreKey = Model.extend({ storeName: 'signedPreKeys' });
|
var SignedPreKey = Model.extend({ storeName: 'signedPreKeys' });
|
||||||
var Contact = Model.extend({ storeName: 'contacts' });
|
var Session = Model.extend({ storeName: 'sessions' });
|
||||||
|
var SessionCollection = Backbone.Collection.extend({
|
||||||
|
storeName: 'sessions',
|
||||||
|
database: Whisper.Database,
|
||||||
|
model: Session,
|
||||||
|
fetchSessionsForNumber: function(number) {
|
||||||
|
return this.fetch({range: [number + '.1', number + '.' + ':']});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var IdentityKey = Model.extend({ storeName: 'identityKeys' });
|
||||||
|
|
||||||
function AxolotlStore() {}
|
function AxolotlStore() {}
|
||||||
|
|
||||||
|
@ -176,13 +185,9 @@
|
||||||
if (encodedNumber === null || encodedNumber === undefined)
|
if (encodedNumber === null || encodedNumber === undefined)
|
||||||
throw new Error("Tried to get session for undefined/null key");
|
throw new Error("Tried to get session for undefined/null key");
|
||||||
return new Promise(function(resolve) {
|
return new Promise(function(resolve) {
|
||||||
var number = textsecure.utils.unencodeNumber(encodedNumber)[0];
|
var session = new Session({id: encodedNumber});
|
||||||
var deviceId = textsecure.utils.unencodeNumber(encodedNumber)[1];
|
session.fetch().always(function() {
|
||||||
|
resolve(session.get('record'));
|
||||||
var contact = new Contact({id: number});
|
|
||||||
contact.fetch().always(function() {
|
|
||||||
var sessions = contact.get('sessions') || {};
|
|
||||||
resolve(sessions[deviceId]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -192,13 +197,15 @@
|
||||||
throw new Error("Tried to put session for undefined/null key");
|
throw new Error("Tried to put session for undefined/null key");
|
||||||
return new Promise(function(resolve) {
|
return new Promise(function(resolve) {
|
||||||
var number = textsecure.utils.unencodeNumber(encodedNumber)[0];
|
var number = textsecure.utils.unencodeNumber(encodedNumber)[0];
|
||||||
var deviceId = textsecure.utils.unencodeNumber(encodedNumber)[1];
|
var deviceId = parseInt(textsecure.utils.unencodeNumber(encodedNumber)[1]);
|
||||||
|
|
||||||
var contact = new Contact({id: number});
|
var session = new Session({id: encodedNumber});
|
||||||
contact.fetch().always(function() {
|
session.fetch().always(function() {
|
||||||
var sessions = contact.get('sessions') || {};
|
session.save({
|
||||||
sessions[deviceId] = record;
|
record: record,
|
||||||
contact.save({sessions: sessions}).always(resolve);
|
deviceId: deviceId,
|
||||||
|
number: number
|
||||||
|
}).always(resolve);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -206,25 +213,17 @@
|
||||||
if (number === null || number === undefined)
|
if (number === null || number === undefined)
|
||||||
throw new Error("Tried to put session for undefined/null key");
|
throw new Error("Tried to put session for undefined/null key");
|
||||||
return new Promise(function(resolve) {
|
return new Promise(function(resolve) {
|
||||||
var contact = new Contact({id: number});
|
var sessions = new SessionCollection();
|
||||||
contact.fetch().always(function() {
|
sessions.fetchSessionsForNumber(number).always(function() {
|
||||||
var sessions = contact.get('sessions') || {};
|
resolve(sessions.pluck('deviceId'));
|
||||||
resolve(_.keys(sessions).map(function(n) {
|
|
||||||
return parseInt(n);
|
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
removeSession: function(encodedNumber) {
|
removeSession: function(encodedNumber) {
|
||||||
return new Promise(function(resolve) {
|
return new Promise(function(resolve) {
|
||||||
var number = textsecure.utils.unencodeNumber(encodedNumber)[0];
|
var session = new Session({id: encodedNumber});
|
||||||
var deviceId = textsecure.utils.unencodeNumber(encodedNumber)[1];
|
session.fetch().then(function() {
|
||||||
|
session.destroy().then(resolve);
|
||||||
var contact = new Contact({id: number});
|
|
||||||
contact.fetch().then(function() {
|
|
||||||
var sessions = contact.get('sessions') || {};
|
|
||||||
delete sessions[deviceId];
|
|
||||||
contact.save({sessions: sessions}).always(resolve);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -232,9 +231,15 @@
|
||||||
if (number === null || number === undefined)
|
if (number === null || number === undefined)
|
||||||
throw new Error("Tried to put session for undefined/null key");
|
throw new Error("Tried to put session for undefined/null key");
|
||||||
return new Promise(function(resolve) {
|
return new Promise(function(resolve) {
|
||||||
var contact = new Contact({id: number});
|
var sessions = new SessionCollection();
|
||||||
contact.fetch().then(function() {
|
sessions.fetchSessionsForNumber(number).always(function() {
|
||||||
contact.save({sessions: {}}).always(resolve);
|
var promises = [];
|
||||||
|
while(sessions.length > 0) {
|
||||||
|
promises.push(new Promise(function(res) {
|
||||||
|
sessions.pop().destroy().then(res);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
Promise.all(promises).then(resolve);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -243,31 +248,31 @@
|
||||||
throw new Error("Tried to get identity key for undefined/null key");
|
throw new Error("Tried to get identity key for undefined/null key");
|
||||||
var number = textsecure.utils.unencodeNumber(identifier)[0];
|
var number = textsecure.utils.unencodeNumber(identifier)[0];
|
||||||
return new Promise(function(resolve) {
|
return new Promise(function(resolve) {
|
||||||
var contact = new Contact({id: number});
|
var identityKey = new IdentityKey({id: number});
|
||||||
contact.fetch().always(function() {
|
identityKey.fetch().always(function() {
|
||||||
resolve(contact.get('identityKey'));
|
resolve(identityKey.get('publicKey'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
putIdentityKey: function(identifier, identityKey) {
|
putIdentityKey: function(identifier, publicKey) {
|
||||||
if (identifier === null || identifier === undefined)
|
if (identifier === null || identifier === undefined)
|
||||||
throw new Error("Tried to put identity key for undefined/null key");
|
throw new Error("Tried to put identity key for undefined/null key");
|
||||||
var number = textsecure.utils.unencodeNumber(identifier)[0];
|
var number = textsecure.utils.unencodeNumber(identifier)[0];
|
||||||
return new Promise(function(resolve) {
|
return new Promise(function(resolve) {
|
||||||
var contact = new Contact({id: number});
|
var identityKey = new IdentityKey({id: number});
|
||||||
contact.fetch().always(function() {
|
identityKey.fetch().always(function() {
|
||||||
var oldidentityKey = contact.get('identityKey');
|
var oldpublicKey = identityKey.get('publicKey');
|
||||||
if (oldidentityKey && !equalArrayBuffers(oldidentityKey, identityKey))
|
if (oldpublicKey && !equalArrayBuffers(oldpublicKey, publicKey))
|
||||||
throw new Error("Attempted to overwrite a different identity key");
|
throw new Error("Attempted to overwrite a different identity key");
|
||||||
contact.save({identityKey: identityKey}).then(resolve);
|
identityKey.save({publicKey: publicKey}).then(resolve);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
removeIdentityKey: function(number) {
|
removeIdentityKey: function(number) {
|
||||||
return new Promise(function(resolve) {
|
return new Promise(function(resolve) {
|
||||||
var contact = new Contact({id: number});
|
var identityKey = new IdentityKey({id: number});
|
||||||
contact.fetch().then(function() {
|
identityKey.fetch().then(function() {
|
||||||
contact.save({identityKey: undefined});
|
identityKey.save({publicKey: undefined});
|
||||||
}).fail(function() {
|
}).fail(function() {
|
||||||
throw new Error("Tried to remove identity for unknown number");
|
throw new Error("Tried to remove identity for unknown number");
|
||||||
});
|
});
|
||||||
|
|
|
@ -34,7 +34,8 @@
|
||||||
conversations.createIndex("group", "members", { unique: false, multiEntry: true });
|
conversations.createIndex("group", "members", { unique: false, multiEntry: true });
|
||||||
conversations.createIndex("type", "type", { unique: false });
|
conversations.createIndex("type", "type", { unique: false });
|
||||||
|
|
||||||
var contacts = transaction.db.createObjectStore('contacts');
|
var sessions = transaction.db.createObjectStore('sessions');
|
||||||
|
var identityKeys = transaction.db.createObjectStore('identityKeys');
|
||||||
|
|
||||||
var preKeys = transaction.db.createObjectStore("preKeys");
|
var preKeys = transaction.db.createObjectStore("preKeys");
|
||||||
var signedPreKeys = transaction.db.createObjectStore("signedPreKeys");
|
var signedPreKeys = transaction.db.createObjectStore("signedPreKeys");
|
||||||
|
|
Loading…
Reference in a new issue