Improve message download performance

This commit is contained in:
Scott Nonnenberg 2019-09-26 12:56:31 -07:00
parent 957f6f6474
commit 0c09f9620f
32 changed files with 906 additions and 633 deletions

View file

@ -34,14 +34,6 @@ module.exports = grunt => {
src: components, src: components,
dest: 'js/components.js', dest: 'js/components.js',
}, },
util_worker: {
src: [
'components/bytebuffer/dist/ByteBufferAB.js',
'components/long/dist/Long.js',
'js/util_worker_tasks.js',
],
dest: 'js/util_worker.js',
},
libtextsecurecomponents: { libtextsecurecomponents: {
src: libtextsecurecomponents, src: libtextsecurecomponents,
dest: 'libtextsecure/components.js', dest: 'libtextsecure/components.js',

View file

@ -69,6 +69,17 @@ function initialize() {
}); });
}); });
ipc.on('batch-log', (first, batch) => {
batch.forEach(item => {
logger[item.level](
{
time: new Date(item.timestamp),
},
item.logText
);
});
});
ipc.on('fetch-log', event => { ipc.on('fetch-log', event => {
fetch(logPath).then( fetch(logPath).then(
data => { data => {

View file

@ -19,10 +19,6 @@ const {
pick, pick,
} = require('lodash'); } = require('lodash');
// To get long stack traces
// https://github.com/mapbox/node-sqlite3/wiki/API#sqlite3verbose
sql.verbose();
module.exports = { module.exports = {
initialize, initialize,
close, close,
@ -58,6 +54,7 @@ module.exports = {
removeAllItems, removeAllItems,
createOrUpdateSession, createOrUpdateSession,
createOrUpdateSessions,
getSessionById, getSessionById,
getSessionsByNumber, getSessionsByNumber,
bulkAddSessions, bulkAddSessions,
@ -71,6 +68,7 @@ module.exports = {
saveConversations, saveConversations,
getConversationById, getConversationById,
updateConversation, updateConversation,
updateConversations,
removeConversation, removeConversation,
getAllConversations, getAllConversations,
getAllConversationIds, getAllConversationIds,
@ -105,6 +103,7 @@ module.exports = {
saveUnprocessed, saveUnprocessed,
updateUnprocessedAttempts, updateUnprocessedAttempts,
updateUnprocessedWithData, updateUnprocessedWithData,
updateUnprocessedsWithData,
getUnprocessedById, getUnprocessedById,
saveUnprocesseds, saveUnprocesseds,
removeUnprocessed, removeUnprocessed,
@ -1259,10 +1258,20 @@ async function initialize({ configDir, key, messages }) {
promisified = await openAndSetUpSQLCipher(filePath, { key }); promisified = await openAndSetUpSQLCipher(filePath, { key });
// promisified.on('trace', async statement => { // promisified.on('trace', async statement => {
// if (!db || statement.startsWith('--')) { // if (
// console._log(statement); // !db ||
// statement.startsWith('--') ||
// statement.includes('COMMIT') ||
// statement.includes('BEGIN') ||
// statement.includes('ROLLBACK')
// ) {
// return; // return;
// } // }
// // Note that this causes problems when attempting to commit transactions - this
// // statement is running, and we get at SQLITE_BUSY error. So we delay.
// await new Promise(resolve => setTimeout(resolve, 1000));
// const data = await db.get(`EXPLAIN QUERY PLAN ${statement}`); // const data = await db.get(`EXPLAIN QUERY PLAN ${statement}`);
// console._log(`EXPLAIN QUERY PLAN ${statement}\n`, data && data.detail); // console._log(`EXPLAIN QUERY PLAN ${statement}\n`, data && data.detail);
// }); // });
@ -1469,6 +1478,19 @@ async function createOrUpdateSession(data) {
} }
); );
} }
async function createOrUpdateSessions(array) {
await db.run('BEGIN TRANSACTION;');
try {
await Promise.all([...map(array, item => createOrUpdateSession(item))]);
await db.run('COMMIT TRANSACTION;');
} catch (error) {
await db.run('ROLLBACK;');
throw error;
}
}
createOrUpdateSessions.needsSerial = true;
async function getSessionById(id) { async function getSessionById(id) {
return getById(SESSIONS_TABLE, id); return getById(SESSIONS_TABLE, id);
} }
@ -1663,6 +1685,18 @@ async function updateConversation(data) {
} }
); );
} }
async function updateConversations(array) {
await db.run('BEGIN TRANSACTION;');
try {
await Promise.all([...map(array, item => updateConversation(item))]);
await db.run('COMMIT TRANSACTION;');
} catch (error) {
await db.run('ROLLBACK;');
throw error;
}
}
updateConversations.needsSerial = true;
async function removeConversation(id) { async function removeConversation(id) {
if (!Array.isArray(id)) { if (!Array.isArray(id)) {
@ -2353,6 +2387,23 @@ async function updateUnprocessedWithData(id, data = {}) {
} }
); );
} }
async function updateUnprocessedsWithData(arrayOfUnprocessed) {
await db.run('BEGIN TRANSACTION;');
try {
await Promise.all([
...map(arrayOfUnprocessed, ({ id, data }) =>
updateUnprocessedWithData(id, data)
),
]);
await db.run('COMMIT TRANSACTION;');
} catch (error) {
await db.run('ROLLBACK;');
throw error;
}
}
updateUnprocessedsWithData.needsSerial = true;
async function getUnprocessedById(id) { async function getUnprocessedById(id) {
const row = await db.get('SELECT * FROM unprocessed WHERE id = $id;', { const row = await db.get('SELECT * FROM unprocessed WHERE id = $id;', {

View file

@ -27,19 +27,22 @@ function makeNewMultipleQueue() {
return multipleQueue; return multipleQueue;
} }
function initialize() { function makeSQLJob(fn, callName, jobId, args) {
if (initialized) { // console.log(`Job ${jobId} (${callName}) queued`);
throw new Error('sqlChannels: already initialized!'); return async () => {
// const start = Date.now();
// console.log(`Job ${jobId} (${callName}) started`);
const result = await fn(...args);
// const end = Date.now();
// console.log(`Job ${jobId} (${callName}) succeeded in ${end - start}ms`);
return result;
};
} }
initialized = true;
ipcMain.on(SQL_CHANNEL_KEY, async (event, jobId, callName, ...args) => { async function handleCall(callName, jobId, args) {
try {
const fn = sql[callName]; const fn = sql[callName];
if (!fn) { if (!fn) {
throw new Error( throw new Error(`sql channel: ${callName} is not an available function`);
`sql channel: ${callName} is not an available function`
);
} }
let result; let result;
@ -51,24 +54,24 @@ function initialize() {
// A needsSerial method must be run in our single concurrency queue. // A needsSerial method must be run in our single concurrency queue.
if (fn.needsSerial) { if (fn.needsSerial) {
if (singleQueue) { if (singleQueue) {
result = await singleQueue.add(() => fn(...args)); result = await singleQueue.add(makeSQLJob(fn, callName, jobId, args));
} else if (multipleQueue) { } else if (multipleQueue) {
makeNewSingleQueue(); makeNewSingleQueue();
singleQueue.add(() => multipleQueue.onIdle()); singleQueue.add(() => multipleQueue.onIdle());
multipleQueue = null; multipleQueue = null;
result = await singleQueue.add(() => fn(...args)); result = await singleQueue.add(makeSQLJob(fn, callName, jobId, args));
} else { } else {
makeNewSingleQueue(); makeNewSingleQueue();
result = await singleQueue.add(() => fn(...args)); result = await singleQueue.add(makeSQLJob(fn, callName, jobId, args));
} }
} else { } else {
// The request can be parallelized. To keep the same structure as the above block // The request can be parallelized. To keep the same structure as the above block
// we force this section into the 'lonely if' pattern. // we force this section into the 'lonely if' pattern.
// eslint-disable-next-line no-lonely-if // eslint-disable-next-line no-lonely-if
if (multipleQueue) { if (multipleQueue) {
result = await multipleQueue.add(() => fn(...args)); result = await multipleQueue.add(makeSQLJob(fn, callName, jobId, args));
} else if (singleQueue) { } else if (singleQueue) {
makeNewMultipleQueue(); makeNewMultipleQueue();
multipleQueue.pause(); multipleQueue.pause();
@ -77,17 +80,31 @@ function initialize() {
const singleQueueRef = singleQueue; const singleQueueRef = singleQueue;
singleQueue = null; singleQueue = null;
const promise = multipleQueueRef.add(() => fn(...args)); const promise = multipleQueueRef.add(
makeSQLJob(fn, callName, jobId, args)
);
await singleQueueRef.onIdle(); await singleQueueRef.onIdle();
multipleQueueRef.start(); multipleQueueRef.start();
result = await promise; result = await promise;
} else { } else {
makeNewMultipleQueue(); makeNewMultipleQueue();
result = await multipleQueue.add(() => fn(...args)); result = await multipleQueue.add(makeSQLJob(fn, callName, jobId, args));
} }
} }
return result;
}
function initialize() {
if (initialized) {
throw new Error('sqlChannels: already initialized!');
}
initialized = true;
ipcMain.on(SQL_CHANNEL_KEY, async (event, jobId, callName, ...args) => {
try {
const result = await handleCall(callName, jobId, args);
event.sender.send(`${SQL_CHANNEL_KEY}-done`, jobId, null, result); event.sender.send(`${SQL_CHANNEL_KEY}-done`, jobId, null, result);
} catch (error) { } catch (error) {
const errorForDisplay = error && error.stack ? error.stack : error; const errorForDisplay = error && error.stack ? error.stack : error;

View file

@ -21,6 +21,38 @@
concurrency: 1, concurrency: 1,
}); });
deliveryReceiptQueue.pause(); deliveryReceiptQueue.pause();
const deliveryReceiptBatcher = window.Signal.Util.createBatcher({
wait: 500,
maxSize: 500,
processBatch: async items => {
const bySource = _.groupBy(items, item => item.source);
const sources = Object.keys(bySource);
for (let i = 0, max = sources.length; i < max; i += 1) {
const source = sources[i];
const timestamps = bySource[source].map(item => item.timestamp);
try {
const { wrap, sendOptions } = ConversationController.prepareForSend(
source
);
// eslint-disable-next-line no-await-in-loop
await wrap(
textsecure.messaging.sendDeliveryReceipt(
source,
timestamps,
sendOptions
)
);
} catch (error) {
window.log.error(
`Failed to send delivery receipt to ${source} for timestamps ${timestamps}:`,
error && error.stack ? error.stack : error
);
}
}
},
});
// Globally disable drag and drop // Globally disable drag and drop
document.body.addEventListener( document.body.addEventListener(
@ -78,7 +110,6 @@
}; };
// Load these images now to ensure that they don't flicker on first use // Load these images now to ensure that they don't flicker on first use
window.Signal.EmojiLib.preloadImages();
const images = []; const images = [];
function preload(list) { function preload(list) {
for (let index = 0, max = list.length; index < max; index += 1) { for (let index = 0, max = list.length; index < max; index += 1) {
@ -341,9 +372,20 @@
// Stop processing incoming messages // Stop processing incoming messages
if (messageReceiver) { if (messageReceiver) {
await messageReceiver.stopProcessing(); await messageReceiver.stopProcessing();
await window.waitForAllBatchers();
messageReceiver.unregisterBatchers();
messageReceiver = null; messageReceiver = null;
} }
// A number of still-to-queue database queries might be waiting inside batchers.
// We wait for these to empty first, and then shut down the data interface.
await Promise.all([
window.waitForAllBatchers(),
window.waitForAllWaitBatchers(),
]);
// Shut down the data interface cleanly // Shut down the data interface cleanly
await window.Signal.Data.shutdown(); await window.Signal.Data.shutdown();
}, },
@ -850,7 +892,12 @@
} }
if (messageReceiver) { if (messageReceiver) {
messageReceiver.close(); await messageReceiver.stopProcessing();
await window.waitForAllBatchers();
messageReceiver.unregisterBatchers();
messageReceiver = null;
} }
const USERNAME = storage.get('number_id'); const USERNAME = storage.get('number_id');
@ -1022,7 +1069,12 @@
view.applyTheme(); view.applyTheme();
} }
} }
function onEmpty() { async function onEmpty() {
await Promise.all([
window.waitForAllBatchers(),
window.waitForAllWaitBatchers(),
]);
window.log.info('onEmpty: All outstanding database requests complete');
initialLoadComplete = true; initialLoadComplete = true;
window.readyForUpdates(); window.readyForUpdates();
@ -1057,6 +1109,8 @@
} }
} }
function onConfiguration(ev) { function onConfiguration(ev) {
ev.confirm();
const { configuration } = ev; const { configuration } = ev;
const { const {
readReceipts, readReceipts,
@ -1084,11 +1138,11 @@
if (linkPreviews === true || linkPreviews === false) { if (linkPreviews === true || linkPreviews === false) {
storage.put('linkPreviews', linkPreviews); storage.put('linkPreviews', linkPreviews);
} }
ev.confirm();
} }
function onTyping(ev) { function onTyping(ev) {
// Note: this type of message is automatically removed from cache in MessageReceiver
const { typing, sender, senderDevice } = ev; const { typing, sender, senderDevice } = ev;
const { groupId, started } = typing || {}; const { groupId, started } = typing || {};
@ -1118,6 +1172,8 @@
} }
async function onStickerPack(ev) { async function onStickerPack(ev) {
ev.confirm();
const packs = ev.stickerPacks || []; const packs = ev.stickerPacks || [];
packs.forEach(pack => { packs.forEach(pack => {
@ -1149,8 +1205,6 @@
} }
} }
}); });
ev.confirm();
} }
async function onContactReceived(ev) { async function onContactReceived(ev) {
@ -1228,9 +1282,7 @@
conversation.set(newAttributes); conversation.set(newAttributes);
} }
await window.Signal.Data.updateConversation(id, conversation.attributes, { window.Signal.Data.updateConversation(id, conversation.attributes);
Conversation: Whisper.Conversation,
});
const { expireTimer } = details; const { expireTimer } = details;
const isValidExpireTimer = typeof expireTimer === 'number'; const isValidExpireTimer = typeof expireTimer === 'number';
if (isValidExpireTimer) { if (isValidExpireTimer) {
@ -1312,9 +1364,7 @@
conversation.set(newAttributes); conversation.set(newAttributes);
} }
await window.Signal.Data.updateConversation(id, conversation.attributes, { window.Signal.Data.updateConversation(id, conversation.attributes);
Conversation: Whisper.Conversation,
});
const { expireTimer } = details; const { expireTimer } = details;
const isValidExpireTimer = typeof expireTimer === 'number'; const isValidExpireTimer = typeof expireTimer === 'number';
if (!isValidExpireTimer) { if (!isValidExpireTimer) {
@ -1375,20 +1425,31 @@
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
const isProfileUpdate = Boolean(data.message.flags & PROFILE_KEY_UPDATE); const isProfileUpdate = Boolean(data.message.flags & PROFILE_KEY_UPDATE);
if (isProfileUpdate) { if (isProfileUpdate) {
return handleMessageReceivedProfileUpdate({ await handleMessageReceivedProfileUpdate({
data, data,
confirm, confirm,
messageDescriptor, messageDescriptor,
}); });
return;
} }
const message = await initIncomingMessage(data); const isDuplicate = await isMessageDuplicate({
const isDuplicate = await isMessageDuplicate(message); source: data.source,
sourceDevice: data.sourceDevice,
sent_at: data.timestamp,
});
if (isDuplicate) { if (isDuplicate) {
window.log.warn('Received duplicate message', message.idForLogging()); window.log.warn(
return event.confirm(); 'Received duplicate message',
`${data.source}.${data.sourceDevice} ${data.timestamp}`
);
confirm();
return;
} }
// We do this after the duplicate check because it might send a delivery receipt
const message = await initIncomingMessage(data);
const ourNumber = textsecure.storage.user.getNumber(); const ourNumber = textsecure.storage.user.getNumber();
const isGroupUpdate = const isGroupUpdate =
data.message.group && data.message.group &&
@ -1406,7 +1467,8 @@
window.log.warn( window.log.warn(
`Received message destined for group ${conversation.idForLogging()}, which we're not a part of. Dropping.` `Received message destined for group ${conversation.idForLogging()}, which we're not a part of. Dropping.`
); );
return event.confirm(); confirm();
return;
} }
await ConversationController.getOrCreateAndWait( await ConversationController.getOrCreateAndWait(
@ -1414,7 +1476,8 @@
messageDescriptor.type messageDescriptor.type
); );
return message.handleDataMessage(data.message, event.confirm, { // Don't wait for handleDataMessage, as it has its own per-conversation queueing
message.handleDataMessage(data.message, event.confirm, {
initialLoadComplete, initialLoadComplete,
}); });
} }
@ -1433,9 +1496,7 @@
); );
conversation.set({ profileSharing: true }); conversation.set({ profileSharing: true });
await window.Signal.Data.updateConversation(id, conversation.attributes, { window.Signal.Data.updateConversation(id, conversation.attributes);
Conversation: Whisper.Conversation,
});
// Then we update our own profileKey if it's different from what we have // Then we update our own profileKey if it's different from what we have
const ourNumber = textsecure.storage.user.getNumber(); const ourNumber = textsecure.storage.user.getNumber();
@ -1496,7 +1557,7 @@
} }
const message = await createSentMessage(data); const message = await createSentMessage(data);
const existing = await getExistingMessage(message); const existing = await getExistingMessage(message.attributes);
const isUpdate = Boolean(data.isRecipientUpdate); const isUpdate = Boolean(data.isRecipientUpdate);
if (isUpdate && existing) { if (isUpdate && existing) {
@ -1538,16 +1599,17 @@
messageDescriptor.id, messageDescriptor.id,
messageDescriptor.type messageDescriptor.type
); );
await message.handleDataMessage(data.message, event.confirm, {
// Don't wait for handleDataMessage, as it has its own per-conversation queueing
message.handleDataMessage(data.message, event.confirm, {
initialLoadComplete, initialLoadComplete,
}); });
} }
} }
async function getExistingMessage(message) { async function getExistingMessage(data) {
try { try {
const { attributes } = message; const result = await window.Signal.Data.getMessageBySender(data, {
const result = await window.Signal.Data.getMessageBySender(attributes, {
Message: Whisper.Message, Message: Whisper.Message,
}); });
@ -1562,8 +1624,8 @@
} }
} }
async function isMessageDuplicate(message) { async function isMessageDuplicate(data) {
const result = await getExistingMessage(message); const result = await getExistingMessage(data);
return Boolean(result); return Boolean(result);
} }
@ -1587,26 +1649,14 @@
return message; return message;
} }
deliveryReceiptQueue.add(async () => { // Note: We both queue and batch because we want to wait until we are done processing
try { // incoming messages to start sending outgoing delivery receipts. The queue can be
const { wrap, sendOptions } = ConversationController.prepareForSend( // paused easily.
data.source deliveryReceiptQueue.add(() => {
); deliveryReceiptBatcher.add({
await wrap( source: data.source,
textsecure.messaging.sendDeliveryReceipt( timestamp: data.timestamp,
data.source, });
data.timestamp,
sendOptions
)
);
} catch (error) {
window.log.error(
`Failed to send delivery receipt to ${data.source} for message ${
data.timestamp
}:`,
error && error.stack ? error.stack : error
);
}
}); });
return message; return message;
@ -1625,6 +1675,10 @@
if (messageReceiver) { if (messageReceiver) {
await messageReceiver.stopProcessing(); await messageReceiver.stopProcessing();
await window.waitForAllBatchers();
messageReceiver.unregisterBatchers();
messageReceiver = null; messageReceiver = null;
} }
@ -1701,7 +1755,7 @@
} }
const envelope = ev.proto; const envelope = ev.proto;
const message = await initIncomingMessage(envelope, { isError: true }); const message = await initIncomingMessage(envelope, { isError: true });
const isDuplicate = await isMessageDuplicate(message); const isDuplicate = await isMessageDuplicate(message.attributes);
if (isDuplicate) { if (isDuplicate) {
ev.confirm(); ev.confirm();
window.log.warn( window.log.warn(
@ -1710,48 +1764,62 @@
return; return;
} }
const id = await window.Signal.Data.saveMessage(message.attributes, {
Message: Whisper.Message,
});
message.set({ id });
await message.saveErrors(error || new Error('Error was null'));
const conversationId = message.get('conversationId'); const conversationId = message.get('conversationId');
const conversation = await ConversationController.getOrCreateAndWait( const conversation = await ConversationController.getOrCreateAndWait(
conversationId, conversationId,
'private' 'private'
); );
// This matches the queueing behavior used in Message.handleDataMessage
conversation.queueJob(async () => {
const model = new Whisper.Message({
...message.attributes,
id: window.getGuid(),
});
await model.saveErrors(error || new Error('Error was null'), {
skipSave: true,
});
MessageController.register(model.id, model);
await window.Signal.Data.saveMessage(model.attributes, {
Message: Whisper.Message,
forceSave: true,
});
conversation.set({ conversation.set({
active_at: Date.now(), active_at: Date.now(),
unreadCount: conversation.get('unreadCount') + 1, unreadCount: conversation.get('unreadCount') + 1,
}); });
const conversationTimestamp = conversation.get('timestamp'); const conversationTimestamp = conversation.get('timestamp');
const messageTimestamp = message.get('timestamp'); const messageTimestamp = model.get('timestamp');
if (!conversationTimestamp || messageTimestamp > conversationTimestamp) { if (
conversation.set({ timestamp: message.get('sent_at') }); !conversationTimestamp ||
messageTimestamp > conversationTimestamp
) {
conversation.set({ timestamp: model.get('sent_at') });
} }
conversation.trigger('newmessage', message); conversation.trigger('newmessage', model);
conversation.notify(message); conversation.notify(model);
if (ev.confirm) { if (ev.confirm) {
ev.confirm(); ev.confirm();
} }
await window.Signal.Data.updateConversation( window.Signal.Data.updateConversation(
conversationId, conversationId,
conversation.attributes, conversation.attributes
{
Conversation: Whisper.Conversation,
}
); );
});
} }
throw error; throw error;
} }
async function onViewSync(ev) { async function onViewSync(ev) {
ev.confirm();
const { source, timestamp } = ev; const { source, timestamp } = ev;
window.log.info(`view sync ${source} ${timestamp}`); window.log.info(`view sync ${source} ${timestamp}`);
@ -1760,10 +1828,7 @@
timestamp, timestamp,
}); });
sync.on('remove', ev.confirm); Whisper.ViewSyncs.onSync(sync);
// Calling this directly so we can wait for completion
return Whisper.ViewSyncs.onSync(sync);
} }
function onReadReceipt(ev) { function onReadReceipt(ev) {
@ -1772,8 +1837,10 @@
const { reader } = ev.read; const { reader } = ev.read;
window.log.info('read receipt', reader, timestamp); window.log.info('read receipt', reader, timestamp);
ev.confirm();
if (!storage.get('read-receipt-setting')) { if (!storage.get('read-receipt-setting')) {
return ev.confirm(); return;
} }
const receipt = Whisper.ReadReceipts.add({ const receipt = Whisper.ReadReceipts.add({
@ -1782,10 +1849,8 @@
read_at: readAt, read_at: readAt,
}); });
receipt.on('remove', ev.confirm); // Note: We do not wait for completion here
Whisper.ReadReceipts.onReceipt(receipt);
// Calling this directly so we can wait for completion
return Whisper.ReadReceipts.onReceipt(receipt);
} }
function onReadSync(ev) { function onReadSync(ev) {
@ -1802,7 +1867,8 @@
receipt.on('remove', ev.confirm); receipt.on('remove', ev.confirm);
// Calling this directly so we can wait for completion // Note: Here we wait, because we want read states to be in the database
// before we move on.
return Whisper.ReadSyncs.onReceipt(receipt); return Whisper.ReadSyncs.onReceipt(receipt);
} }
@ -1811,6 +1877,10 @@
const key = ev.verified.identityKey; const key = ev.verified.identityKey;
let state; let state;
if (ev.confirm) {
ev.confirm();
}
const c = new Whisper.Conversation({ const c = new Whisper.Conversation({
id: number, id: number,
}); });
@ -1861,10 +1931,6 @@
} else { } else {
await contact.setUnverified(options); await contact.setUnverified(options);
} }
if (ev.confirm) {
ev.confirm();
}
} }
function onDeliveryReceipt(ev) { function onDeliveryReceipt(ev) {
@ -1875,14 +1941,14 @@
deliveryReceipt.timestamp deliveryReceipt.timestamp
); );
ev.confirm();
const receipt = Whisper.DeliveryReceipts.add({ const receipt = Whisper.DeliveryReceipts.add({
timestamp: deliveryReceipt.timestamp, timestamp: deliveryReceipt.timestamp,
source: deliveryReceipt.source, source: deliveryReceipt.source,
}); });
ev.confirm(); // Note: We don't wait for completion here
Whisper.DeliveryReceipts.onReceipt(receipt);
// Calling this directly so we can wait for completion
return Whisper.DeliveryReceipts.onReceipt(receipt);
} }
})(); })();

View file

@ -196,12 +196,9 @@
this.model.set({ this.model.set({
draft: draft.slice(0, MAX_MESSAGE_BODY_LENGTH), draft: draft.slice(0, MAX_MESSAGE_BODY_LENGTH),
}); });
await window.Signal.Data.updateConversation( window.Signal.Data.updateConversation(
conversation.id, conversation.id,
conversation.attributes, conversation.attributes
{
Conversation: Whisper.Conversation,
}
); );
} }
}) })

View file

@ -93,13 +93,12 @@
}); });
if (message.isExpiring() && !expirationStartTimestamp) { if (message.isExpiring() && !expirationStartTimestamp) {
// This will save the message for us while starting the timer await message.setToExpire(false, { skipSave: true });
await message.setToExpire(); }
} else {
await window.Signal.Data.saveMessage(message.attributes, { await window.Signal.Data.saveMessage(message.attributes, {
Message: Whisper.Message, Message: Whisper.Message,
}); });
}
// notify frontend listeners // notify frontend listeners
const conversation = ConversationController.get( const conversation = ConversationController.get(

View file

@ -88,7 +88,7 @@
clearTimeout(timeout); clearTimeout(timeout);
timeout = setTimeout(destroyExpiredMessages, wait); timeout = setTimeout(destroyExpiredMessages, wait);
} }
const throttledCheckExpiringMessages = _.throttle( const debouncedCheckExpiringMessages = _.debounce(
checkExpiringMessages, checkExpiringMessages,
1000 1000
); );
@ -97,9 +97,9 @@
nextExpiration: null, nextExpiration: null,
init(events) { init(events) {
checkExpiringMessages(); checkExpiringMessages();
events.on('timetravel', throttledCheckExpiringMessages); events.on('timetravel', debouncedCheckExpiringMessages);
}, },
update: throttledCheckExpiringMessages, update: debouncedCheckExpiringMessages,
}; };
const TimerOption = Backbone.Model.extend({ const TimerOption = Backbone.Model.extend({

View file

@ -83,7 +83,7 @@
checkTapToViewMessages(); checkTapToViewMessages();
}, wait); }, wait);
} }
const throttledCheckTapToViewMessages = _.throttle( const debouncedCheckTapToViewMessages = _.debounce(
checkTapToViewMessages, checkTapToViewMessages,
1000 1000
); );
@ -92,8 +92,8 @@
nextCheck: null, nextCheck: null,
init(events) { init(events) {
checkTapToViewMessages(); checkTapToViewMessages();
events.on('timetravel', throttledCheckTapToViewMessages); events.on('timetravel', debouncedCheckTapToViewMessages);
}, },
update: throttledCheckTapToViewMessages, update: debouncedCheckTapToViewMessages,
}; };
})(); })();

View file

@ -8,6 +8,7 @@ const _ = require('lodash');
const debuglogs = require('./modules/debuglogs'); const debuglogs = require('./modules/debuglogs');
const Privacy = require('./modules/privacy'); const Privacy = require('./modules/privacy');
const { createBatcher } = require('../ts/util/batcher');
const ipc = electron.ipcRenderer; const ipc = electron.ipcRenderer;
@ -98,13 +99,31 @@ const publish = debuglogs.upload;
// A modern logging interface for the browser // A modern logging interface for the browser
const env = window.getEnvironment();
const IS_PRODUCTION = env === 'production';
const ipcBatcher = createBatcher({
wait: 500,
size: 20,
processBatch: items => {
ipc.send('batch-log', items);
},
});
// The Bunyan API: https://github.com/trentm/node-bunyan#log-method-api // The Bunyan API: https://github.com/trentm/node-bunyan#log-method-api
function logAtLevel(level, prefix, ...args) { function logAtLevel(level, prefix, ...args) {
if (!IS_PRODUCTION) {
console._log(prefix, now(), ...args); console._log(prefix, now(), ...args);
}
const str = cleanArgsForIPC(args); const str = cleanArgsForIPC(args);
const logText = Privacy.redactAll(str); const logText = Privacy.redactAll(str);
ipc.send(`log-${level}`, logText);
ipcBatcher.add({
timestamp: Date.now(),
level,
logText,
});
} }
window.log = { window.log = {

View file

@ -54,6 +54,11 @@
} }
} }
function getById(id) {
const existing = messageLookup[id];
return existing && existing.message ? existing.message : null;
}
function _get() { function _get() {
return messageLookup; return messageLookup;
} }
@ -64,6 +69,7 @@
register, register,
unregister, unregister,
cleanup, cleanup,
getById,
_get, _get,
}; };
})(); })();

View file

@ -101,14 +101,14 @@
this.messageCollection.on('send-error', this.onMessageError, this); this.messageCollection.on('send-error', this.onMessageError, this);
this.throttledBumpTyping = _.throttle(this.bumpTyping, 300); this.throttledBumpTyping = _.throttle(this.bumpTyping, 300);
const debouncedUpdateLastMessage = _.debounce( this.debouncedUpdateLastMessage = _.debounce(
this.updateLastMessage.bind(this), this.updateLastMessage.bind(this),
200 200
); );
this.listenTo( this.listenTo(
this.messageCollection, this.messageCollection,
'add remove destroy', 'add remove destroy',
debouncedUpdateLastMessage this.debouncedUpdateLastMessage
); );
this.listenTo(this.messageCollection, 'sent', this.updateLastMessage); this.listenTo(this.messageCollection, 'sent', this.updateLastMessage);
this.listenTo( this.listenTo(
@ -268,7 +268,7 @@
}, },
async updateAndMerge(message) { async updateAndMerge(message) {
this.updateLastMessage(); this.debouncedUpdateLastMessage();
const mergeMessage = () => { const mergeMessage = () => {
const existing = this.messageCollection.get(message.id); const existing = this.messageCollection.get(message.id);
@ -284,7 +284,7 @@
}, },
async onExpired(message) { async onExpired(message) {
this.updateLastMessage(); this.debouncedUpdateLastMessage();
const removeMessage = () => { const removeMessage = () => {
const { id } = message; const { id } = message;
@ -317,7 +317,7 @@
: `${message.source}.${message.sourceDevice}`; : `${message.source}.${message.sourceDevice}`;
this.clearContactTypingTimer(identifier); this.clearContactTypingTimer(identifier);
await this.updateLastMessage(); this.debouncedUpdateLastMessage();
}, },
addSingleMessage(message) { addSingleMessage(message) {
@ -411,11 +411,7 @@
if (this.get('verified') !== verified) { if (this.get('verified') !== verified) {
this.set({ verified }); this.set({ verified });
window.Signal.Data.updateConversation(this.id, this.attributes);
// we don't await here because we don't need to wait for this to finish
window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation,
});
} }
return; return;
@ -479,9 +475,7 @@
} }
this.set({ verified }); this.set({ verified });
await window.Signal.Data.updateConversation(this.id, this.attributes, { window.Signal.Data.updateConversation(this.id, this.attributes);
Conversation: Whisper.Conversation,
});
// Three situations result in a verification notice in the conversation: // Three situations result in a verification notice in the conversation:
// 1) The message came from an explicit verification in another client (not // 1) The message came from an explicit verification in another client (not
@ -1014,9 +1008,7 @@
draft: null, draft: null,
draftTimestamp: null, draftTimestamp: null,
}); });
await window.Signal.Data.updateConversation(this.id, this.attributes, { window.Signal.Data.updateConversation(this.id, this.attributes);
Conversation: Whisper.Conversation,
});
// We're offline! // We're offline!
if (!textsecure.messaging) { if (!textsecure.messaging) {
@ -1143,10 +1135,9 @@
conversation.set({ conversation.set({
sealedSender: SEALED_SENDER.DISABLED, sealedSender: SEALED_SENDER.DISABLED,
}); });
await window.Signal.Data.updateConversation( window.Signal.Data.updateConversation(
conversation.id, conversation.id,
conversation.attributes, conversation.attributes
{ Conversation: Whisper.Conversation }
); );
} }
}) })
@ -1175,10 +1166,9 @@
sealedSender: SEALED_SENDER.UNRESTRICTED, sealedSender: SEALED_SENDER.UNRESTRICTED,
}); });
} }
await window.Signal.Data.updateConversation( window.Signal.Data.updateConversation(
conversation.id, conversation.id,
conversation.attributes, conversation.attributes
{ Conversation: Whisper.Conversation }
); );
} }
}) })
@ -1299,7 +1289,7 @@
this.set(lastMessageUpdate); this.set(lastMessageUpdate);
if (this.hasChanged()) { if (this.hasChanged()) {
await window.Signal.Data.updateConversation(this.id, this.attributes, { window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation, Conversation: Whisper.Conversation,
}); });
} }
@ -1307,9 +1297,7 @@
async setArchived(isArchived) { async setArchived(isArchived) {
this.set({ isArchived }); this.set({ isArchived });
await window.Signal.Data.updateConversation(this.id, this.attributes, { window.Signal.Data.updateConversation(this.id, this.attributes);
Conversation: Whisper.Conversation,
});
}, },
async updateExpirationTimer( async updateExpirationTimer(
@ -1346,9 +1334,7 @@
const timestamp = (receivedAt || Date.now()) - 1; const timestamp = (receivedAt || Date.now()) - 1;
this.set({ expireTimer }); this.set({ expireTimer });
await window.Signal.Data.updateConversation(this.id, this.attributes, { window.Signal.Data.updateConversation(this.id, this.attributes);
Conversation: Whisper.Conversation,
});
const model = new Whisper.Message({ const model = new Whisper.Message({
// Even though this isn't reflected to the user, we want to place the last seen // Even though this isn't reflected to the user, we want to place the last seen
@ -1516,9 +1502,7 @@
if (this.get('type') === 'group') { if (this.get('type') === 'group') {
const groupNumbers = this.getRecipients(); const groupNumbers = this.getRecipients();
this.set({ left: true }); this.set({ left: true });
await window.Signal.Data.updateConversation(this.id, this.attributes, { window.Signal.Data.updateConversation(this.id, this.attributes);
Conversation: Whisper.Conversation,
});
const model = new Whisper.Message({ const model = new Whisper.Message({
group_update: { left: 'You' }, group_update: { left: 'You' },
@ -1565,14 +1549,9 @@
_.map(oldUnread, async providedM => { _.map(oldUnread, async providedM => {
const m = MessageController.register(providedM.id, providedM); const m = MessageController.register(providedM.id, providedM);
if (!this.messageCollection.get(m.id)) { // Note that this will update the message in the database
window.log.warn(
'Marked a message as read in the database, but ' +
'it was not in messageCollection.'
);
}
await m.markRead(options.readAt); await m.markRead(options.readAt);
const errors = m.get('errors'); const errors = m.get('errors');
return { return {
sender: m.get('source'), sender: m.get('source'),
@ -1588,9 +1567,7 @@
const unreadCount = unreadMessages.length - read.length; const unreadCount = unreadMessages.length - read.length;
this.set({ unreadCount }); this.set({ unreadCount });
await window.Signal.Data.updateConversation(this.id, this.attributes, { window.Signal.Data.updateConversation(this.id, this.attributes);
Conversation: Whisper.Conversation,
});
// If a message has errors, we don't want to send anything out about it. // If a message has errors, we don't want to send anything out about it.
// read syncs - let's wait for a client that really understands the message // read syncs - let's wait for a client that really understands the message
@ -1783,9 +1760,7 @@
} }
if (c.hasChanged()) { if (c.hasChanged()) {
await window.Signal.Data.updateConversation(id, c.attributes, { window.Signal.Data.updateConversation(id, c.attributes);
Conversation: Whisper.Conversation,
});
} }
}, },
async setProfileName(encryptedName) { async setProfileName(encryptedName) {
@ -1860,7 +1835,7 @@
await this.deriveAccessKeyIfNeeded(); await this.deriveAccessKeyIfNeeded();
await window.Signal.Data.updateConversation(this.id, this.attributes, { window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation, Conversation: Whisper.Conversation,
}); });
} }
@ -1883,9 +1858,7 @@
sealedSender: SEALED_SENDER.UNKNOWN, sealedSender: SEALED_SENDER.UNKNOWN,
}); });
await window.Signal.Data.updateConversation(this.id, this.attributes, { window.Signal.Data.updateConversation(this.id, this.attributes);
Conversation: Whisper.Conversation,
});
} }
}, },
@ -1944,9 +1917,7 @@
timestamp: null, timestamp: null,
active_at: null, active_at: null,
}); });
await window.Signal.Data.updateConversation(this.id, this.attributes, { window.Signal.Data.updateConversation(this.id, this.attributes);
Conversation: Whisper.Conversation,
});
await window.Signal.Data.removeAllMessagesInConversation(this.id, { await window.Signal.Data.removeAllMessagesInConversation(this.id, {
MessageCollection: Whisper.MessageCollection, MessageCollection: Whisper.MessageCollection,

View file

@ -1004,7 +1004,9 @@
hasErrors() { hasErrors() {
return _.size(this.get('errors')) > 0; return _.size(this.get('errors')) > 0;
}, },
async saveErrors(providedErrors) { async saveErrors(providedErrors, options = {}) {
const { skipSave } = options;
let errors = providedErrors; let errors = providedErrors;
if (!(errors instanceof Array)) { if (!(errors instanceof Array)) {
@ -1030,11 +1032,16 @@
errors = errors.concat(this.get('errors') || []); errors = errors.concat(this.get('errors') || []);
this.set({ errors }); this.set({ errors });
if (!skipSave) {
await window.Signal.Data.saveMessage(this.attributes, { await window.Signal.Data.saveMessage(this.attributes, {
Message: Whisper.Message, Message: Whisper.Message,
}); });
}
}, },
async markRead(readAt) { async markRead(readAt, options = {}) {
const { skipSave } = options;
this.unset('unread'); this.unset('unread');
if (this.get('expireTimer') && !this.get('expirationStartTimestamp')) { if (this.get('expireTimer') && !this.get('expirationStartTimestamp')) {
@ -1051,9 +1058,11 @@
}) })
); );
if (!skipSave) {
await window.Signal.Data.saveMessage(this.attributes, { await window.Signal.Data.saveMessage(this.attributes, {
Message: Whisper.Message, Message: Whisper.Message,
}); });
}
}, },
isExpiring() { isExpiring() {
return this.get('expireTimer') && this.get('expirationStartTimestamp'); return this.get('expireTimer') && this.get('expirationStartTimestamp');
@ -1074,7 +1083,9 @@
} }
return msFromNow; return msFromNow;
}, },
async setToExpire(force = false) { async setToExpire(force = false, options = {}) {
const { skipSave } = options;
if (this.isExpiring() && (force || !this.get('expires_at'))) { if (this.isExpiring() && (force || !this.get('expires_at'))) {
const start = this.get('expirationStartTimestamp'); const start = this.get('expirationStartTimestamp');
const delta = this.get('expireTimer') * 1000; const delta = this.get('expireTimer') * 1000;
@ -1082,7 +1093,7 @@
this.set({ expires_at: expiresAt }); this.set({ expires_at: expiresAt });
const id = this.get('id'); const id = this.get('id');
if (id) { if (id && !skipSave) {
await window.Signal.Data.saveMessage(this.attributes, { await window.Signal.Data.saveMessage(this.attributes, {
Message: Whisper.Message, Message: Whisper.Message,
}); });
@ -1664,10 +1675,6 @@
sticker, sticker,
}); });
await window.Signal.Data.saveMessage(this.attributes, {
Message: Whisper.Message,
});
return true; return true;
} }
@ -1880,6 +1887,7 @@
} }
message.set({ message.set({
id: window.getGuid(),
attachments: dataMessage.attachments, attachments: dataMessage.attachments,
body: dataMessage.body, body: dataMessage.body,
contact: dataMessage.contact, contact: dataMessage.contact,
@ -2024,8 +2032,8 @@
!conversationTimestamp || !conversationTimestamp ||
message.get('sent_at') > conversationTimestamp message.get('sent_at') > conversationTimestamp
) { ) {
conversation.lastMessage = message.getNotificationText();
conversation.set({ conversation.set({
lastMessage: message.getNotificationText(),
timestamp: message.get('sent_at'), timestamp: message.get('sent_at'),
}); });
} }
@ -2045,12 +2053,6 @@
} }
} }
const id = await window.Signal.Data.saveMessage(message.attributes, {
Message: Whisper.Message,
});
message.set({ id });
MessageController.register(message.id, message);
if (message.isTapToView() && type === 'outgoing') { if (message.isTapToView() && type === 'outgoing') {
await message.eraseContents(); await message.eraseContents();
} }
@ -2076,56 +2078,24 @@
} }
} }
MessageController.register(message.id, message);
window.Signal.Data.updateConversation(
conversationId,
conversation.attributes
);
if (message.isUnsupportedMessage()) { if (message.isUnsupportedMessage()) {
await message.eraseContents(); await message.eraseContents();
} else {
// Note that this can save the message again, if jobs were queued. We need to
// call it after we have an id for this message, because the jobs refer back
// to their source message.
await message.queueAttachmentDownloads();
} }
await window.Signal.Data.updateConversation( await message.queueAttachmentDownloads();
conversationId, await window.Signal.Data.saveMessage(message.attributes, {
conversation.attributes, Message: Whisper.Message,
{ Conversation: Whisper.Conversation } forceSave: true,
); });
conversation.trigger('newmessage', message); conversation.trigger('newmessage', message);
try {
// We go to the database here because, between the message save above and
// the previous line's trigger() call, we might have marked all messages
// unread in the database. This message might already be read!
const fetched = await window.Signal.Data.getMessageById(
message.get('id'),
{
Message: Whisper.Message,
}
);
const previousUnread = message.get('unread');
// Important to update message with latest read state from database
message.merge(fetched);
if (previousUnread !== message.get('unread')) {
window.log.warn(
'Caught race condition on new message read state! ' +
'Manually starting timers.'
);
// We call markRead() even though the message is already
// marked read because we need to start expiration
// timers, etc.
message.markRead();
}
} catch (error) {
window.log.warn(
'handleDataMessage: Message',
message.idForLogging(),
'was deleted'
);
}
if (message.get('unread')) { if (message.get('unread')) {
await conversation.notify(message); await conversation.notify(message);
} }

View file

@ -166,9 +166,11 @@ async function _runJob(job) {
); );
} }
const found = await getMessageById(messageId, { const found =
MessageController.getById(messageId) ||
(await getMessageById(messageId, {
Message: Whisper.Message, Message: Whisper.Message,
}); }));
if (!found) { if (!found) {
logger.error('_runJob: Source message not found, deleting job'); logger.error('_runJob: Source message not found, deleting job');
await _finishJob(null, id); await _finishJob(null, id);
@ -434,13 +436,7 @@ async function _addAttachmentToMessage(message, attachment, { type, index }) {
hash: await computeHash(loadedAttachment.data), hash: await computeHash(loadedAttachment.data),
}, },
}); });
await Signal.Data.updateConversation( Signal.Data.updateConversation(conversationId, conversation.attributes);
conversationId,
conversation.attributes,
{
Conversation: Whisper.Conversation,
}
);
return; return;
} }

View file

@ -6,14 +6,17 @@ const {
cloneDeep, cloneDeep,
forEach, forEach,
get, get,
groupBy,
isFunction, isFunction,
isObject, isObject,
last,
map, map,
set, set,
} = require('lodash'); } = require('lodash');
const { base64ToArrayBuffer, arrayBufferToBase64 } = require('./crypto'); const { base64ToArrayBuffer, arrayBufferToBase64 } = require('./crypto');
const MessageType = require('./types/message'); const MessageType = require('./types/message');
const { createBatcher } = require('../../ts/util/batcher');
const { ipcRenderer } = electron; const { ipcRenderer } = electron;
@ -78,6 +81,7 @@ module.exports = {
removeAllItems, removeAllItems,
createOrUpdateSession, createOrUpdateSession,
createOrUpdateSessions,
getSessionById, getSessionById,
getSessionsByNumber, getSessionsByNumber,
bulkAddSessions, bulkAddSessions,
@ -91,6 +95,7 @@ module.exports = {
saveConversations, saveConversations,
getConversationById, getConversationById,
updateConversation, updateConversation,
updateConversations,
removeConversation, removeConversation,
_removeConversations, _removeConversations,
@ -134,6 +139,7 @@ module.exports = {
saveUnprocesseds, saveUnprocesseds,
updateUnprocessedAttempts, updateUnprocessedAttempts,
updateUnprocessedWithData, updateUnprocessedWithData,
updateUnprocessedsWithData,
removeUnprocessed, removeUnprocessed,
removeAllUnprocessed, removeAllUnprocessed,
@ -205,20 +211,21 @@ function _cleanData(data) {
} }
async function _shutdown() { async function _shutdown() {
const jobKeys = Object.keys(_jobs);
window.log.info(
`data.shutdown: shutdown requested. ${jobKeys.length} jobs outstanding`
);
if (_shutdownPromise) { if (_shutdownPromise) {
return _shutdownPromise; await _shutdownPromise;
return;
} }
_shuttingDown = true; _shuttingDown = true;
const jobKeys = Object.keys(_jobs);
window.log.info(
`data.shutdown: starting process. ${jobKeys.length} jobs outstanding`
);
// No outstanding jobs, return immediately // No outstanding jobs, return immediately
if (jobKeys.length === 0) { if (jobKeys.length === 0 || _DEBUG) {
return null; return;
} }
// Outstanding jobs; we need to wait until the last one is done // Outstanding jobs; we need to wait until the last one is done
@ -233,7 +240,7 @@ async function _shutdown() {
}; };
}); });
return _shutdownPromise; await _shutdownPromise;
} }
function _makeJob(fnName) { function _makeJob(fnName) {
@ -268,7 +275,7 @@ function _updateJob(id, data) {
_removeJob(id); _removeJob(id);
const end = Date.now(); const end = Date.now();
const delta = end - start; const delta = end - start;
if (delta > 10) { if (delta > 10 || _DEBUG) {
window.log.info( window.log.info(
`SQL channel job ${id} (${fnName}) succeeded in ${end - start}ms` `SQL channel job ${id} (${fnName}) succeeded in ${end - start}ms`
); );
@ -556,6 +563,9 @@ async function removeAllItems() {
async function createOrUpdateSession(data) { async function createOrUpdateSession(data) {
await channels.createOrUpdateSession(data); await channels.createOrUpdateSession(data);
} }
async function createOrUpdateSessions(items) {
await channels.createOrUpdateSessions(items);
}
async function getSessionById(id) { async function getSessionById(id) {
const session = await channels.getSessionById(id); const session = await channels.getSessionById(id);
return session; return session;
@ -600,17 +610,25 @@ async function getConversationById(id, { Conversation }) {
return new Conversation(data); return new Conversation(data);
} }
async function updateConversation(id, data, { Conversation }) { const updateConversationBatcher = createBatcher({
const existing = await getConversationById(id, { Conversation }); wait: 500,
if (!existing) { maxSize: 20,
throw new Error(`Conversation ${id} does not exist!`); processBatch: async items => {
// We only care about the most recent update for each conversation
const byId = groupBy(items, item => item.id);
const ids = Object.keys(byId);
const mostRecent = ids.map(id => last(byId[id]));
await updateConversations(mostRecent);
},
});
function updateConversation(id, data) {
updateConversationBatcher.add(data);
} }
const merged = { async function updateConversations(data) {
...existing.attributes, await channels.updateConversations(data);
...data,
};
await channels.updateConversation(merged);
} }
async function removeConversation(id, { Conversation }) { async function removeConversation(id, { Conversation }) {
@ -932,6 +950,9 @@ async function updateUnprocessedAttempts(id, attempts) {
async function updateUnprocessedWithData(id, data) { async function updateUnprocessedWithData(id, data) {
await channels.updateUnprocessedWithData(id, data); await channels.updateUnprocessedWithData(id, data);
} }
async function updateUnprocessedsWithData(items) {
await channels.updateUnprocessedsWithData(items);
}
async function removeUnprocessed(id) { async function removeUnprocessed(id) {
await channels.removeUnprocessed(id); await channels.removeUnprocessed(id);

View file

@ -50,11 +50,7 @@ exports.createConversation = async ({
unread: numMessages, unread: numMessages,
}); });
const conversationId = conversation.get('id'); const conversationId = conversation.get('id');
await Signal.Data.updateConversation( Signal.Data.updateConversation(conversationId, conversation.attributes);
conversationId,
conversation.attributes,
{ Conversation: Whisper.Conversation }
);
await Promise.all( await Promise.all(
range(0, numMessages).map(async index => { range(0, numMessages).map(async index => {

View file

@ -117,11 +117,11 @@ async function migrateToSQL({
if (item.envelope) { if (item.envelope) {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
item.envelope = await arrayBufferToString(item.envelope); item.envelope = arrayBufferToString(item.envelope);
} }
if (item.decrypted) { if (item.decrypted) {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
item.decrypted = await arrayBufferToString(item.decrypted); item.decrypted = arrayBufferToString(item.decrypted);
} }
}) })
); );

View file

@ -98,13 +98,12 @@
}); });
if (message.isExpiring() && !expirationStartTimestamp) { if (message.isExpiring() && !expirationStartTimestamp) {
// This will save the message for us while starting the timer await message.setToExpire(false, { skipSave: true });
await message.setToExpire(); }
} else {
await window.Signal.Data.saveMessage(message.attributes, { await window.Signal.Data.saveMessage(message.attributes, {
Message: Whisper.Message, Message: Whisper.Message,
}); });
}
// notify frontend listeners // notify frontend listeners
const conversation = ConversationController.get( const conversation = ConversationController.get(

View file

@ -41,23 +41,14 @@
const notificationForMessage = found const notificationForMessage = found
? Whisper.Notifications.findWhere({ messageId: found.id }) ? Whisper.Notifications.findWhere({ messageId: found.id })
: null; : null;
const removedNotification = Whisper.Notifications.remove( Whisper.Notifications.remove(notificationForMessage);
notificationForMessage
);
const receiptSender = receipt.get('sender');
const receiptTimestamp = receipt.get('timestamp');
const wasMessageFound = Boolean(found);
const wasNotificationFound = Boolean(notificationForMessage);
const wasNotificationRemoved = Boolean(removedNotification);
window.log.info('Receive read sync:', {
receiptSender,
receiptTimestamp,
wasMessageFound,
wasNotificationFound,
wasNotificationRemoved,
});
if (!found) { if (!found) {
window.log.info(
'No message for read sync',
receipt.get('sender'),
receipt.get('timestamp')
);
return; return;
} }
@ -68,7 +59,7 @@
// timer to the time specified by the read sync if it's earlier than // timer to the time specified by the read sync if it's earlier than
// the previous read time. // the previous read time.
if (message.isUnread()) { if (message.isUnread()) {
await message.markRead(readAt); await message.markRead(readAt, { skipSave: true });
// onReadMessage may result in messages older than this one being // onReadMessage may result in messages older than this one being
// marked read. We want those messages to have the same expire timer // marked read. We want those messages to have the same expire timer
@ -87,7 +78,7 @@
message.set({ expirationStartTimestamp }); message.set({ expirationStartTimestamp });
const force = true; const force = true;
await message.setToExpire(force); await message.setToExpire(force, { skipSave: true });
const conversation = message.getConversation(); const conversation = message.getConversation();
if (conversation) { if (conversation) {
@ -95,6 +86,10 @@
} }
} }
await window.Signal.Data.saveMessage(message.attributes, {
Message: Whisper.Message,
});
this.remove(receipt); this.remove(receipt);
} catch (error) { } catch (error) {
window.log.error( window.log.error(

View file

@ -147,9 +147,31 @@
}, },
}); });
function SignalProtocolStore() {} function SignalProtocolStore() {
this.sessionUpdateBatcher = window.Signal.Util.createBatcher({
wait: 500,
maxSize: 20,
processBatch: async items => {
// We only care about the most recent update for each session
const byId = _.groupBy(items, item => item.id);
const ids = Object.keys(byId);
const mostRecent = ids.map(id => {
const item = _.last(byId[id]);
return {
...item,
record: item.record.serialize(),
};
});
await window.Signal.Data.createOrUpdateSessions(mostRecent);
},
});
}
async function _hydrateCache(object, field, itemsPromise, idField) {
const items = await itemsPromise;
async function _hydrateCache(object, field, items, idField) {
const cache = Object.create(null); const cache = Object.create(null);
for (let i = 0, max = items.length; i < max; i += 1) { for (let i = 0, max = items.length; i < max; i += 1) {
const item = items[i]; const item = items[i];
@ -167,48 +189,53 @@
constructor: SignalProtocolStore, constructor: SignalProtocolStore,
async hydrateCaches() { async hydrateCaches() {
await Promise.all([ await Promise.all([
(async () => {
const item = await window.Signal.Data.getItemById('identityKey');
this.ourIdentityKey = item ? item.value : undefined;
})(),
(async () => {
const item = await window.Signal.Data.getItemById('registrationId');
this.ourRegistrationId = item ? item.value : undefined;
})(),
_hydrateCache( _hydrateCache(
this, this,
'identityKeys', 'identityKeys',
await window.Signal.Data.getAllIdentityKeys(), window.Signal.Data.getAllIdentityKeys(),
'id' 'id'
), ),
_hydrateCache( _hydrateCache(
this, this,
'sessions', 'sessions',
await window.Signal.Data.getAllSessions(), (async () => {
const sessions = await window.Signal.Data.getAllSessions();
return sessions.map(item => ({
...item,
record: libsignal.SessionRecord.deserialize(item.record),
}));
})(),
'id' 'id'
), ),
_hydrateCache( _hydrateCache(
this, this,
'preKeys', 'preKeys',
await window.Signal.Data.getAllPreKeys(), window.Signal.Data.getAllPreKeys(),
'id' 'id'
), ),
_hydrateCache( _hydrateCache(
this, this,
'signedPreKeys', 'signedPreKeys',
await window.Signal.Data.getAllSignedPreKeys(), window.Signal.Data.getAllSignedPreKeys(),
'id' 'id'
), ),
]); ]);
}, },
async getIdentityKeyPair() { async getIdentityKeyPair() {
const item = await window.Signal.Data.getItemById('identityKey'); return this.ourIdentityKey;
if (item) {
return item.value;
}
return undefined;
}, },
async getLocalRegistrationId() { async getLocalRegistrationId() {
const item = await window.Signal.Data.getItemById('registrationId'); return this.ourRegistrationId;
if (item) {
return item.value;
}
return undefined;
}, },
// PreKeys // PreKeys
@ -337,7 +364,10 @@
}; };
this.sessions[encodedNumber] = data; this.sessions[encodedNumber] = data;
await window.Signal.Data.createOrUpdateSession(data);
// Note: Because these are cached in memory, we batch and make these database
// updates out of band.
this.sessionUpdateBatcher.add(data);
}, },
async getDeviceIds(number) { async getDeviceIds(number) {
if (number === null || number === undefined) { if (number === null || number === undefined) {
@ -845,14 +875,24 @@
forceSave: true, forceSave: true,
}); });
}, },
addMultipleUnprocessed(array) {
// We need to pass forceSave because the data has an id already, which will cause
// an update instead of an insert.
return window.Signal.Data.saveUnprocesseds(array, {
forceSave: true,
});
},
updateUnprocessedAttempts(id, attempts) { updateUnprocessedAttempts(id, attempts) {
return window.Signal.Data.updateUnprocessedAttempts(id, attempts); return window.Signal.Data.updateUnprocessedAttempts(id, attempts);
}, },
updateUnprocessedWithData(id, data) { updateUnprocessedWithData(id, data) {
return window.Signal.Data.updateUnprocessedWithData(id, data); return window.Signal.Data.updateUnprocessedWithData(id, data);
}, },
removeUnprocessed(id) { updateUnprocessedsWithData(items) {
return window.Signal.Data.removeUnprocessed(id); return window.Signal.Data.updateUnprocessedsWithData(items);
},
removeUnprocessed(idOrArray) {
return window.Signal.Data.removeUnprocessed(idOrArray);
}, },
removeAllUnprocessed() { removeAllUnprocessed() {
return window.Signal.Data.removeAllUnprocessed(); return window.Signal.Data.removeAllUnprocessed();

View file

@ -1,44 +0,0 @@
/* global dcodeIO */
/* eslint-disable strict */
'use strict';
const functions = {
stringToArrayBufferBase64,
arrayBufferToStringBase64,
};
onmessage = async e => {
const [jobId, fnName, ...args] = e.data;
try {
const fn = functions[fnName];
if (!fn) {
throw new Error(`Worker: job ${jobId} did not find function ${fnName}`);
}
const result = await fn(...args);
postMessage([jobId, null, result]);
} catch (error) {
const errorForDisplay = prepareErrorForPostMessage(error);
postMessage([jobId, errorForDisplay]);
}
};
function prepareErrorForPostMessage(error) {
if (!error) {
return null;
}
if (error.stack) {
return error.stack;
}
return error.message;
}
function stringToArrayBufferBase64(string) {
return dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer();
}
function arrayBufferToStringBase64(arrayBuffer) {
return dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');
}

View file

@ -944,7 +944,7 @@
}, },
async saveModel() { async saveModel() {
await window.Signal.Data.updateConversation( window.Signal.Data.updateConversation(
this.model.id, this.model.id,
this.model.attributes, this.model.attributes,
{ {

View file

@ -493,6 +493,7 @@
const regionCode = libphonenumber.util.getRegionCodeForNumber(number); const regionCode = libphonenumber.util.getRegionCodeForNumber(number);
await textsecure.storage.put('regionCode', regionCode); await textsecure.storage.put('regionCode', regionCode);
await textsecure.storage.protocol.hydrateCaches();
}, },
async clearSessionsAndPreKeys() { async clearSessionsAndPreKeys() {
const store = textsecure.storage.protocol; const store = textsecure.storage.protocol;

View file

@ -35766,6 +35766,8 @@ Internal.SessionRecord = function() {
return SessionRecord; return SessionRecord;
}(); }();
libsignal.SessionRecord = Internal.SessionRecord;
function SignalProtocolAddress(name, deviceId) { function SignalProtocolAddress(name, deviceId) {
this.name = name; this.name = name;
this.deviceId = deviceId; this.deviceId = deviceId;
@ -35844,18 +35846,15 @@ SessionBuilder.prototype = {
}); });
}.bind(this)).then(function(session) { }.bind(this)).then(function(session) {
var address = this.remoteAddress.toString(); var address = this.remoteAddress.toString();
return this.storage.loadSession(address).then(function(serialized) { return this.storage.loadSession(address).then(function(record) {
var record; if (record === undefined) {
if (serialized !== undefined) {
record = Internal.SessionRecord.deserialize(serialized);
} else {
record = new Internal.SessionRecord(); record = new Internal.SessionRecord();
} }
record.archiveCurrentState(); record.archiveCurrentState();
record.updateSessionState(session); record.updateSessionState(session);
return Promise.all([ return Promise.all([
this.storage.storeSession(address, record.serialize()), this.storage.storeSession(address, record),
this.storage.saveIdentity(this.remoteAddress.toString(), device.identityKey) this.storage.saveIdentity(this.remoteAddress.toString(), device.identityKey)
]); ]);
}.bind(this)); }.bind(this));
@ -36039,12 +36038,7 @@ function SessionCipher(storage, remoteAddress) {
SessionCipher.prototype = { SessionCipher.prototype = {
getRecord: function(encodedNumber) { getRecord: function(encodedNumber) {
return this.storage.loadSession(encodedNumber).then(function(serialized) { return this.storage.loadSession(encodedNumber);
if (serialized === undefined) {
return undefined;
}
return Internal.SessionRecord.deserialize(serialized);
});
}, },
// encoding is an optional parameter - wrap() will only translate if one is provided // encoding is an optional parameter - wrap() will only translate if one is provided
encrypt: function(buffer, encoding) { encrypt: function(buffer, encoding) {
@ -36124,7 +36118,7 @@ SessionCipher.prototype = {
return this.storage.saveIdentity(this.remoteAddress.toString(), theirIdentityKey); return this.storage.saveIdentity(this.remoteAddress.toString(), theirIdentityKey);
}.bind(this)).then(function() { }.bind(this)).then(function() {
record.updateSessionState(session); record.updateSessionState(session);
return this.storage.storeSession(address, record.serialize()).then(function() { return this.storage.storeSession(address, record).then(function() {
return result; return result;
}); });
}.bind(this)); }.bind(this));
@ -36211,7 +36205,7 @@ SessionCipher.prototype = {
return this.storage.saveIdentity(this.remoteAddress.toString(), result.session.indexInfo.remoteIdentityKey); return this.storage.saveIdentity(this.remoteAddress.toString(), result.session.indexInfo.remoteIdentityKey);
}.bind(this)).then(function() { }.bind(this)).then(function() {
record.updateSessionState(result.session); record.updateSessionState(result.session);
return this.storage.storeSession(address, record.serialize()).then(function() { return this.storage.storeSession(address, record).then(function() {
return result.plaintext; return result.plaintext;
}); });
}.bind(this)); }.bind(this));
@ -36246,7 +36240,7 @@ SessionCipher.prototype = {
preKeyProto.message.toArrayBuffer(), session preKeyProto.message.toArrayBuffer(), session
).then(function(plaintext) { ).then(function(plaintext) {
record.updateSessionState(session); record.updateSessionState(session);
return this.storage.storeSession(address, record.serialize()).then(function() { return this.storage.storeSession(address, record).then(function() {
if (preKeyId !== undefined && preKeyId !== null) { if (preKeyId !== undefined && preKeyId !== null) {
return this.storage.removePreKey(preKeyId); return this.storage.removePreKey(preKeyId);
} }
@ -36444,7 +36438,7 @@ SessionCipher.prototype = {
} }
record.archiveCurrentState(); record.archiveCurrentState();
return this.storage.storeSession(address, record.serialize()); return this.storage.storeSession(address, record);
}.bind(this)); }.bind(this));
}.bind(this)); }.bind(this));
}, },
@ -36458,7 +36452,7 @@ SessionCipher.prototype = {
} }
record.deleteAllSessions(); record.deleteAllSessions();
return this.storage.storeSession(address, record.serialize()); return this.storage.storeSession(address, record);
}.bind(this)); }.bind(this));
}.bind(this)); }.bind(this));
} }

View file

@ -9,113 +9,11 @@
/* global _: false */ /* global _: false */
/* global ContactBuffer: false */ /* global ContactBuffer: false */
/* global GroupBuffer: false */ /* global GroupBuffer: false */
/* global Worker: false */
/* eslint-disable more/no-then */ /* eslint-disable more/no-then */
const WORKER_TIMEOUT = 60 * 1000; // one minute
const RETRY_TIMEOUT = 2 * 60 * 1000; const RETRY_TIMEOUT = 2 * 60 * 1000;
const _utilWorker = new Worker('js/util_worker.js');
const _jobs = Object.create(null);
const _DEBUG = false;
let _jobCounter = 0;
function _makeJob(fnName) {
_jobCounter += 1;
const id = _jobCounter;
if (_DEBUG) {
window.log.info(`Worker job ${id} (${fnName}) started`);
}
_jobs[id] = {
fnName,
start: Date.now(),
};
return id;
}
function _updateJob(id, data) {
const { resolve, reject } = data;
const { fnName, start } = _jobs[id];
_jobs[id] = {
..._jobs[id],
...data,
resolve: value => {
_removeJob(id);
const end = Date.now();
window.log.info(
`Worker job ${id} (${fnName}) succeeded in ${end - start}ms`
);
return resolve(value);
},
reject: error => {
_removeJob(id);
const end = Date.now();
window.log.info(
`Worker job ${id} (${fnName}) failed in ${end - start}ms`
);
return reject(error);
},
};
}
function _removeJob(id) {
if (_DEBUG) {
_jobs[id].complete = true;
} else {
delete _jobs[id];
}
}
function _getJob(id) {
return _jobs[id];
}
async function callWorker(fnName, ...args) {
const jobId = _makeJob(fnName);
return new Promise((resolve, reject) => {
_utilWorker.postMessage([jobId, fnName, ...args]);
_updateJob(jobId, {
resolve,
reject,
args: _DEBUG ? args : null,
});
setTimeout(
() => reject(new Error(`Worker job ${jobId} (${fnName}) timed out`)),
WORKER_TIMEOUT
);
});
}
_utilWorker.onmessage = e => {
const [jobId, errorForDisplay, result] = e.data;
const job = _getJob(jobId);
if (!job) {
throw new Error(
`Received worker reply to job ${jobId}, but did not have it in our registry!`
);
}
const { resolve, reject, fnName } = job;
if (errorForDisplay) {
return reject(
new Error(
`Error received from worker job ${jobId} (${fnName}): ${errorForDisplay}`
)
);
}
return resolve(result);
};
function MessageReceiver(username, password, signalingKey, options = {}) { function MessageReceiver(username, password, signalingKey, options = {}) {
this.count = 0; this.count = 0;
@ -135,24 +33,39 @@ function MessageReceiver(username, password, signalingKey, options = {}) {
this.number = address.getName(); this.number = address.getName();
this.deviceId = address.getDeviceId(); this.deviceId = address.getDeviceId();
this.pendingQueue = new window.PQueue({ concurrency: 1 });
this.incomingQueue = new window.PQueue({ concurrency: 1 }); this.incomingQueue = new window.PQueue({ concurrency: 1 });
this.pendingQueue = new window.PQueue({ concurrency: 1 });
this.appQueue = new window.PQueue({ concurrency: 1 }); this.appQueue = new window.PQueue({ concurrency: 1 });
this.cacheAddBatcher = window.Signal.Util.createBatcher({
wait: 200,
maxSize: 30,
processBatch: this.cacheAndQueueBatch.bind(this),
});
this.cacheUpdateBatcher = window.Signal.Util.createBatcher({
wait: 500,
maxSize: 30,
processBatch: this.cacheUpdateBatch.bind(this),
});
this.cacheRemoveBatcher = window.Signal.Util.createBatcher({
wait: 500,
maxSize: 30,
processBatch: this.cacheRemoveBatch.bind(this),
});
if (options.retryCached) { if (options.retryCached) {
this.pendingQueue.add(() => this.queueAllCached()); this.pendingQueue.add(() => this.queueAllCached());
} }
} }
MessageReceiver.stringToArrayBuffer = string => MessageReceiver.stringToArrayBuffer = string =>
Promise.resolve(dcodeIO.ByteBuffer.wrap(string, 'binary').toArrayBuffer()); dcodeIO.ByteBuffer.wrap(string, 'binary').toArrayBuffer();
MessageReceiver.arrayBufferToString = arrayBuffer => MessageReceiver.arrayBufferToString = arrayBuffer =>
Promise.resolve(dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('binary')); dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('binary');
MessageReceiver.stringToArrayBufferBase64 = string => MessageReceiver.stringToArrayBufferBase64 = string =>
callWorker('stringToArrayBufferBase64', string); dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer();
MessageReceiver.arrayBufferToStringBase64 = arrayBuffer => MessageReceiver.arrayBufferToStringBase64 = arrayBuffer =>
callWorker('arrayBufferToStringBase64', arrayBuffer); dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');
MessageReceiver.prototype = new textsecure.EventTarget(); MessageReceiver.prototype = new textsecure.EventTarget();
MessageReceiver.prototype.extend({ MessageReceiver.prototype.extend({
@ -197,6 +110,12 @@ MessageReceiver.prototype.extend({
this.stoppingProcessing = true; this.stoppingProcessing = true;
return this.close(); return this.close();
}, },
unregisterBatchers() {
window.log.info('MessageReceiver: unregister batchers');
this.cacheAddBatcher.unregister();
this.cacheUpdateBatcher.unregister();
this.cacheRemoveBatcher.unregister();
},
shutdown() { shutdown() {
if (this.socket) { if (this.socket) {
this.socket.onclose = null; this.socket.onclose = null;
@ -308,20 +227,7 @@ MessageReceiver.prototype.extend({
? envelope.serverTimestamp.toNumber() ? envelope.serverTimestamp.toNumber()
: null; : null;
try { this.cacheAndQueue(envelope, plaintext, request);
await this.addToCache(envelope, plaintext);
request.respond(200, 'OK');
this.queueEnvelope(envelope);
this.clearRetryTimeout();
this.maybeScheduleRetryTimeout();
} catch (error) {
request.respond(500, 'Failed to cache message');
window.log.error(
'handleRequest error trying to add message to cache:',
error && error.stack ? error.stack : error
);
}
} catch (e) { } catch (e) {
request.respond(500, 'Bad encrypted websocket message'); request.respond(500, 'Bad encrypted websocket message');
window.log.error( window.log.error(
@ -377,7 +283,12 @@ MessageReceiver.prototype.extend({
this.count = 0; this.count = 0;
}; };
const waitForCacheAddBatcher = async () => {
await this.cacheAddBatcher.onIdle();
this.incomingQueue.add(waitForIncomingQueue); this.incomingQueue.add(waitForIncomingQueue);
};
waitForCacheAddBatcher();
}, },
drain() { drain() {
const waitForIncomingQueue = () => const waitForIncomingQueue = () =>
@ -408,13 +319,13 @@ MessageReceiver.prototype.extend({
let envelopePlaintext = item.envelope; let envelopePlaintext = item.envelope;
if (item.version === 2) { if (item.version === 2) {
envelopePlaintext = await MessageReceiver.stringToArrayBufferBase64( envelopePlaintext = MessageReceiver.stringToArrayBufferBase64(
envelopePlaintext envelopePlaintext
); );
} }
if (typeof envelopePlaintext === 'string') { if (typeof envelopePlaintext === 'string') {
envelopePlaintext = await MessageReceiver.stringToArrayBuffer( envelopePlaintext = MessageReceiver.stringToArrayBuffer(
envelopePlaintext envelopePlaintext
); );
} }
@ -430,13 +341,13 @@ MessageReceiver.prototype.extend({
let payloadPlaintext = decrypted; let payloadPlaintext = decrypted;
if (item.version === 2) { if (item.version === 2) {
payloadPlaintext = await MessageReceiver.stringToArrayBufferBase64( payloadPlaintext = MessageReceiver.stringToArrayBufferBase64(
payloadPlaintext payloadPlaintext
); );
} }
if (typeof payloadPlaintext === 'string') { if (typeof payloadPlaintext === 'string') {
payloadPlaintext = await MessageReceiver.stringToArrayBuffer( payloadPlaintext = MessageReceiver.stringToArrayBuffer(
payloadPlaintext payloadPlaintext
); );
} }
@ -530,44 +441,61 @@ MessageReceiver.prototype.extend({
}) })
); );
}, },
async addToCache(envelope, plaintext) { async cacheAndQueueBatch(items) {
const dataArray = items.map(item => item.data);
try {
await textsecure.storage.unprocessed.batchAdd(dataArray);
items.forEach(item => {
item.request.respond(200, 'OK');
this.queueEnvelope(item.envelope);
});
this.clearRetryTimeout();
this.maybeScheduleRetryTimeout();
} catch (error) {
items.forEach(item => {
item.request.respond(500, 'Failed to cache message');
});
window.log.error(
'cacheAndQueue error trying to add messages to cache:',
error && error.stack ? error.stack : error
);
}
},
cacheAndQueue(envelope, plaintext, request) {
const { id } = envelope; const { id } = envelope;
const data = { const data = {
id, id,
version: 2, version: 2,
envelope: await MessageReceiver.arrayBufferToStringBase64(plaintext), envelope: MessageReceiver.arrayBufferToStringBase64(plaintext),
timestamp: Date.now(), timestamp: Date.now(),
attempts: 1, attempts: 1,
}; };
return textsecure.storage.unprocessed.add(data); this.cacheAddBatcher.add({
request,
envelope,
data,
});
}, },
async updateCache(envelope, plaintext) { async cacheUpdateBatch(items) {
await textsecure.storage.unprocessed.addDecryptedDataToList(items);
},
updateCache(envelope, plaintext) {
const { id } = envelope; const { id } = envelope;
const item = await textsecure.storage.unprocessed.get(id); const data = {
if (!item) { source: envelope.source,
window.log.error( sourceDevice: envelope.sourceDevice,
`updateCache: Didn't find item ${id} in cache to update` serverTimestamp: envelope.serverTimestamp,
); decrypted: MessageReceiver.arrayBufferToStringBase64(plaintext),
return null; };
} this.cacheUpdateBatcher.add({ id, data });
},
item.source = envelope.source; async cacheRemoveBatch(items) {
item.sourceDevice = envelope.sourceDevice; await textsecure.storage.unprocessed.remove(items);
item.serverTimestamp = envelope.serverTimestamp;
if (item.version === 2) {
item.decrypted = await MessageReceiver.arrayBufferToStringBase64(
plaintext
);
} else {
item.decrypted = await MessageReceiver.arrayBufferToString(plaintext);
}
return textsecure.storage.unprocessed.addDecryptedData(item.id, item);
}, },
removeFromCache(envelope) { removeFromCache(envelope) {
const { id } = envelope; const { id } = envelope;
return textsecure.storage.unprocessed.remove(id); this.cacheRemoveBatcher.add(id);
}, },
queueDecryptedEnvelope(envelope, plaintext) { queueDecryptedEnvelope(envelope, plaintext) {
const id = this.getEnvelopeId(envelope); const id = this.getEnvelopeId(envelope);
@ -811,12 +739,7 @@ MessageReceiver.prototype.extend({
// Note: this is an out of band update; there are cases where the item in the // Note: this is an out of band update; there are cases where the item in the
// cache has already been deleted by the time this runs. That's okay. // cache has already been deleted by the time this runs. That's okay.
this.updateCache(envelope, plaintext).catch(error => { this.updateCache(envelope, plaintext);
window.log.error(
'decrypt failed to save decrypted message contents to cache:',
error && error.stack ? error.stack : error
);
});
return plaintext; return plaintext;
}) })
@ -1497,6 +1420,9 @@ textsecure.MessageReceiver = function MessageReceiverWrapper(
messageReceiver messageReceiver
); );
this.stopProcessing = messageReceiver.stopProcessing.bind(messageReceiver); this.stopProcessing = messageReceiver.stopProcessing.bind(messageReceiver);
this.unregisterBatchers = messageReceiver.unregisterBatchers.bind(
messageReceiver
);
messageReceiver.connect(); messageReceiver.connect();
}; };

View file

@ -676,7 +676,7 @@ MessageSender.prototype = {
); );
}, },
sendDeliveryReceipt(recipientId, timestamp, options) { sendDeliveryReceipt(recipientId, timestamps, options) {
const myNumber = textsecure.storage.user.getNumber(); const myNumber = textsecure.storage.user.getNumber();
const myDevice = textsecure.storage.user.getDeviceId(); const myDevice = textsecure.storage.user.getDeviceId();
if (myNumber === recipientId && (myDevice === 1 || myDevice === '1')) { if (myNumber === recipientId && (myDevice === 1 || myDevice === '1')) {
@ -685,7 +685,7 @@ MessageSender.prototype = {
const receiptMessage = new textsecure.protobuf.ReceiptMessage(); const receiptMessage = new textsecure.protobuf.ReceiptMessage();
receiptMessage.type = textsecure.protobuf.ReceiptMessage.Type.DELIVERY; receiptMessage.type = textsecure.protobuf.ReceiptMessage.Type.DELIVERY;
receiptMessage.timestamp = [timestamp]; receiptMessage.timestamp = timestamps;
const contentMessage = new textsecure.protobuf.Content(); const contentMessage = new textsecure.protobuf.Content();
contentMessage.receiptMessage = receiptMessage; contentMessage.receiptMessage = receiptMessage;

View file

@ -21,6 +21,9 @@
add(data) { add(data) {
return textsecure.storage.protocol.addUnprocessed(data); return textsecure.storage.protocol.addUnprocessed(data);
}, },
batchAdd(array) {
return textsecure.storage.protocol.addMultipleUnprocessed(array);
},
updateAttempts(id, attempts) { updateAttempts(id, attempts) {
return textsecure.storage.protocol.updateUnprocessedAttempts( return textsecure.storage.protocol.updateUnprocessedAttempts(
id, id,
@ -30,8 +33,11 @@
addDecryptedData(id, data) { addDecryptedData(id, data) {
return textsecure.storage.protocol.updateUnprocessedWithData(id, data); return textsecure.storage.protocol.updateUnprocessedWithData(id, data);
}, },
remove(id) { addDecryptedDataToList(array) {
return textsecure.storage.protocol.removeUnprocessed(id); return textsecure.storage.protocol.updateUnprocessedsWithData(array);
},
remove(idOrArray) {
return textsecure.storage.protocol.removeUnprocessed(idOrArray);
}, },
removeAll() { removeAll() {
return textsecure.storage.protocol.removeAllUnprocessed(); return textsecure.storage.protocol.removeAllUnprocessed();

View file

@ -27,12 +27,14 @@ describe('SignalProtocolStore', () => {
describe('getLocalRegistrationId', () => { describe('getLocalRegistrationId', () => {
it('retrieves my registration id', async () => { it('retrieves my registration id', async () => {
await store.hydrateCaches();
const id = await store.getLocalRegistrationId(); const id = await store.getLocalRegistrationId();
assert.strictEqual(id, 1337); assert.strictEqual(id, 1337);
}); });
}); });
describe('getIdentityKeyPair', () => { describe('getIdentityKeyPair', () => {
it('retrieves my identity key', async () => { it('retrieves my identity key', async () => {
await store.hydrateCaches();
const key = await store.getIdentityKeyPair(); const key = await store.getIdentityKeyPair();
assertEqualArrayBuffers(key.pubKey, identityKey.pubKey); assertEqualArrayBuffers(key.pubKey, identityKey.pubKey);
assertEqualArrayBuffers(key.privKey, identityKey.privKey); assertEqualArrayBuffers(key.privKey, identityKey.privKey);

97
ts/util/batcher.ts Normal file
View file

@ -0,0 +1,97 @@
import PQueue from 'p-queue';
// @ts-ignore
window.batchers = [];
// @ts-ignore
window.waitForAllBatchers = async () => {
// @ts-ignore
await Promise.all(window.batchers.map(item => item.onIdle()));
};
type BatcherOptionsType<ItemType> = {
wait: number;
maxSize: number;
processBatch: (items: Array<ItemType>) => Promise<void>;
};
type BatcherType<ItemType> = {
add: (item: ItemType) => void;
anyPending: () => boolean;
onIdle: () => Promise<void>;
unregister: () => void;
};
async function sleep(ms: number): Promise<void> {
// tslint:disable-next-line:no-string-based-set-timeout
await new Promise(resolve => setTimeout(resolve, ms));
}
export function createBatcher<ItemType>(
options: BatcherOptionsType<ItemType>
): BatcherType<ItemType> {
let batcher: BatcherType<ItemType>;
let timeout: any;
let items: Array<ItemType> = [];
const queue = new PQueue({ concurrency: 1 });
function _kickBatchOff() {
const itemsRef = items;
items = [];
// tslint:disable-next-line:no-floating-promises
queue.add(async () => {
await options.processBatch(itemsRef);
});
}
function add(item: ItemType) {
items.push(item);
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
if (items.length >= options.maxSize) {
_kickBatchOff();
} else {
timeout = setTimeout(() => {
timeout = null;
_kickBatchOff();
}, options.wait);
}
}
function anyPending(): boolean {
return queue.size > 0 || queue.pending > 0 || items.length > 0;
}
async function onIdle() {
while (anyPending()) {
if (queue.size > 0 || queue.pending > 0) {
await queue.onIdle();
}
if (items.length > 0) {
await sleep(options.wait * 2);
}
}
}
function unregister() {
// @ts-ignore
window.batchers = window.batchers.filter((item: any) => item !== batcher);
}
batcher = {
add,
anyPending,
onIdle,
unregister,
};
// @ts-ignore
window.batchers.push(batcher);
return batcher;
}

View file

@ -1,5 +1,7 @@
import * as GoogleChrome from './GoogleChrome'; import * as GoogleChrome from './GoogleChrome';
import { arrayBufferToObjectURL } from './arrayBufferToObjectURL'; import { arrayBufferToObjectURL } from './arrayBufferToObjectURL';
import { createBatcher } from './batcher';
import { createWaitBatcher } from './waitBatcher';
import { isFileDangerous } from './isFileDangerous'; import { isFileDangerous } from './isFileDangerous';
import { missingCaseError } from './missingCaseError'; import { missingCaseError } from './missingCaseError';
import { migrateColor } from './migrateColor'; import { migrateColor } from './migrateColor';
@ -7,6 +9,8 @@ import { makeLookup } from './makeLookup';
export { export {
arrayBufferToObjectURL, arrayBufferToObjectURL,
createBatcher,
createWaitBatcher,
GoogleChrome, GoogleChrome,
isFileDangerous, isFileDangerous,
makeLookup, makeLookup,

View file

@ -172,7 +172,7 @@
"rule": "jQuery-load(", "rule": "jQuery-load(",
"path": "js/conversation_controller.js", "path": "js/conversation_controller.js",
"line": " this._initialPromise = load();", "line": " this._initialPromise = load();",
"lineNumber": 219, "lineNumber": 216,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2019-07-31T00:19:18.696Z" "updated": "2019-07-31T00:19:18.696Z"
}, },
@ -301,26 +301,10 @@
"rule": "jQuery-load(", "rule": "jQuery-load(",
"path": "js/signal_protocol_store.js", "path": "js/signal_protocol_store.js",
"line": " await ConversationController.load();", "line": " await ConversationController.load();",
"lineNumber": 868, "lineNumber": 908,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2018-09-15T00:38:04.183Z" "updated": "2018-09-15T00:38:04.183Z"
}, },
{
"rule": "jQuery-wrap(",
"path": "js/util_worker_tasks.js",
"line": " return dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer();",
"lineNumber": 40,
"reasonCategory": "falseMatch",
"updated": "2018-09-19T18:13:29.628Z"
},
{
"rule": "jQuery-wrap(",
"path": "js/util_worker_tasks.js",
"line": " return dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');",
"lineNumber": 43,
"reasonCategory": "falseMatch",
"updated": "2018-09-19T18:13:29.628Z"
},
{ {
"rule": "DOM-innerHTML", "rule": "DOM-innerHTML",
"path": "js/views/app_view.js", "path": "js/views/app_view.js",
@ -1227,34 +1211,50 @@
{ {
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
"path": "libtextsecure/message_receiver.js", "path": "libtextsecure/message_receiver.js",
"line": " Promise.resolve(dcodeIO.ByteBuffer.wrap(string, 'binary').toArrayBuffer());", "line": " dcodeIO.ByteBuffer.wrap(string, 'binary').toArrayBuffer();",
"lineNumber": 148, "lineNumber": 62,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2018-09-19T18:13:29.628Z" "updated": "2019-09-20T18:36:19.909Z"
}, },
{ {
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
"path": "libtextsecure/message_receiver.js", "path": "libtextsecure/message_receiver.js",
"line": " Promise.resolve(dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('binary'));", "line": " dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('binary');",
"lineNumber": 150, "lineNumber": 64,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2018-09-19T18:13:29.628Z" "updated": "2019-09-20T18:36:19.909Z"
},
{
"rule": "jQuery-wrap(",
"path": "libtextsecure/message_receiver.js",
"line": " dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer();",
"lineNumber": 66,
"reasonCategory": "falseMatch",
"updated": "2019-09-20T18:36:19.909Z"
},
{
"rule": "jQuery-wrap(",
"path": "libtextsecure/message_receiver.js",
"line": " dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');",
"lineNumber": 68,
"reasonCategory": "falseMatch",
"updated": "2019-09-20T18:36:19.909Z"
}, },
{ {
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
"path": "libtextsecure/message_receiver.js", "path": "libtextsecure/message_receiver.js",
"line": " const buffer = dcodeIO.ByteBuffer.wrap(ciphertext);", "line": " const buffer = dcodeIO.ByteBuffer.wrap(ciphertext);",
"lineNumber": 829, "lineNumber": 752,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2018-09-19T18:13:29.628Z" "updated": "2019-09-20T18:36:19.909Z"
}, },
{ {
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
"path": "libtextsecure/message_receiver.js", "path": "libtextsecure/message_receiver.js",
"line": " const buffer = dcodeIO.ByteBuffer.wrap(ciphertext);", "line": " const buffer = dcodeIO.ByteBuffer.wrap(ciphertext);",
"lineNumber": 854, "lineNumber": 777,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2018-09-19T18:13:29.628Z" "updated": "2019-09-20T18:36:19.909Z"
}, },
{ {
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",

141
ts/util/waitBatcher.ts Normal file
View file

@ -0,0 +1,141 @@
import PQueue from 'p-queue';
// @ts-ignore
window.waitBatchers = [];
// @ts-ignore
window.waitForAllWaitBatchers = async () => {
// @ts-ignore
await Promise.all(window.waitBatchers.map(item => item.onIdle()));
};
type ItemHolderType<ItemType> = {
resolve: (value?: any) => void;
reject: (error: Error) => void;
item: ItemType;
};
type ExplodedPromiseType = {
resolve: (value?: any) => void;
reject: (error: Error) => void;
promise: Promise<any>;
};
type BatcherOptionsType<ItemType> = {
wait: number;
maxSize: number;
processBatch: (items: Array<ItemType>) => Promise<void>;
};
type BatcherType<ItemType> = {
add: (item: ItemType) => Promise<void>;
anyPending: () => boolean;
onIdle: () => Promise<void>;
unregister: () => void;
};
async function sleep(ms: number): Promise<void> {
// tslint:disable-next-line:no-string-based-set-timeout
await new Promise(resolve => setTimeout(resolve, ms));
}
export function createWaitBatcher<ItemType>(
options: BatcherOptionsType<ItemType>
): BatcherType<ItemType> {
let waitBatcher: BatcherType<ItemType>;
let timeout: any;
let items: Array<ItemHolderType<ItemType>> = [];
const queue = new PQueue({ concurrency: 1 });
function _kickBatchOff() {
const itemsRef = items;
items = [];
// tslint:disable-next-line:no-floating-promises
queue.add(async () => {
try {
await options.processBatch(itemsRef.map(item => item.item));
itemsRef.forEach(item => {
item.resolve();
});
} catch (error) {
itemsRef.forEach(item => {
item.reject(error);
});
}
});
}
function _makeExplodedPromise(): ExplodedPromiseType {
let resolve;
let reject;
// tslint:disable-next-line:promise-must-complete
const promise = new Promise((resolveParam, rejectParam) => {
resolve = resolveParam;
reject = rejectParam;
});
// @ts-ignore
return { promise, resolve, reject };
}
async function add(item: ItemType) {
const { promise, resolve, reject } = _makeExplodedPromise();
items.push({
resolve,
reject,
item,
});
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
if (items.length >= options.maxSize) {
_kickBatchOff();
} else {
timeout = setTimeout(() => {
timeout = null;
_kickBatchOff();
}, options.wait);
}
await promise;
}
function anyPending(): boolean {
return queue.size > 0 || queue.pending > 0 || items.length > 0;
}
async function onIdle() {
while (anyPending()) {
if (queue.size > 0 || queue.pending > 0) {
await queue.onIdle();
}
if (items.length > 0) {
await sleep(options.wait * 2);
}
}
}
function unregister() {
// @ts-ignore
window.waitBatchers = window.waitBatchers.filter(
(item: any) => item !== waitBatcher
);
}
waitBatcher = {
add,
anyPending,
onIdle,
unregister,
};
// @ts-ignore
window.waitBatchers.push(waitBatcher);
return waitBatcher;
}