Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

Commit

Permalink
feat(*): implement more granular pending task tracking
Browse files Browse the repository at this point in the history
Previously, all pending async tasks (tracked via `$browser`) are treated
the same. I.e. things like `$$testability.whenStable()` and
`ngMock#$timeout.verifyNoPendingTasks()` take all tasks into account.

Yet, in some cases we might be interested in specific tasks only. For
example, if one wants to verify there are no pending `$timeout`s, they
don't care if there are other pending tasks, such as `$http` requests.
Similarly, one might want to get notified when all `$http` requests have
completed and does not care about pending promises.

This commit adds support for more granular task tracking, by enabling
callers to specify the type of task that is being added/removed from the
queue and enabling listeners to be triggered when specific types of
tasks are completed (even if there are more pending tasks of different
types).

The change is backwards compatible. I.e. calling the affected methods
with no explicit task-type, behaves the same as before.

Related to #14336.
gkalpak committed Jul 13, 2018

Unverified

This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
1 parent 10973c3 commit 17b139f
Showing 13 changed files with 579 additions and 194 deletions.
101 changes: 76 additions & 25 deletions src/ng/browser.js
Original file line number Diff line number Diff line change
@@ -23,35 +23,48 @@
* @param {object} $sniffer $sniffer service
*/
function Browser(window, document, $log, $sniffer) {
var ALL_TASKS_TYPE = '$$all$$',
DEFAULT_TASK_TYPE = '$$default$$';

var self = this,
location = window.location,
history = window.history,
setTimeout = window.setTimeout,
clearTimeout = window.clearTimeout,
pendingDeferIds = {};
pendingDeferIds = {},
outstandingRequestCounts = {},
outstandingRequestCallbacks = [];

self.isMock = false;

var outstandingRequestCount = 0;
var outstandingRequestCallbacks = [];

// TODO(vojta): remove this temporary api
self.$$completeOutstandingRequest = completeOutstandingRequest;
self.$$incOutstandingRequestCount = function() { outstandingRequestCount++; };
self.$$incOutstandingRequestCount = incOutstandingRequestCount;

/**
* Executes the `fn` function(supports currying) and decrements the `outstandingRequestCallbacks`
* counter. If the counter reaches 0, all the `outstandingRequestCallbacks` are executed.
* 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) {
function completeOutstandingRequest(fn, taskType) {
taskType = taskType || DEFAULT_TASK_TYPE;
try {
fn.apply(null, sliceArgs(arguments, 1));
fn();
} finally {
outstandingRequestCount--;
if (outstandingRequestCount === 0) {
while (outstandingRequestCallbacks.length) {
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 {
outstandingRequestCallbacks.pop()();
nextCb();
} catch (e) {
$log.error(e);
}
@@ -60,6 +73,35 @@ function Browser(window, document, $log, $sniffer) {
}
}

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);
@@ -68,13 +110,15 @@ function Browser(window, document, $log, $sniffer) {
/**
* @private
* TODO(vojta): prefix this method with $$ ?
* @param {function()} callback Function that will be called when no outstanding request
* @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) {
if (outstandingRequestCount === 0) {
self.notifyWhenNoOutstandingRequests = function(callback, taskType) {
taskType = taskType || ALL_TASKS_TYPE;
if (!outstandingRequestCounts[taskType]) {
callback();
} else {
outstandingRequestCallbacks.push(callback);
outstandingRequestCallbacks.push({type: taskType, cb: callback});
}
};

@@ -307,7 +351,8 @@ function Browser(window, document, $log, $sniffer) {
/**
* @name $browser#defer
* @param {function()} fn A function, who's execution should be deferred.
* @param {number=} [delay=0] of milliseconds to defer the function execution.
* @param {number=} [delay=0] Number of milliseconds to defer the function execution.
* @param {string=} [taskType=DEFAULT_TASK_TYPE] The type of task that is deferred.
* @returns {*} DeferId that can be used to cancel the task via `$browser.defer.cancel()`.
*
* @description
@@ -318,14 +363,19 @@ function Browser(window, document, $log, $sniffer) {
* via `$browser.defer.flush()`.
*
*/
self.defer = function(fn, delay) {
self.defer = function(fn, delay, taskType) {
var timeoutId;
outstandingRequestCount++;

delay = delay || 0;
taskType = taskType || DEFAULT_TASK_TYPE;

incOutstandingRequestCount(taskType);
timeoutId = setTimeout(function() {
delete pendingDeferIds[timeoutId];
completeOutstandingRequest(fn);
}, delay || 0);
pendingDeferIds[timeoutId] = true;
completeOutstandingRequest(fn, taskType);
}, delay);
pendingDeferIds[timeoutId] = taskType;

return timeoutId;
};

@@ -341,10 +391,11 @@ function Browser(window, document, $log, $sniffer) {
* canceled.
*/
self.defer.cancel = function(deferId) {
if (pendingDeferIds[deferId]) {
if (pendingDeferIds.hasOwnProperty(deferId)) {
var taskType = pendingDeferIds[deferId];
delete pendingDeferIds[deferId];
clearTimeout(deferId);
completeOutstandingRequest(noop);
completeOutstandingRequest(noop, taskType);
return true;
}
return false;
4 changes: 2 additions & 2 deletions src/ng/http.js
Original file line number Diff line number Diff line change
@@ -1054,7 +1054,7 @@ function $HttpProvider() {
config.paramSerializer = isString(config.paramSerializer) ?
$injector.get(config.paramSerializer) : config.paramSerializer;

$browser.$$incOutstandingRequestCount();
$browser.$$incOutstandingRequestCount('$http');

var requestInterceptors = [];
var responseInterceptors = [];
@@ -1092,7 +1092,7 @@ function $HttpProvider() {
}

function completeOutstandingRequest() {
$browser.$$completeOutstandingRequest(noop);
$browser.$$completeOutstandingRequest(noop, '$http');
}

function executeHeaderFns(headers, config) {
4 changes: 2 additions & 2 deletions src/ng/rootScope.js
Original file line number Diff line number Diff line change
@@ -1122,7 +1122,7 @@ function $RootScopeProvider() {
if (asyncQueue.length) {
$rootScope.$digest();
}
});
}, null, '$evalAsync');
}

asyncQueue.push({scope: this, fn: $parse(expr), locals: locals});
@@ -1493,7 +1493,7 @@ function $RootScopeProvider() {
if (applyAsyncId === null) {
applyAsyncId = $browser.defer(function() {
$rootScope.$apply(flushApplyAsync);
});
}, null, '$applyAsync');
}
}
}];
10 changes: 9 additions & 1 deletion src/ng/testability.js
Original file line number Diff line number Diff line change
@@ -104,7 +104,15 @@ function $$TestabilityProvider() {
* @name $$testability#whenStable
*
* @description
* Calls the callback when $timeout and $http requests are completed.
* Calls the callback when all pending tasks are completed.
*
* Types of tasks waited for include:
* - Pending timeouts (via {@link $timeout}).
* - Pending HTTP requests (via {@link $http}).
* - In-progress route transitions (via {@link $route}).
* - Pending tasks scheduled via {@link $rootScope#$applyAsync}.
* - Pending tasks scheduled via {@link $rootScope#$evalAsync}.
* These include tasks scheduled via `$evalAsync()` indirectly (such as {@link $q} promises).
*
* @param {function} callback
*/
2 changes: 1 addition & 1 deletion src/ng/timeout.js
Original file line number Diff line number Diff line change
@@ -63,7 +63,7 @@ function $TimeoutProvider() {
}

if (!skipApply) $rootScope.$apply();
}, delay);
}, delay, '$timeout');

promise.$$timeoutId = timeoutId;
deferreds[timeoutId] = deferred;
Loading

0 comments on commit 17b139f

Please sign in to comment.