diff --git a/src/auto/injector.js b/src/auto/injector.js index 6399dca8de03..fbbb25e9f132 100644 --- a/src/auto/injector.js +++ b/src/auto/injector.js @@ -149,7 +149,7 @@ function annotate(fn) { * @param {Object=} self The `this` for the invoked method. * @param {Object=} locals Optional object. If preset then any argument names are read from this object first, before * the `$injector` is consulted. - * @returns {*} the value returned by the invoked `fn` function. + * @returns {*} the value returned by the invoked `fn` function, or a promise if any injectable dependencies are themselves unresolved promises. */ /** @@ -556,7 +556,10 @@ function createInjector(modulesToLoad) { var args = [], $inject = annotate(fn), length, i, - key; + key, + hasPromisedDependencies = false, + invocationDeferredReturn, + dependenciesPromise; for(i = 0, length = $inject.length; i < length; i++) { key = $inject[i]; @@ -568,27 +571,53 @@ function createInjector(modulesToLoad) { ? locals[key] : getService(key) ); + + // make note if we discover a dependency which is currently a promise. + // this will trigger a deferred return value from this function and + // delay the invocation until all promised dependencies have been resolved. + if(!hasPromisedDependencies && args[i] && args[i].then && typeof args[i].then == "function") { + hasPromisedDependencies = true; + } } if (!fn.$inject) { // this means that we must be an array. fn = fn[length]; } + // this will either be invoked immediately or + // once all dependencies have been fulfilled. + function performInvocation(fn, self, args) { + // Performance optimization: http://jsperf.com/apply-vs-call-vs-invoke + switch (self ? -1 : args.length) { + case 0: return fn(); + case 1: return fn(args[0]); + case 2: return fn(args[0], args[1]); + case 3: return fn(args[0], args[1], args[2]); + case 4: return fn(args[0], args[1], args[2], args[3]); + case 5: return fn(args[0], args[1], args[2], args[3], args[4]); + case 6: return fn(args[0], args[1], args[2], args[3], args[4], args[5]); + case 7: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6]); + case 8: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]); + case 9: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8]); + case 10: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9]); + default: return fn.apply(self, args); + } + } - // Performance optimization: http://jsperf.com/apply-vs-call-vs-invoke - switch (self ? -1 : args.length) { - case 0: return fn(); - case 1: return fn(args[0]); - case 2: return fn(args[0], args[1]); - case 3: return fn(args[0], args[1], args[2]); - case 4: return fn(args[0], args[1], args[2], args[3]); - case 5: return fn(args[0], args[1], args[2], args[3], args[4]); - case 6: return fn(args[0], args[1], args[2], args[3], args[4], args[5]); - case 7: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6]); - case 8: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]); - case 9: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8]); - case 10: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9]); - default: return fn.apply(self, args); + if(hasPromisedDependencies) { + // wrap all args in .when() for common interface, then use .all() + // to perform the invocation once all are resolved. + for(i = 0, length = args.length; i < length; i++) { + args[i] = cache['$q'].when(args[i]); + } + dependenciesPromise = cache['$q'].all(args); + invocationDeferredReturn = cache['$q'].defer(); + dependenciesPromise.then(function(resolvedArgs){ + invocationDeferredReturn.resolve(performInvocation(fn, self, resolvedArgs)); + }) + return invocationDeferredReturn.promise; + } else { + return performInvocation(fn, self, args); } } diff --git a/test/auto/injectorSpec.js b/test/auto/injectorSpec.js index 2c4856550bae..96f79855bd32 100644 --- a/test/auto/injectorSpec.js +++ b/test/auto/injectorSpec.js @@ -90,6 +90,27 @@ describe('injector', function() { describe('invoke', function() { var args; + var q, defer, deferred, promise, log; + var mockNextTick = { + nextTick: function(task) { + mockNextTick.queue.push(task); + }, + queue: [], + flush: function() { + if (!mockNextTick.queue.length) throw new Error('Nothing to be flushed!'); + while (mockNextTick.queue.length) { + var queue = mockNextTick.queue; + mockNextTick.queue = []; + forEach(queue, function(task) { + try { + task(); + } catch(e) { + dump('exception in mockNextTick:', e, e.name, e.message, task); + } + }); + } + } + } beforeEach(function() { args = null; @@ -132,6 +153,91 @@ describe('injector', function() { injector.invoke(['a', 123], {}); }).toThrow("[ng:areq] Argument 'fn' is not a function, got number"); }); + + it('should return a promise if an injected dependency is a promised service', function() { + providers('e', function() {return { then: function(){}};}); + var returned = injector.invoke(['$q', 'e', fn]); + expect(typeof returned.then).toBe('function'); + }); + + it('should call the function immediately if no injected dependencies are promised services', function() { + var testFnSpy = jasmine.createSpy('injectedPromise'); + var returned = injector.invoke(['a', 'b', testFnSpy]); + expect(testFnSpy).toHaveBeenCalled(); + }); + + it('should not call the function immediately if an injected dependency is a promised service', function() { + var testFnSpy = jasmine.createSpy('injectedPromise'); + providers('e', function() {return { then: function(){}};}); + var returned = injector.invoke(['$q', 'e', testFnSpy]); + expect(testFnSpy).not.toHaveBeenCalled(); + }); + + it('should eventually call the function if an injected dependency is a promised service', function() { + var testFnSpy = jasmine.createSpy('injectedPromise'); + var flag = false; + var asyncService = function($q){ + return promise; + } + + q = qFactory(mockNextTick.nextTick, noop); + deferred = q.defer() + promise = deferred.promise; + providers('$q', function(){return q}); + + runs(function(){ + providers('e', asyncService); + var returned = injector.invoke(['e', testFnSpy]); + expect(testFnSpy).not.toHaveBeenCalled(); + setTimeout(function(){ + deferred.resolve(); + mockNextTick.flush(); + flag = true; + }, 50); + }); + + waitsFor(function(){ + return flag; + }, "Error waiting for async-service injection", 200); + + runs(function(){ + expect(testFnSpy).toHaveBeenCalled(); + }); + }); + + it('should use the eventually-resolved Service as argument if an injected dependency is a promised service', function() { + var testFnSpy = jasmine.createSpy('injectedPromise'); + var flag = false; + var asyncService = function($q){ + return promise; + } + var testResolvedValue = '1234ABCD'; + + q = qFactory(mockNextTick.nextTick, noop); + deferred = q.defer() + promise = deferred.promise; + providers('$q', function(){return q}); + + runs(function(){ + providers('e', asyncService); + var returned = injector.invoke(['e', testFnSpy]); + expect(testFnSpy).not.toHaveBeenCalled(); + setTimeout(function(){ + deferred.resolve(testResolvedValue); + mockNextTick.flush(); + flag = true; + }, 50); + }); + + waitsFor(function(){ + return flag; + }, "Error waiting for async-service injection", 200); + + runs(function(){ + expect(testFnSpy.mostRecentCall.args[0]).toEqual(testResolvedValue); + }); + }); + });