Skip to content

Commit

Permalink
beef up $defer service
Browse files Browse the repository at this point in the history
- adding concept of named, manually flushable queues
- unifying the api for defering stuff via queues and setTimeout
- adding support for canceling tasks
  • Loading branch information
IgorMinar committed May 28, 2011
1 parent bbff9cf commit b004276
Show file tree
Hide file tree
Showing 2 changed files with 194 additions and 62 deletions.
94 changes: 83 additions & 11 deletions src/service/defer.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,92 @@
* @requires $updateView
*
* @description
* Delegates to {@link angular.service.$browser.defer $browser.defer}, but wraps the `fn` function
* into a try/catch block and delegates any exceptions to
* {@link angular.services.$exceptionHandler $exceptionHandler} service.
* Defers an execution of a function using various queues.
*
* In tests you can use `$browser.defer.flush()` to flush the queue of deferred functions.
* When `queue` argument is undefined or set to `'$setTimeout'`, the defered function `fn` will be
* delegated to {@link angular.service.$browser.defer $browser.defer}, which will result in
* in a `setTimeout` timer to be registered with delay set to the `delay` value. The function
* `fn` will be wrapped into {@link angular.scope.$apply rootScope.$apply}, in order to to allow the
* deferred function to participate in angular's app life-cycle.
*
* @param {function()} fn A function, who's execution should be deferred.
* @param {number=} [delay=0] of milliseconds to defer the function execution.
* In tests you can use `$defer.flush('$setTimeout') or `$browser.defer.flush()` to flush the queue
* of deferred functions in the `'$setTimeout'` queue.
*
* When `queue` argument is defined, the `fn` function will be put into a queue specified by this
* argument. To flush the queue in application or test, call `$defer.flush(queueName)`.
*
* Angular uses a queue called `'$burp'`, to execute task synchronously with regards to the $apply
* cycle. This queue is flushed right after `$digest` (hence the name).
*
* A task can be removed from any execution queue (if it hasn't executed yet), by calling
*`$defer.cancel(cancelToken)`, where `cancelToken` is the return value of calling the $defer
* function when registering `fn`.
*
* @param {string=} [queue='$setTimeout'] The name of the deferral queue.
* @param {function()} fn A task — function, execution of which should be deferred.
* @param {(number|string)=} [delay=0] of milliseconds to defer the function execution in the
* $setTimeout queue.
* @returns {*} A token, which can be passed into $defer.cancel() method to cancel the deferred
* task.
*/
angularServiceInject('$defer', function($browser) {
var scope = this;
return function(fn, delay) {
$browser.defer(function() {
angularServiceInject('$defer', function($browser, $exceptionHandler) {
var scope = this,
queues = {},
canceledTasks = {},
idGenerator = 0,
setTimeoutQ = '$setTimeout';

function defer(queue, fn, delay) {
if (isFunction(queue)) {
delay = fn;
fn = queue;
queue = setTimeoutQ;
}

if (queue != setTimeoutQ) {
var id = idGenerator++;
(queues[queue] || (queues[queue] = [])).push({id: id, fn: fn});
return {q: queue, id: id};
}

return $browser.defer(function() {
scope.$apply(fn);
}, delay);
};
}, ['$browser', '$exceptionHandler', '$updateView']);


defer.flush = function(queue) {
assertArg(queue, 'queue');

if (queue == setTimeoutQ) {
$browser.defer.flush();
}

forEach(queues[queue], function(task) {
try {
if (!(canceledTasks[queue] && canceledTasks[queue][task.id])) {
task.fn();
}
} catch(e) {
$exceptionHandler(e);
}
});

queues[queue] = [];
canceledTasks[queue] = {};
}


defer.cancel = function(cancelToken) {
if (isUndefined(cancelToken)) return;

if (cancelToken.q) {
(canceledTasks[cancelToken.q] || (canceledTasks[cancelToken.q] = {}))[cancelToken.id] = true;
} else {
$browser.defer.cancel(deferId);
}
}


return defer;
}, ['$browser', '$exceptionHandler']);
162 changes: 111 additions & 51 deletions test/service/deferSpec.js
Original file line number Diff line number Diff line change
@@ -1,76 +1,136 @@
describe('$defer', function() {
var scope, $browser, $defer, $exceptionHandler;

beforeEach(function(){
scope = angular.scope(angular.service,
{'$exceptionHandler': jasmine.createSpy('$exceptionHandler')});
$browser = scope.$service('$browser');
$defer = scope.$service('$defer');
$exceptionHandler = scope.$service('$exceptionHandler');
});

afterEach(function(){
dealoc(scope);
});
describe('setTimeout backed deferral', function() {
var scope, $browser, $defer, $exceptionHandler;

beforeEach(function(){
scope = angular.scope(angular.service,
{'$exceptionHandler': jasmine.createSpy('$exceptionHandler')});
$browser = scope.$service('$browser');
$defer = scope.$service('$defer');
$exceptionHandler = scope.$service('$exceptionHandler');
});

it('should delegate functions to $browser.defer', function() {
var counter = 0;
$defer(function() { counter++; });
afterEach(function(){
dealoc(scope);
});

expect(counter).toBe(0);

$browser.defer.flush();
expect(counter).toBe(1);
it('should delegate functions to $browser.defer', function() {
var counter = 0;
$defer(function() { counter++; });

$browser.defer.flush(); //does nothing
expect(counter).toBe(1);
expect(counter).toBe(0);

expect($exceptionHandler).not.toHaveBeenCalled();
});
$browser.defer.flush();
expect(counter).toBe(1);

$browser.defer.flush(); //does nothing
expect(counter).toBe(1);

it('should delegate exception to the $exceptionHandler service', function() {
$defer(function() {throw "Test Error";});
expect($exceptionHandler).not.toHaveBeenCalled();
expect($exceptionHandler).not.toHaveBeenCalled();
});

$browser.defer.flush();
expect($exceptionHandler).toHaveBeenCalledWith("Test Error");
});

it('should delegate exception to the $exceptionHandler service', function() {
$defer(function() {throw "Test Error";});
expect($exceptionHandler).not.toHaveBeenCalled();

it('should call eval after each callback is executed', function() {
var eval = this.spyOn(scope, '$eval').andCallThrough();
$browser.defer.flush();
expect($exceptionHandler).toHaveBeenCalledWith("Test Error");
});

$defer(function() {});
expect(eval).wasNotCalled();

$browser.defer.flush();
expect(eval).wasCalled();
it('should call $apply after each callback is executed', function() {
var eval = this.spyOn(scope, '$apply').andCallThrough();

eval.reset(); //reset the spy;
$defer(function() {});
expect(eval).wasNotCalled();

$defer(function() {});
$defer(function() {});
$browser.defer.flush();
expect(eval.callCount).toBe(2);
});
$browser.defer.flush();
expect(eval).wasCalled();

eval.reset(); //reset the spy;

$defer(function() {});
$defer(function() {});
$browser.defer.flush();
expect(eval.callCount).toBe(2);
});

it('should call eval even if an exception is thrown in callback', function() {
var eval = this.spyOn(scope, '$eval').andCallThrough();

$defer(function() {throw "Test Error";});
expect(eval).wasNotCalled();
it('should call $apply even if an exception is thrown in callback', function() {
var eval = this.spyOn(scope, '$apply').andCallThrough();

$browser.defer.flush();
expect(eval).wasCalled();
$defer(function() {throw "Test Error";});
expect(eval).wasNotCalled();

$browser.defer.flush();
expect(eval).wasCalled();
});


it('should allow you to specify the delay time', function(){
var defer = this.spyOn($browser, 'defer');
$defer(noop, 123);
expect(defer.callCount).toEqual(1);
expect(defer.mostRecentCall.args[1]).toEqual(123);
});
});

it('should allow you to specify the delay time', function(){
var defer = this.spyOn($browser, 'defer');
$defer(noop, 123);
expect(defer.callCount).toEqual(1);
expect(defer.mostRecentCall.args[1]).toEqual(123);

describe('queue based deferral', function() {
var scope, defer, log;

beforeEach(function() {
scope = angular.scope();
$defer = scope.$service('$defer');
log = [];
});


it('should allow a task to be scheduled and executed upon flush()', function() {
var id = $defer('myQueue', function() { log.push('work'); });
expect(id).toBeDefined();
expect(log).toEqual([]);

$defer.flush('wrongQueue');
expect(log).toEqual([]);

$defer.flush('myQueue');
expect(log).toEqual(['work']);
});


it('should allow a task to be overriden by another task', function() {
$defer('myQueue', function() { log.push('work 0') });
var id = $defer('myQueue', function() { log.push('work 1') });
$defer('myQueue', function() { log.push('work 2') });
$defer('myQueue', function() { log.push('work 3') });
$defer.cancel(id);

$defer.flush('myQueue');
expect(log).toEqual(['work 0', 'work 2', 'work 3']);
});


it('should ignore attempts to overide flushed tasks', function() {
var id = $defer('myQueue', function() { log.push('work 0') });
$defer.flush('myQueue');

$defer('myQueue', function() { log.push('work 1') });
$defer.cancel(id);
$defer.flush('myQueue');

expect(log).toEqual(['work 0', 'work 1']);
});


it('should generate different ids for tasks', function() {
var id1 = $defer('myQueue', function() {});
var id2 = $defer('myQueue', function() {});
var id3 = $defer('myQueue', function() {});
expect(id1.id < id2.id < id3.id).toBe(true);
});
});
});

0 comments on commit b004276

Please sign in to comment.