diff --git a/lib/observer.js b/lib/observer.js index 893ac0be..57e3ed58 100644 --- a/lib/observer.js +++ b/lib/observer.js @@ -58,17 +58,36 @@ ObserverMixin.clearObservers = function(operation) { }; /** - * Invoke all async observers for the given operation. - * @param {String} operation The operation name. + * Invoke all async observers for the given operation(s). + * @param {String|String[]} operation The operation name(s). * @param {Object} context Operation-specific context. * @param {function(Error=)} callback The callback to call when all observers * has finished. */ ObserverMixin.notifyObserversOf = function(operation, context, callback) { - var observers = this._observers && this._observers[operation]; - + var self = this; if (!callback) callback = utils.createPromiseCallback(); + function createNotifier(op) { + return function(ctx, done) { + if (typeof ctx === 'function' && done === undefined) { + done = ctx; + ctx = context; + } + self.notifyObserversOf(op, context, done); + }; + } + + if (Array.isArray(operation)) { + var tasks = []; + for (var i = 0, n = operation.length; i < n; i++) { + tasks.push(createNotifier(operation[i])); + } + return async.waterfall(tasks, callback); + } + + var observers = this._observers && this._observers[operation]; + this._notifyBaseObservers(operation, context, function doNotify(err) { if (err) return callback(err, context); if (!observers || !observers.length) return callback(null, context); @@ -115,21 +134,34 @@ ObserverMixin._notifyBaseObservers = function(operation, context, callback) { */ ObserverMixin.notifyObserversAround = function(operation, context, fn, callback) { var self = this; + context = context || {}; + // Add callback to the context object so that an observer can skip other + // ones by calling the callback function directly and not calling next + if (context.end === undefined) { + context.end = callback; + } + // First notify before observers return self.notifyObserversOf('before ' + operation, context, function(err, context) { - if (err) return callback(err, context); + if (err) return callback(err); function cbForWork(err) { - if (err) return callback(err, context); - var returnedArgs = [].slice.call(arguments, 1); + var args = [].slice.call(arguments, 0); + if (err) return callback.apply(null, args); + // Find the list of params from the callback in addition to err + var returnedArgs = args.slice(1); + // Set up the array of results context.results = returnedArgs; + // Notify after observers self.notifyObserversOf('after ' + operation, context, function(err, context) { if (err) return callback(err, context); var results = returnedArgs; - if (context) { + if (context && Array.isArray(context.results)) { + // Pickup the results from context results = context.results; } + // Build the list of params for final callback var args = [err].concat(results); callback.apply(null, args); }); diff --git a/test/async-observer.test.js b/test/async-observer.test.js index f337f158..f840be14 100644 --- a/test/async-observer.test.js +++ b/test/async-observer.test.js @@ -37,6 +37,18 @@ describe('async observer', function() { }); }); + it('allows multiple operations to be notified in one call', function(done) { + var notifications = []; + TestModel.observe('event1', pushAndNext(notifications, 'one')); + TestModel.observe('event2', pushAndNext(notifications, 'two')); + + TestModel.notifyObserversOf(['event1', 'event2'], {}, function(err) { + if (err) return done(err); + notifications.should.eql(['one', 'two']); + done(); + }); + }); + it('inherits observers from base model', function(done) { var notifications = []; TestModel.observe('event', pushAndNext(notifications, 'base')); @@ -51,6 +63,22 @@ describe('async observer', function() { }); }); + it('allow multiple operations to be notified with base models', function(done) { + var notifications = []; + TestModel.observe('event1', pushAndNext(notifications, 'base1')); + TestModel.observe('event2', pushAndNext(notifications, 'base2')); + + var Child = TestModel.extend('Child'); + Child.observe('event1', pushAndNext(notifications, 'child1')); + Child.observe('event2', pushAndNext(notifications, 'child2')); + + Child.notifyObserversOf(['event1', 'event2'], {}, function(err) { + if (err) return done(err); + notifications.should.eql(['base1', 'child1', 'base2', 'child2']); + done(); + }); + }); + it('does not modify observers in the base model', function(done) { var notifications = []; TestModel.observe('event', pushAndNext(notifications, 'base')); @@ -194,6 +222,57 @@ describe('async observer', function() { done(); }); }); + + it('should allow observers to skip other ones', + function(done) { + TestModel.observe('before invoke', + function(context, next) { + notifications.push('before invoke'); + context.end(null, 0); + }); + TestModel.observe('after invoke', + pushAndNext(notifications, 'after invoke')); + + var context = {}; + + function work(done) { + process.nextTick(function() { + done(null, 1, 2); + }); + } + + TestModel.notifyObserversAround('invoke', context, work, + function(err, r1) { + r1.should.eql(0); + notifications.should.eql(['before invoke']); + done(); + }); + }); + + it('should allow observers to tweak results', + function(done) { + TestModel.observe('after invoke', + function(context, next) { + notifications.push('after invoke'); + context.results = [3]; + next(); + }); + + var context = {}; + + function work(done) { + process.nextTick(function() { + done(null, 1, 2); + }); + } + + TestModel.notifyObserversAround('invoke', context, work, + function(err, r1) { + r1.should.eql(3); + notifications.should.eql(['after invoke']); + done(); + }); + }); }); it('resolves promises returned by observers', function(done) {