diff --git a/angularFiles.js b/angularFiles.js index f0925f94a..6c4bcab00 100644 --- a/angularFiles.js +++ b/angularFiles.js @@ -41,6 +41,7 @@ var angularFiles = { 'src/ng/sanitizeUri.js', 'src/ng/sce.js', 'src/ng/sniffer.js', + 'src/ng/taskTrackerFactory.js', 'src/ng/templateRequest.js', 'src/ng/testability.js', 'src/ng/timeout.js', diff --git a/src/AngularPublic.js b/src/AngularPublic.js index 3d94d77d4..725e28770 100644 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -89,6 +89,7 @@ $SceProvider, $SceDelegateProvider, $SnifferProvider, + $$TaskTrackerFactoryProvider, $TemplateCacheProvider, $TemplateRequestProvider, $$TestabilityProvider, @@ -258,6 +259,7 @@ function publishExternalAPI(angular) { $sce: $SceProvider, $sceDelegate: $SceDelegateProvider, $sniffer: $SnifferProvider, + $$taskTrackerFactory: $$TaskTrackerFactoryProvider, $templateCache: $TemplateCacheProvider, $templateRequest: $TemplateRequestProvider, $$testability: $$TestabilityProvider, diff --git a/src/ng/browser.js b/src/ng/browser.js index 089ef911a..2f1494c90 100644 --- a/src/ng/browser.js +++ b/src/ng/browser.js @@ -22,105 +22,27 @@ * @param {object} $log window.console or an object with the same interface. * @param {object} $sniffer $sniffer service */ -function Browser(window, document, $log, $sniffer) { - var ALL_TASKS_TYPE = '$$all$$', - DEFAULT_TASK_TYPE = '$$default$$'; - +function Browser(window, document, $log, $sniffer, $$taskTrackerFactory) { var self = this, location = window.location, history = window.history, setTimeout = window.setTimeout, clearTimeout = window.clearTimeout, pendingDeferIds = {}, - outstandingRequestCounts = {}, - outstandingRequestCallbacks = []; + taskTracker = $$taskTrackerFactory($log); self.isMock = false; + ////////////////////////////////////////////////////////////// + // Task-tracking API + ////////////////////////////////////////////////////////////// + // TODO(vojta): remove this temporary api - self.$$completeOutstandingRequest = completeOutstandingRequest; - self.$$incOutstandingRequestCount = incOutstandingRequestCount; + self.$$completeOutstandingRequest = taskTracker.completeTask; + self.$$incOutstandingRequestCount = taskTracker.incTaskCount; - /** - * Executes the `fn` function and decrements the appropriate `outstandingRequestCounts` counter. - * If the counter reaches 0, all the corresponding `outstandingRequestCallbacks` are executed. - * @param {Function} fn - The function to execute. - * @param {string=} [taskType=DEFAULT_TASK_TYPE] The type of task that is being completed. - */ - function completeOutstandingRequest(fn, taskType) { - taskType = taskType || DEFAULT_TASK_TYPE; - try { - fn(); - } finally { - decOutstandingRequestCount(taskType); - - var countForType = outstandingRequestCounts[taskType]; - var countForAll = outstandingRequestCounts[ALL_TASKS_TYPE]; - - // If at least one of the queues (`ALL_TASKS_TYPE` or `taskType`) is empty, run callbacks. - if (!countForAll || !countForType) { - var getNextCallback = !countForAll ? getLastCallback : getLastCallbackForType; - var nextCb; - - while ((nextCb = getNextCallback(taskType))) { - try { - nextCb(); - } catch (e) { - $log.error(e); - } - } - } - } - } - - function decOutstandingRequestCount(taskType) { - taskType = taskType || DEFAULT_TASK_TYPE; - if (outstandingRequestCounts[taskType]) { - outstandingRequestCounts[taskType]--; - outstandingRequestCounts[ALL_TASKS_TYPE]--; - } - } - - function incOutstandingRequestCount(taskType) { - taskType = taskType || DEFAULT_TASK_TYPE; - outstandingRequestCounts[taskType] = (outstandingRequestCounts[taskType] || 0) + 1; - outstandingRequestCounts[ALL_TASKS_TYPE] = (outstandingRequestCounts[ALL_TASKS_TYPE] || 0) + 1; - } - - function getLastCallback() { - var cbInfo = outstandingRequestCallbacks.pop(); - return cbInfo && cbInfo.cb; - } - - function getLastCallbackForType(taskType) { - for (var i = outstandingRequestCallbacks.length - 1; i >= 0; --i) { - var cbInfo = outstandingRequestCallbacks[i]; - if (cbInfo.type === taskType) { - outstandingRequestCallbacks.splice(i, 1); - return cbInfo.cb; - } - } - } - - function getHash(url) { - var index = url.indexOf('#'); - return index === -1 ? '' : url.substr(index); - } - - /** - * @private - * TODO(vojta): prefix this method with $$ ? - * @param {function()} callback Function that will be called when no outstanding request. - * @param {string=} [taskType=ALL_TASKS_TYPE] The type of tasks that will be waited for. - */ - self.notifyWhenNoOutstandingRequests = function(callback, taskType) { - taskType = taskType || ALL_TASKS_TYPE; - if (!outstandingRequestCounts[taskType]) { - callback(); - } else { - outstandingRequestCallbacks.push({type: taskType, cb: callback}); - } - }; + // TODO(vojta): prefix this method with $$ ? + self.notifyWhenNoOutstandingRequests = taskTracker.notifyWhenNoPendingTasks; ////////////////////////////////////////////////////////////// // URL API @@ -140,6 +62,11 @@ function Browser(window, document, $log, $sniffer) { cacheState(); + function getHash(url) { + var index = url.indexOf('#'); + return index === -1 ? '' : url.substr(index); + } + /** * @name $browser#url * @@ -367,12 +294,12 @@ function Browser(window, document, $log, $sniffer) { var timeoutId; delay = delay || 0; - taskType = taskType || DEFAULT_TASK_TYPE; + taskType = taskType || taskTracker.DEFAULT_TASK_TYPE; - incOutstandingRequestCount(taskType); + taskTracker.incTaskCount(taskType); timeoutId = setTimeout(function() { delete pendingDeferIds[timeoutId]; - completeOutstandingRequest(fn, taskType); + taskTracker.completeTask(fn, taskType); }, delay); pendingDeferIds[timeoutId] = taskType; @@ -395,7 +322,7 @@ function Browser(window, document, $log, $sniffer) { var taskType = pendingDeferIds[deferId]; delete pendingDeferIds[deferId]; clearTimeout(deferId); - completeOutstandingRequest(noop, taskType); + taskTracker.completeTask(noop, taskType); return true; } return false; @@ -405,8 +332,8 @@ function Browser(window, document, $log, $sniffer) { /** @this */ function $BrowserProvider() { - this.$get = ['$window', '$log', '$sniffer', '$document', - function($window, $log, $sniffer, $document) { - return new Browser($window, $document, $log, $sniffer); - }]; + this.$get = ['$window', '$log', '$sniffer', '$document', '$$taskTrackerFactory', + function($window, $log, $sniffer, $document, $$taskTrackerFactory) { + return new Browser($window, $document, $log, $sniffer, $$taskTrackerFactory); + }]; } diff --git a/src/ng/taskTrackerFactory.js b/src/ng/taskTrackerFactory.js new file mode 100644 index 000000000..04717da37 --- /dev/null +++ b/src/ng/taskTrackerFactory.js @@ -0,0 +1,122 @@ +'use strict'; + +/** + * ! This is a private undocumented service ! + * + * @name $$taskTrackerFactory + * @description + * A function to create `TaskTracker` instances. + * + * A `TaskTracker` can keep track of pending tasks (grouped by type) and can notify interested + * parties when all pending tasks (or tasks of a specific type) have been completed. + * + * @param {$log} log - A logger instance (such as `$log`). Used to log error during callback + * execution. + * + * @this + */ +function $$TaskTrackerFactoryProvider() { + this.$get = valueFn(function(log) { return new TaskTracker(log); }); +} + +function TaskTracker(log) { + var self = this; + var taskCounts = {}; + var taskCallbacks = []; + + var ALL_TASKS_TYPE = self.ALL_TASKS_TYPE = '$$all$$'; + var DEFAULT_TASK_TYPE = self.DEFAULT_TASK_TYPE = '$$default$$'; + + /** + * Execute the specified function and decrement the appropriate `taskCounts` counter. + * If the counter reaches 0, all corresponding `taskCallbacks` are executed. + * + * @param {Function} fn - The function to execute. + * @param {string=} [taskType=DEFAULT_TASK_TYPE] - The type of task that is being completed. + */ + self.completeTask = completeTask; + + /** + * Increase the task count for the specified task type (or the default task type if non is + * specified). + * + * @param {string=} [taskType=DEFAULT_TASK_TYPE] - The type of task whose count will be increased. + */ + self.incTaskCount = incTaskCount; + + /** + * Execute the specified callback when all pending tasks have been completed. + * + * If there are no pending tasks, the callback is executed immediately. You can optionally limit + * the tasks that will be waited for to a specific type, by passing a `taskType`. + * + * @param {function} callback - The function to call when there are no pending tasks. + * @param {string=} [taskType=ALL_TASKS_TYPE] - The type of tasks that will be waited for. + */ + self.notifyWhenNoPendingTasks = notifyWhenNoPendingTasks; + + function completeTask(fn, taskType) { + taskType = taskType || DEFAULT_TASK_TYPE; + + try { + fn(); + } finally { + decTaskCount(taskType); + + var countForType = taskCounts[taskType]; + var countForAll = taskCounts[ALL_TASKS_TYPE]; + + // If at least one of the queues (`ALL_TASKS_TYPE` or `taskType`) is empty, run callbacks. + if (!countForAll || !countForType) { + var getNextCallback = !countForAll ? getLastCallback : getLastCallbackForType; + var nextCb; + + while ((nextCb = getNextCallback(taskType))) { + try { + nextCb(); + } catch (e) { + log.error(e); + } + } + } + } + } + + function decTaskCount(taskType) { + taskType = taskType || DEFAULT_TASK_TYPE; + if (taskCounts[taskType]) { + taskCounts[taskType]--; + taskCounts[ALL_TASKS_TYPE]--; + } + } + + function getLastCallback() { + var cbInfo = taskCallbacks.pop(); + return cbInfo && cbInfo.cb; + } + + function getLastCallbackForType(taskType) { + for (var i = taskCallbacks.length - 1; i >= 0; --i) { + var cbInfo = taskCallbacks[i]; + if (cbInfo.type === taskType) { + taskCallbacks.splice(i, 1); + return cbInfo.cb; + } + } + } + + function incTaskCount(taskType) { + taskType = taskType || DEFAULT_TASK_TYPE; + taskCounts[taskType] = (taskCounts[taskType] || 0) + 1; + taskCounts[ALL_TASKS_TYPE] = (taskCounts[ALL_TASKS_TYPE] || 0) + 1; + } + + function notifyWhenNoPendingTasks(callback, taskType) { + taskType = taskType || ALL_TASKS_TYPE; + if (!taskCounts[taskType]) { + callback(); + } else { + taskCallbacks.push({type: taskType, cb: callback}); + } + } +} diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js index 71bff4a40..76da4c72d 100644 --- a/src/ngMock/angular-mocks.js +++ b/src/ngMock/angular-mocks.js @@ -26,84 +26,27 @@ angular.mock = {}; * that there are several helper methods available which can be used in tests. */ angular.mock.$BrowserProvider = function() { - this.$get = function() { - return new angular.mock.$Browser(); - }; + this.$get = [ + '$log', '$$taskTrackerFactory', + function($log, $$taskTrackerFactory) { + return new angular.mock.$Browser($log, $$taskTrackerFactory); + } + ]; }; -angular.mock.$Browser = function() { - var ALL_TASKS_TYPE = '$$all$$'; - var DEFAULT_TASK_TYPE = '$$default$$'; +angular.mock.$Browser = function($log, $$taskTrackerFactory) { var self = this; + var taskTracker = $$taskTrackerFactory($log); this.isMock = true; self.$$url = 'http://server/'; self.$$lastUrl = self.$$url; // used by url polling fn self.pollFns = []; - // Testability API - - var outstandingRequestCounts = {}; - var outstandingRequestCallbacks = []; - - self.$$completeOutstandingRequest = completeOutstandingRequest; - self.$$incOutstandingRequestCount = incOutstandingRequestCount; - self.notifyWhenNoOutstandingRequests = notifyWhenNoOutstandingRequests; - - function decOutstandingRequestCount(taskType) { - taskType = taskType || DEFAULT_TASK_TYPE; - if (outstandingRequestCounts[taskType]) { - outstandingRequestCounts[taskType]--; - outstandingRequestCounts[ALL_TASKS_TYPE]--; - } - } - function incOutstandingRequestCount(taskType) { - taskType = taskType || DEFAULT_TASK_TYPE; - outstandingRequestCounts[taskType] = (outstandingRequestCounts[taskType] || 0) + 1; - outstandingRequestCounts[ALL_TASKS_TYPE] = (outstandingRequestCounts[ALL_TASKS_TYPE] || 0) + 1; - } - function completeOutstandingRequest(fn, taskType) { - taskType = taskType || DEFAULT_TASK_TYPE; - try { - fn(); - } finally { - decOutstandingRequestCount(taskType); - - var countForType = outstandingRequestCounts[taskType]; - var countForAll = outstandingRequestCounts[ALL_TASKS_TYPE]; - - // If at least one of the queues (`ALL_TASKS_TYPE` or `taskType`) is empty, run callbacks. - if (!countForAll || !countForType) { - var getNextCallback = !countForAll ? getLastCallback : getLastCallbackForType; - var nextCb; - - while ((nextCb = getNextCallback(taskType))) { - nextCb(); - } - } - } - } - function getLastCallback() { - var cbInfo = outstandingRequestCallbacks.pop(); - return cbInfo && cbInfo.cb; - } - function getLastCallbackForType(taskType) { - for (var i = outstandingRequestCallbacks.length - 1; i >= 0; --i) { - var cbInfo = outstandingRequestCallbacks[i]; - if (cbInfo.type === taskType) { - outstandingRequestCallbacks.splice(i, 1); - return cbInfo.cb; - } - } - } - function notifyWhenNoOutstandingRequests(callback, taskType) { - taskType = taskType || ALL_TASKS_TYPE; - if (!outstandingRequestCounts[taskType]) { - callback(); - } else { - outstandingRequestCallbacks.push({type: taskType, cb: callback}); - } - } + // Task-tracking API + self.$$completeOutstandingRequest = taskTracker.completeTask; + self.$$incOutstandingRequestCount = taskTracker.incTaskCount; + self.notifyWhenNoOutstandingRequests = taskTracker.notifyWhenNoPendingTasks; // register url polling fn @@ -131,9 +74,9 @@ angular.mock.$Browser = function() { var timeoutId = self.deferredNextId++; delay = delay || 0; - taskType = taskType || DEFAULT_TASK_TYPE; + taskType = taskType || taskTracker.DEFAULT_TASK_TYPE; - incOutstandingRequestCount(taskType); + taskTracker.incTaskCount(taskType); self.deferredFns.push({ id: timeoutId, type: taskType, @@ -164,7 +107,7 @@ angular.mock.$Browser = function() { if (angular.isDefined(taskIndex)) { var task = self.deferredFns.splice(taskIndex, 1)[0]; - completeOutstandingRequest(angular.noop, task.type); + taskTracker.completeTask(angular.noop, task.type); return true; } @@ -198,7 +141,7 @@ angular.mock.$Browser = function() { // Increment the time and call the next deferred function self.defer.now = self.deferredFns[0].time; var task = self.deferredFns.shift(); - completeOutstandingRequest(task.fn, task.type); + taskTracker.completeTask(task.fn, task.type); } // Ensure that the current time is correct diff --git a/test/ng/browserSpecs.js b/test/ng/browserSpecs.js index 7b8d7268d..acaf63e18 100644 --- a/test/ng/browserSpecs.js +++ b/test/ng/browserSpecs.js @@ -143,13 +143,14 @@ function MockDocument() { } describe('browser', function() { - /* global Browser: false */ - var browser, fakeWindow, fakeDocument, fakeLog, logs; + /* global Browser: false, TaskTracker: false */ + var browser, fakeWindow, fakeDocument, fakeLog, logs, taskTrackerFactory; beforeEach(function() { sniffer = {history: true}; fakeWindow = new MockWindow(); fakeDocument = new MockDocument(); + taskTrackerFactory = function(log) { return new TaskTracker(log); }; logs = {log:[], warn:[], info:[], error:[]}; @@ -160,7 +161,8 @@ describe('browser', function() { error: function() { logs.error.push(slice.call(arguments)); } }; - browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer); + + browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer, taskTrackerFactory); }); describe('MockBrowser', function() { @@ -200,7 +202,7 @@ describe('browser', function() { fakeWindow = new MockWindow({msie: msie}); fakeWindow.location.state = {prop: 'val'}; - browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer); + browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer, taskTrackerFactory); browser.url(fakeWindow.location.href, false, {prop: 'val'}); if (msie) { @@ -573,7 +575,7 @@ describe('browser', function() { // the initial URL contains a lengthy oauth token in the hash var initialUrl = 'http://test.com/oauthcallback#state=xxx%3D¬-before-policy=0'; fakeWindow.location.href = initialUrl; - browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer); + browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer, taskTrackerFactory); // somehow, $location gets a version of this url where the = is no longer escaped, and tells the browser: var initialUrlFixedByLocation = initialUrl.replace('%3D', '='); @@ -608,7 +610,7 @@ describe('browser', function() { replaceState = spyOn(fakeWindow.history, 'replaceState').and.callThrough(); locationReplace = spyOn(fakeWindow.location, 'replace').and.callThrough(); - browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer); + browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer, taskTrackerFactory); browser.onUrlChange(function() {}); }); @@ -707,7 +709,7 @@ describe('browser', function() { } }); - var browser = new Browser(mockWindow, fakeDocument, fakeLog, mockSniffer); + var browser = new Browser(mockWindow, fakeDocument, fakeLog, mockSniffer, taskTrackerFactory); expect(historyStateAccessed).toBe(false); }); @@ -720,7 +722,7 @@ describe('browser', function() { return function() { beforeEach(function() { fakeWindow = new MockWindow({msie: options.msie}); - browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer); + browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer, taskTrackerFactory); }); it('should return history.state', function() { @@ -823,7 +825,7 @@ describe('browser', function() { return function() { beforeEach(function() { fakeWindow = new MockWindow({msie: options.msie}); - browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer); + browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer, taskTrackerFactory); }); it('should fire onUrlChange listeners only once if both popstate and hashchange triggered', function() { @@ -892,7 +894,7 @@ describe('browser', function() { function setup(options) { fakeWindow = new MockWindow(options); - browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer); + browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer, taskTrackerFactory); module(function($provide, $locationProvider) { diff --git a/test/ng/locationSpec.js b/test/ng/locationSpec.js index 5814b405b..58d4d013d 100644 --- a/test/ng/locationSpec.js +++ b/test/ng/locationSpec.js @@ -2873,9 +2873,9 @@ describe('$location', function() { }; return win; }; - $browserProvider.$get = function($document, $window, $log, $sniffer) { + $browserProvider.$get = function($document, $window, $log, $sniffer, $$taskTrackerFactory) { /* global Browser: false */ - browser = new Browser($window, $document, $log, $sniffer); + browser = new Browser($window, $document, $log, $sniffer, $$taskTrackerFactory); browser.baseHref = function() { return options.baseHref; };