From 65c904c6089fd91c58837996a2cbaef35cae1505 Mon Sep 17 00:00:00 2001 From: Chris Trahey Date: Wed, 10 Jul 2013 17:01:02 -0700 Subject: [PATCH] feat($injector): allow service factories to return promises Change $injector's invoke method to delay the invocation in the event that an injected service is a promise. The change allows user-land services to be constructed asyncronously if they simply return a promise from their factory function. For any invocation that does not have promises as dependencies, the invocation happens immediately, as though this change was not in place. It may be interesting to note that in cases where the invocation does have promise (async-fulfilled) dependencies, the call to invoke will itself return a promise. There is ongoing discussion as to the merrits of this relatively non-deterministic behavior, though it is not considered 'BREAKING' because currently there is no support for asynchronously-generated services. --- src/auto/injector.js | 61 ++++++++++++++++------ test/auto/injectorSpec.js | 106 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+), 16 deletions(-) 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); + }); + }); + });