diff --git a/.eslintignore b/.eslintignore index aa9a74971d3f..ff968ce0e947 100644 --- a/.eslintignore +++ b/.eslintignore @@ -12,6 +12,7 @@ test/views/*.js # Generated files js/components.js js/libtextsecure.js +js/util_worker.js js/libsignal-protocol-worker.js libtextsecure/components.js libtextsecure/test/test.js diff --git a/.gitignore b/.gitignore index 3ee9ea879dd7..b1b7020d175b 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ sql/ # generated files js/components.js +js/util_worker.js js/libtextsecure.js libtextsecure/components.js libtextsecure/test/test.js diff --git a/.prettierignore b/.prettierignore index 21047597f8f8..0b44d051edeb 100644 --- a/.prettierignore +++ b/.prettierignore @@ -6,6 +6,7 @@ config/local-*.json config/local.json dist/** js/components.js +js/util_worker.js js/libtextsecure.js libtextsecure/components.js libtextsecure/test/test.js diff --git a/Gruntfile.js b/Gruntfile.js index 355d4add7702..30f2259f27d4 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -33,6 +33,14 @@ module.exports = grunt => { src: components, 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: { src: libtextsecurecomponents, dest: 'libtextsecure/components.js', diff --git a/js/modules/data.js b/js/modules/data.js index 31b41b4add5c..ff3c996e7e59 100644 --- a/js/modules/data.js +++ b/js/modules/data.js @@ -166,7 +166,7 @@ ipcRenderer.on( const job = _getJob(jobId); if (!job) { throw new Error( - `Received job reply to job ${jobId}, but did not have it in our registry!` + `Received SQL channel reply to job ${jobId}, but did not have it in our registry!` ); } @@ -174,7 +174,9 @@ ipcRenderer.on( if (errorForDisplay) { return reject( - new Error(`Error calling channel ${fnName}: ${errorForDisplay}`) + new Error( + `Error received from SQL channel job ${jobId} (${fnName}): ${errorForDisplay}` + ) ); } @@ -196,7 +198,8 @@ function makeChannel(fnName) { }); setTimeout( - () => reject(new Error(`Request to ${fnName} timed out`)), + () => + reject(new Error(`SQL channel job ${jobId} (${fnName}) timed out`)), DATABASE_UPDATE_TIMEOUT ); }); diff --git a/js/util_worker_tasks.js b/js/util_worker_tasks.js new file mode 100644 index 000000000000..4d6c80bda066 --- /dev/null +++ b/js/util_worker_tasks.js @@ -0,0 +1,44 @@ +/* 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'); +} diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index ba9eb66afd74..e0a2c7ad7cb7 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -9,9 +9,112 @@ /* global _: false */ /* global ContactBuffer: false */ /* global GroupBuffer: false */ +/* global Worker: false */ /* eslint-disable more/no-then */ +const WORKER_TIMEOUT = 60 * 1000; // one minute + +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 = {}) { this.count = 0; @@ -35,10 +138,11 @@ MessageReceiver.stringToArrayBuffer = string => Promise.resolve(dcodeIO.ByteBuffer.wrap(string, 'binary').toArrayBuffer()); MessageReceiver.arrayBufferToString = arrayBuffer => Promise.resolve(dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('binary')); + MessageReceiver.stringToArrayBufferBase64 = string => - Promise.resolve(dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer()); + callWorker('stringToArrayBufferBase64', string); MessageReceiver.arrayBufferToStringBase64 = arrayBuffer => - Promise.resolve(dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64')); + callWorker('arrayBufferToStringBase64', arrayBuffer); MessageReceiver.prototype = new textsecure.EventTarget(); MessageReceiver.prototype.extend({