<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="style.css">
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" integrity="sha512-dTfge/zgoMYpP7QbHy4gWMEGsbsdZeCXz7irItjcC3sPUFtf0kuFbDz/ixG7ArTxmDjLXDmezHubeNikyKGVyQ==" crossorigin="anonymous">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular-mocks.js"></script>
<script src="QueueModule.js"></script>
<script src="backend.js"></script>
</head>
<body>
<h1 class="text-center" style="padding-bottom: 1em;">Managing Queue</h1>
<article ng-app="myApp">
<div ng-controller="MyAppCtrl">
<div class="container-fluid">
<div class="" ng-repeat="task in tasks">
<div class="row form-group">
<div class="col-xs-12 text-center" ng-bind="$index"></div>
</div>
<div class="col-xs-10 col-xs-offset-1">
<form name="taskForm" ng-submit="onTaskEdited($event, task, taskForm)">
<input class="form-control form-group" ng-model="task.name" placeholder="Task Name" />
<textarea placeholder="Description" class="form-control form-group" ng-model="task.description"></textarea>
<button class="btn btn-primary pull-right" type="submit">Send</button>
</form>
</div>
<div class="row">
<div class="col-xs-12"><hr /></div>
</div>
</div>
</div>
</div>
</article>
<script src="script.js"></script>
</body>
</html>
angular
.module('myApp', ['Queue', 'Backend'])
.controller('MyAppCtrl', function($httpQueue, $scope, $interval) {
var vm = $scope;
var
pollingCount = 0, // infinite polling
pollingDelay = 1000
;
// using a route.resolve could be better!
$httpQueue
.pull()
.then(function(tasks) { vm.tasks = tasks; })
.catch(function() { vm.tasks = [{ name: '', description: '' }]; })
.finally(function() { return $interval(vm.updateViewModel.bind(vm), pollingDelay, pollingCount, true); })
;
var isLastPullFinished = false;
vm.updateViewModel = function() {
if(!isLastPullFinished) { return; }
return $http
.pull()
.then(function(tasks) {
for(var i = 0, len = tasks.length; i < len; i++) {
for(var j = 0, jLen = vm.tasks.length; j < jLen; j++) {
if(tasks[i].id !== vm.tasks[j].id) { continue; }
// todo: manage recursively merging, in angular 1.3+ there is a
// merge method https://docs.angularjs.org/api/ng/function/angular.merge
// todo: control if the task model is $dirty (if the user is editing it)
angular.extend(vm.tasks[j], tasks[i]);
}
};
return vm.tasks;
})
.finally(function() {
isLastPullfinished = true;
})
;
};
vm.onTaskEdited = function(event, task, form) {
event.preventDefault();
if(form.$invalid || form.$pristine ) { return; }
return $httpQueue.push(task);
};
})
;
/* Styles go here */
(function(window, angular, APP) {
'use strict';
function $httpQueueFactory($q, $http) {
var self = this;
var api = '/api/v1/tasks';
self.MAX_ERRORS = 5;
var errorCount = 0;
self.queue = [];
var processing = false;
//Assume it as a private method, never call it directly
self._each = function() {
var configs = { cache: false };
if(errorCount >= self.MAX_ERRORS) {
return;
}
return self
.isQueueEmpty()
.then(function(count) {
processing = false;
return count;
})
.catch(function() {
if(processing) {
return;
}
processing = true;
var payload = self.queue.shift();
var route = api;
var task = 'post';
if(payload.id) {
task = 'put';
route = api + '/' + payload.id;
}
return $http
[task](route, payload, configs)
.catch(function(error) {
console.error('$httpQueue._each:error', error, payload);
//because of the error we re-append this task to the queue;
errorCount += 1;
return self.push(payload);
})
.finally(function() {
processing = false;
return self._each();
})
;
})
;
};
self.isQueueEmpty = function() {
var length = self.queue.length;
var task = length > 0 ? 'reject' : 'when';
return $q[task](length);
};
self.push = function(data) {
self.queue.push(data);
self._each();
return self;
};
self.pull = function(params) {
var configs = { cache: false };
configs.params = angular.extend({}, params || {});
return $http
.get(api, configs)
.then(function(result) {
console.info('$httpQueue.pull:success', result);
return result.data;
})
.catch(function(error) {
console.error('$httpQueue.pull:error', error);
return $q.reject(error);
})
;
};
}
APP
.service('$httpQueue', ['$q', '$http', $httpQueueFactory])
;
})(window, window.angular, window.angular.module('Queue', []));
(function(window, angular, APP) {
'use strict';
var api = '/api/v1/tasks';
function decorate$httpBackend($provide) {
$provide.decorator('$httpBackend', function($delegate) {
var proxy = function(method, url, data, callback, headers) {
var interceptor = function() {
var _this = this,
_arguments = arguments;
setTimeout(function() {
console.log('TaskProcessed:respond', _arguments);
callback.apply(_this, _arguments);
}, 3500);
};
return $delegate.call(this, method, url, data, interceptor, headers);
};
for(var key in $delegate) {
proxy[key] = $delegate[key];
}
return proxy;
});
}
function backend($httpBackend) {
$httpBackend
.whenGET(api)
.respond(function(method, url, data, headers, params) {
return [200, [
{name: 'Foo', description: 'Baz'},
{name: 'Foo1', description: 'Baz1'},
{name: 'Foo2', description: 'Baz2'},
{name: 'Foo3', description: 'Baz3'},
{name: 'Foo4', description: 'Baz4'},
{name: 'Foo5', description: 'Baz5'},
{name: 'Foo6', description: 'Baz6'},
{name: 'Foo7', description: 'Baz7'},
{name: 'Foo8', description: 'Baz8'}
], {}, 'TaskList']
})
;
$httpBackend
.whenPOST(api)
.respond(function(method, url, data, headers, params) {
console.count('New Create Task Request');
return [200, null, {}, 'TaskCreated']
})
;
}
APP
.config(decorate$httpBackend)
.run(backend)
;
})(window, window.angular, window.angular.module('Backend', ['ngMockE2E']));