diff --git a/Gruntfile.js b/Gruntfile.js index d3eae9a55..8a941f895 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -61,6 +61,7 @@ module.exports = function(grunt) { 'libtextsecure/sync_request.js', 'libtextsecure/contacts_parser.js', 'libtextsecure/ProvisioningCipher.js', + 'libtextsecure/task_with_timeout.js', ], dest: 'js/libtextsecure.js', }, diff --git a/js/libtextsecure.js b/js/libtextsecure.js index e914aa122..bca94a2a0 100644 --- a/js/libtextsecure.js +++ b/js/libtextsecure.js @@ -38094,7 +38094,8 @@ var TextSecureServer = (function() { }.bind(this)); }, queueTask: function(task) { - return this.pending = this.pending.then(task, task); + var taskWithTimeout = textsecure.createTaskWithTimeout(task); + return this.pending = this.pending.then(taskWithTimeout, taskWithTimeout); }, cleanSignedPreKeys: function() { var nextSignedKeyId = textsecure.storage.get('signedKeyId'); @@ -38420,21 +38421,29 @@ MessageReceiver.prototype.extend({ return textsecure.storage.unprocessed.remove(id); }, queueDecryptedEnvelope: function(envelope, plaintext) { - console.log('queueing decrypted envelope', this.getEnvelopeId(envelope)); - var handleDecryptedEnvelope = this.handleDecryptedEnvelope.bind(this, envelope, plaintext); - this.pending = this.pending.then(handleDecryptedEnvelope, handleDecryptedEnvelope); + var id = this.getEnvelopeId(envelope); + console.log('queueing decrypted envelope', id); + + var task = this.handleDecryptedEnvelope.bind(this, envelope, plaintext); + var taskWithTimeout = textsecure.createTaskWithTimeout(task, 'queueEncryptedEnvelope ' + id); + + this.pending = this.pending.then(taskWithTimeout, taskWithTimeout); return this.pending.catch(function(error) { - console.log('queueDecryptedEnvelope error:', error && error.stack ? error.stack : error); + console.log('queueDecryptedEnvelope error handling envelope', id, ':', error && error.stack ? error.stack : error); }); }, queueEnvelope: function(envelope) { - console.log('queueing envelope', this.getEnvelopeId(envelope)); - var handleEnvelope = this.handleEnvelope.bind(this, envelope); - this.pending = this.pending.then(handleEnvelope, handleEnvelope); + var id = this.getEnvelopeId(envelope); + console.log('queueing envelope', id); + + var task = this.handleEnvelope.bind(this, envelope); + var taskWithTimeout = textsecure.createTaskWithTimeout(task, 'queueEnvelope ' + id); + + this.pending = this.pending.then(taskWithTimeout, taskWithTimeout); return this.pending.catch(function(error) { - console.log('queueEnvelope error:', error && error.stack ? error.stack : error); + console.log('queueEnvelope error handling envelope', id, ':', error && error.stack ? error.stack : error); }); }, // Same as handleEnvelope, just without the decryption step. Necessary for handling @@ -39325,8 +39334,10 @@ MessageSender.prototype = { }, queueJobForNumber: function(number, runJob) { + var taskWithTimeout = textsecure.createTaskWithTimeout(runJob, 'queueJobForNumber ' + number); + var runPrevious = this.pendingMessages[number] || Promise.resolve(); - var runCurrent = this.pendingMessages[number] = runPrevious.then(runJob, runJob); + var runCurrent = this.pendingMessages[number] = runPrevious.then(taskWithTimeout, taskWithTimeout); runCurrent.then(function() { if (this.pendingMessages[number] === runCurrent) { delete this.pendingMessages[number]; @@ -39969,4 +39980,70 @@ libsignal.ProvisioningCipher = function() { }; })(); + +/* + * vim: ts=4:sw=4:expandtab + */ +(function () { + window.textsecure = window.textsecure || {}; + + window.textsecure.createTaskWithTimeout = function(task, id, options) { + options = options || {}; + options.timeout = options.timeout || (1000 * 60 * 2); // two minutes + + var errorForStack = new Error('for stack'); + return function() { + return new Promise(function(resolve, reject) { + var complete = false; + var timer = setTimeout(function() { + if (!complete) { + var message = + (id || '') + + ' task did not complete in time. Calling stack: ' + + errorForStack.stack; + + console.log(message); + return reject(new Error(message)); + } + }.bind(this), options.timeout); + var clearTimer = function() { + try { + var localTimer = timer; + if (localTimer) { + timer = null; + clearTimeout(localTimer); + } + } + catch (error) { + console.log( + id || '', + 'task ran into problem canceling timer. Calling stack:', + errorForStack.stack + ); + } + }; + + var success = function(result) { + clearTimer(); + complete = true; + return resolve(result); + }; + var failure = function(error) { + clearTimer(); + complete = true; + return reject(error); + }; + + var promise = task(); + if (!promise || !promise.then) { + clearTimer(); + complete = true; + return resolve(promise); + } + + return promise.then(success, failure); + }); + }; + }; +})(); })(); diff --git a/js/models/conversations.js b/js/models/conversations.js index bbf94ecb9..8864087d2 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -411,7 +411,10 @@ queueJob: function(callback) { var previous = this.pending || Promise.resolve(); - var current = this.pending = previous.then(callback, callback); + + var taskWithTimeout = textsecure.createTaskWithTimeout(callback, 'conversation ' + this.id); + + var current = this.pending = previous.then(taskWithTimeout, taskWithTimeout); current.then(function() { if (this.pending === current) { diff --git a/js/models/messages.js b/js/models/messages.js index 04eb8d074..35ab939f9 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -480,9 +480,6 @@ }; message.save().then(function() { - - // throw new Error('Something went wrong!'); - conversation.save().then(function() { try { conversation.trigger('newmessage', message); diff --git a/libtextsecure/account_manager.js b/libtextsecure/account_manager.js index 8ba9d0d94..f0993dc04 100644 --- a/libtextsecure/account_manager.js +++ b/libtextsecure/account_manager.js @@ -146,7 +146,8 @@ }.bind(this)); }, queueTask: function(task) { - return this.pending = this.pending.then(task, task); + var taskWithTimeout = textsecure.createTaskWithTimeout(task); + return this.pending = this.pending.then(taskWithTimeout, taskWithTimeout); }, cleanSignedPreKeys: function() { var nextSignedKeyId = textsecure.storage.get('signedKeyId'); diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index e68456a9b..29e764d55 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -182,21 +182,29 @@ MessageReceiver.prototype.extend({ return textsecure.storage.unprocessed.remove(id); }, queueDecryptedEnvelope: function(envelope, plaintext) { - console.log('queueing decrypted envelope', this.getEnvelopeId(envelope)); - var handleDecryptedEnvelope = this.handleDecryptedEnvelope.bind(this, envelope, plaintext); - this.pending = this.pending.then(handleDecryptedEnvelope, handleDecryptedEnvelope); + var id = this.getEnvelopeId(envelope); + console.log('queueing decrypted envelope', id); + + var task = this.handleDecryptedEnvelope.bind(this, envelope, plaintext); + var taskWithTimeout = textsecure.createTaskWithTimeout(task, 'queueEncryptedEnvelope ' + id); + + this.pending = this.pending.then(taskWithTimeout, taskWithTimeout); return this.pending.catch(function(error) { - console.log('queueDecryptedEnvelope error:', error && error.stack ? error.stack : error); + console.log('queueDecryptedEnvelope error handling envelope', id, ':', error && error.stack ? error.stack : error); }); }, queueEnvelope: function(envelope) { - console.log('queueing envelope', this.getEnvelopeId(envelope)); - var handleEnvelope = this.handleEnvelope.bind(this, envelope); - this.pending = this.pending.then(handleEnvelope, handleEnvelope); + var id = this.getEnvelopeId(envelope); + console.log('queueing envelope', id); + + var task = this.handleEnvelope.bind(this, envelope); + var taskWithTimeout = textsecure.createTaskWithTimeout(task, 'queueEnvelope ' + id); + + this.pending = this.pending.then(taskWithTimeout, taskWithTimeout); return this.pending.catch(function(error) { - console.log('queueEnvelope error:', error && error.stack ? error.stack : error); + console.log('queueEnvelope error handling envelope', id, ':', error && error.stack ? error.stack : error); }); }, // Same as handleEnvelope, just without the decryption step. Necessary for handling diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js index 46d187d50..68ea346b5 100644 --- a/libtextsecure/sendmessage.js +++ b/libtextsecure/sendmessage.js @@ -149,8 +149,10 @@ MessageSender.prototype = { }, queueJobForNumber: function(number, runJob) { + var taskWithTimeout = textsecure.createTaskWithTimeout(runJob, 'queueJobForNumber ' + number); + var runPrevious = this.pendingMessages[number] || Promise.resolve(); - var runCurrent = this.pendingMessages[number] = runPrevious.then(runJob, runJob); + var runCurrent = this.pendingMessages[number] = runPrevious.then(taskWithTimeout, taskWithTimeout); runCurrent.then(function() { if (this.pendingMessages[number] === runCurrent) { delete this.pendingMessages[number]; diff --git a/libtextsecure/task_with_timeout.js b/libtextsecure/task_with_timeout.js new file mode 100644 index 000000000..ee2ab363f --- /dev/null +++ b/libtextsecure/task_with_timeout.js @@ -0,0 +1,65 @@ +/* + * vim: ts=4:sw=4:expandtab + */ +(function () { + window.textsecure = window.textsecure || {}; + + window.textsecure.createTaskWithTimeout = function(task, id, options) { + options = options || {}; + options.timeout = options.timeout || (1000 * 60 * 2); // two minutes + + var errorForStack = new Error('for stack'); + return function() { + return new Promise(function(resolve, reject) { + var complete = false; + var timer = setTimeout(function() { + if (!complete) { + var message = + (id || '') + + ' task did not complete in time. Calling stack: ' + + errorForStack.stack; + + console.log(message); + return reject(new Error(message)); + } + }.bind(this), options.timeout); + var clearTimer = function() { + try { + var localTimer = timer; + if (localTimer) { + timer = null; + clearTimeout(localTimer); + } + } + catch (error) { + console.log( + id || '', + 'task ran into problem canceling timer. Calling stack:', + errorForStack.stack + ); + } + }; + + var success = function(result) { + clearTimer(); + complete = true; + return resolve(result); + }; + var failure = function(error) { + clearTimer(); + complete = true; + return reject(error); + }; + + var promise = task(); + if (!promise || !promise.then) { + clearTimer(); + complete = true; + return resolve(promise); + } + + return promise.then(success, failure); + }); + }; + }; +})(); diff --git a/libtextsecure/test/index.html b/libtextsecure/test/index.html index 419d1885a..15d007461 100644 --- a/libtextsecure/test/index.html +++ b/libtextsecure/test/index.html @@ -28,9 +28,10 @@ - - - + + + + @@ -39,5 +40,6 @@ + diff --git a/libtextsecure/test/task_with_timeout_test.js b/libtextsecure/test/task_with_timeout_test.js new file mode 100644 index 000000000..65e86b2d7 --- /dev/null +++ b/libtextsecure/test/task_with_timeout_test.js @@ -0,0 +1,60 @@ +'use strict'; + +describe('createTaskWithTimeout', function() { + it('resolves when promise resolves', function() { + var task = function() { + return Promise.resolve('hi!'); + }; + var taskWithTimeout = textsecure.createTaskWithTimeout(task); + + return taskWithTimeout().then(function(result) { + assert.strictEqual(result, 'hi!') + }); + }); + it('flows error from promise back', function() { + var error = new Error('original'); + var task = function() { + return Promise.reject(error); + }; + var taskWithTimeout = textsecure.createTaskWithTimeout(task); + + return taskWithTimeout().catch(function(flowedError) { + assert.strictEqual(error, flowedError); + }); + }); + it('rejects if promise takes too long', function() { + var error = new Error('original'); + var complete = false; + var task = function() { + return new Promise(function(resolve) { + setTimeout(function() { + completed = true; + resolve(); + }, 3000); + }); + }; + var taskWithTimeout = textsecure.createTaskWithTimeout(task, this.name, { + timeout: 10 + }); + + return taskWithTimeout().then(function() { + throw new Error('it was not supposed to resolve!'); + }, function() { + assert.strictEqual(complete, false); + }); + }); + it('resolves if task returns something falsey', function() { + var task = function() {}; + var taskWithTimeout = textsecure.createTaskWithTimeout(task); + return taskWithTimeout(); + }); + it('resolves if task returns a non-promise', function() { + var task = function() { + return 'hi!'; + }; + var taskWithTimeout = textsecure.createTaskWithTimeout(task); + return taskWithTimeout().then(function(result) { + assert.strictEqual(result, 'hi!') + }); + }); +});