diff --git a/background.html b/background.html index 64db864a9..3e224f7f4 100644 --- a/background.html +++ b/background.html @@ -651,6 +651,7 @@ + diff --git a/js/reliable_trigger.js b/js/reliable_trigger.js index b5b961fcf..1b8cd06e0 100644 --- a/js/reliable_trigger.js +++ b/js/reliable_trigger.js @@ -1,5 +1,14 @@ (function () { - // Pure copy from Backbone. + // Note: this is all the code required to customize Backbone's trigger() method to make + // it resilient to exceptions thrown by event handlers. Indentation and code styles + // were kept inline with the Backbone implementation for easier diffs. + + // The changes are: + // 1. added 'name' parameter to triggerEvents to give it access to the current event name + // 2. added try/catch handlers to triggerEvents with error logging inside every while loop + + // And of course, we update the protoypes of Backbone.Model/Backbone.View as well as + // Backbone.Events itself // jscs:disable @@ -39,14 +48,62 @@ // A difficult-to-believe, but optimized internal dispatch function for // triggering events. Tries to keep the usual cases speedy (most internal // Backbone events have 3 arguments). - var triggerEvents = function(events, args) { + var triggerEvents = function(events, name, args) { var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; + var logError = function(error) { + console.log('Model caught error triggering', name, 'event:', error && error.stack ? error.stack : error); + }; switch (args.length) { - case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return; - case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return; - case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return; - case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; - default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return; + case 0: + while (++i < l) { + try { + (ev = events[i]).callback.call(ev.ctx); + } + catch (error) { + logError(error); + } + } + return; + case 1: + while (++i < l) { + try { + (ev = events[i]).callback.call(ev.ctx, a1); + } + catch (error) { + logError(error); + } + } + return; + case 2: + while (++i < l) { + try { + (ev = events[i]).callback.call(ev.ctx, a1, a2); + } + catch (error) { + logError(error); + } + } + return; + case 3: + while (++i < l) { + try { + (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); + } + catch (error) { + logError(error); + } + } + return; + default: + while (++i < l) { + try { + (ev = events[i]).callback.apply(ev.ctx, args); + } + catch (error) { + logError(error); + } + } + return; } }; @@ -60,8 +117,9 @@ if (!eventsApi(this, 'trigger', name, args)) return this; var events = this._events[name]; var allEvents = this._events.all; - if (events) triggerEvents(events, args); - if (allEvents) triggerEvents(allEvents, arguments); + if (events) triggerEvents(events, name, args); + if (allEvents) triggerEvents(allEvents, name, arguments); return this; }; })(); + diff --git a/test/index.html b/test/index.html index d5d2bf808..f138b1023 100644 --- a/test/index.html +++ b/test/index.html @@ -550,6 +550,7 @@ + @@ -624,6 +625,7 @@ + diff --git a/test/reliable_trigger_test.js b/test/reliable_trigger_test.js new file mode 100644 index 000000000..56f84351d --- /dev/null +++ b/test/reliable_trigger_test.js @@ -0,0 +1,147 @@ +'use strict'; + +describe('ReliableTrigger', function() { + describe('trigger', function() { + var Model, model; + + before(function() { + Model = Backbone.Model; + }); + + beforeEach(function() { + model = new Model(); + }); + + it('returns successfully if this._events is falsey', function() { + model._events = null; + model.trigger('click'); + }); + it('handles map of events to trigger', function() { + var a = 0, b = 0; + model.on('a', function(arg) { + a = arg; + }); + model.on('b', function(arg) { + b = arg; + }); + + model.trigger({ + a: 1, + b: 2 + }); + + assert.strictEqual(a, 1); + assert.strictEqual(b, 2); + }); + it('handles space-separated list of events to trigger', function() { + var a = false, b = false; + model.on('a', function() { + a = true; + }); + model.on('b', function() { + b = true; + }); + + model.trigger('a b'); + + assert.strictEqual(a, true); + assert.strictEqual(b, true); + }); + it('calls all clients registered for "all" event', function() { + var count = 0; + model.on('all', function() { + count += 1; + }); + + model.trigger('left'); + model.trigger('right'); + + assert.strictEqual(count, 2); + }); + it('calls all clients registered for target event', function() { + var a = false, b = false; + model.on('event', function() { + a = true; + }); + model.on('event', function() { + b = true; + }); + + model.trigger('event'); + + assert.strictEqual(a, true); + assert.strictEqual(b, true); + }); + it('successfully returns and calls all clients even if first failed', function() { + var a = false, b = false; + model.on('event', function() { + a = true; + throw new Error('a is set, but exception is thrown'); + }); + model.on('event', function() { + b = true; + }); + + model.trigger('event'); + + assert.strictEqual(a, true); + assert.strictEqual(b, true); + }); + it('calls clients with no args', function() { + var called = false; + model.on('event', function() { + called = true; + }); + + model.trigger('event'); + + assert.strictEqual(called, true); + }); + it('calls clients with 1 arg', function() { + var args; + model.on('event', function() { + args = arguments; + }); + + model.trigger('event', 1); + + assert.strictEqual(args[0], 1); + }); + it('calls clients with 2 args', function() { + var args; + model.on('event', function() { + args = arguments; + }); + + model.trigger('event', 1, 2); + + assert.strictEqual(args[0], 1); + assert.strictEqual(args[1], 2); + }); + it('calls clients with 3 args', function() { + var args; + model.on('event', function() { + args = arguments; + }); + + model.trigger('event', 1, 2, 3); + + assert.strictEqual(args[0], 1); + assert.strictEqual(args[1], 2); + assert.strictEqual(args[2], 3); + }); + it('calls clients with 4+ args', function() { + var args; + model.on('event', function() { + args = arguments; + }); + + model.trigger('event', 1, 2, 3, 4); + + assert.strictEqual(args[0], 1); + assert.strictEqual(args[1], 2); + assert.strictEqual(args[2], 3); + assert.strictEqual(args[3], 4); + }); + }); +});