AngularJS and web workers
JavascriptAngularjsWeb WorkerJavascript Problem Overview
How can angularJS use web workers to run processes in the background? Is there any pattern I should follow on doing this?
Currently, I am using a service that has the model in a separate web worker. This service implements methods like:
ClientsFacade.calculateDebt(client1); //Just an example..
In the implementation, this method sends a message to the worker with the data. This allows me to abstract the fact that it is being performed in a separate thread and I could also provide an implementation that queries against a server or even one that does this action in the same thread.
Since I'm new to javascript and I'm just recycling knowledge I have from other platforms I wonder if this is something you would do or perhaps Angular which is what I am using, offers a sort of way of doing this. Also this introduces a change in my architecture since the worker must explicitly push changes to the controller, which then updates its values and then this is reflected in the view, am I over engineering this? It's a bit frustrating that web workers "protect" me so much from screwing up by not allowing me to share memory etc.
Javascript Solutions
Solution 1 - Javascript
Communication with Web workers happens through a messaging mechanism. Intercepting these messages happens in a call back. In AngularJS, the best location to put a web worker is in a service as you duly noted. The best way to deal with this is to use promises, which Angular works amazingly with.
Here is an example of a webworker
in a service
var app = angular.module("myApp",[]);
app.factory("HelloWorldService",['$q',function($q){
var worker = new Worker('doWork.js');
var defer = $q.defer();
worker.addEventListener('message', function(e) {
console.log('Worker said: ', e.data);
defer.resolve(e.data);
}, false);
return {
doWork : function(myData){
defer = $q.defer();
worker.postMessage(myData); // Send data to our worker.
return defer.promise;
}
};
});
Now whatever external entity that accesses Hello World service need not care about the implementation details of HelloWorldService
- HelloWorldService
could probably process the data over a web worker
, over http
or do the processing right there.
Hope this makes sense.
Solution 2 - Javascript
A very interesting question! I find the web worker specification a bit awkward (probably for good reasons, but still awkward). The need to keep the worker code in a separate file makes the intention of a service hard to read and introduces dependencies to static file URLs in your angular application code. This problem can be mitigated by using the URL.createObjectUrl() which can be used to create a URL for a JavaScript string. This allows us to specify the worker code in the same file which creates the worker.
var blobURL = URL.createObjectURL(new Blob([
"var i = 0;//web worker body"
], { type: 'application/javascript' }));
var worker = new Worker(blobURL);
The web worker spec also keeps the worker and main thread contexts completely separate to prevent situations were deadlocks and livelocks etc can occur. But it also means you won't have access to your angular services in the worker without some fiddling. The worker lack some of the things we(and angular) expect when executing JavaScript in the browser, like the global variable "document" etc. By "mocking" these required browser features in the worker we can get angular to run.
var window = self;
self.history = {};
var document = {
readyState: 'complete',
cookie: '',
querySelector: function () {},
createElement: function () {
return {
pathname: '',
setAttribute: function () {}
};
}
};
Some features obviously won't work, bindings to the DOM etc. But the injection framework and for example the $http service will work just fine, which is probably what we want in a worker. What we gain by this is that we can run standard angular services in a worker. We can therefore unit test the services used in the worker just as we would with any other angular dependency.
I made a post which elaborates a bit more about this here and created a github repo which creates a service which implements the ideas discussed above here
Solution 3 - Javascript
I found a fully working example of web workers in Angular [here](http://www.codingterminal.com/articles/092014/1/XMLHttpRequest%20using%20AngularJS%20Promises%20and%20HTML5%20Web%20Workers.php "Angular JS and Web Workers")
webworker.controller('webWorkerCtrl', ['$scope', '$q', function($scope, $q) {
$scope.workerReplyUI;
$scope.callWebWorker = function() {
var worker = new Worker('worker.js');
var defer = $q.defer();
worker.onmessage = function(e) {
defer.resolve(e.data);
worker.terminate();
};
worker.postMessage("http://jsonplaceholder.typicode.com/users");
return defer.promise;
}
$scope.callWebWorker().then(function(workerReply) {
$scope.workerReplyUI = workerReply;
});
}]);
It uses promises to wait for the worker to return the result.
Solution 4 - Javascript
> Angular Web Worker with polling example
When you are dealing with the workers in AngularJS its often required that your worker script to be inline(incase you are using some build tools like gulp/grunt) and we can achieve this using the following approach.
Example below also shows how polling can be done to server using workers:
First lets create our worker factory:
module.factory("myWorker", function($q) {
var worker = undefined;
return {
startWork: function(postData) {
var defer = $q.defer();
if (worker) {
worker.terminate();
}
// function to be your worker
function workerFunction() {
var self = this;
self.onmessage = function(event) {
var timeoutPromise = undefined;
var dataUrl = event.data.dataUrl;
var pollingInterval = event.data.pollingInterval;
if (dataUrl) {
if (timeoutPromise) {
setTimeout.cancel(timeoutPromise); // cancelling previous promises
}
console.log('Notifications - Data URL: ' + dataUrl);
//get Notification count
var delay = 5000; // poller 5sec delay
(function pollerFunc() {
timeoutPromise = setTimeout(function() {
var xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function() {
if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
var response = JSON.parse(xmlhttp.responseText);
self.postMessage(response.id);
pollerFunc();
}
};
xmlhttp.open('GET', dataUrl, true);
xmlhttp.send();
}, delay);
})();
}
}
}
// end worker function
var dataObj = '(' + workerFunction + ')();'; // here is the trick to convert the above fucntion to string
var blob = new Blob([dataObj.replace('"use strict";', '')]); // firefox adds user strict to any function which was blocking might block worker execution so knock it off
var blobURL = (window.URL ? URL : webkitURL).createObjectURL(blob, {
type: 'application/javascript; charset=utf-8'
});
worker = new Worker(blobURL);
worker.onmessage = function(e) {
console.log('Worker said: ', e.data);
defer.notify(e.data);
};
worker.postMessage(postData); // Send data to our worker.
return defer.promise;
},
stopWork: function() {
if (worker) {
worker.terminate();
}
}
}
});
Next from our controller call the worker factory:
var inputToWorker = {
dataUrl: "http://jsonplaceholder.typicode.com/posts/1", // url to poll
pollingInterval: 5 // interval
};
myWorker.startWork(inputToWorker).then(function(response) {
// complete
}, function(error) {
// error
}, function(response) {
// notify (here you receive intermittent responses from worker)
console.log("Notification worker RESPONSE: " + response);
});
You can call myWorker.stopWork();
any time to terminate the worker from your controller!
This is tested in IE11+ and FF and Chrome
Solution 5 - Javascript
you can also take a look at angular plugin https://github.com/vkiryukhin/ng-vkthread
which allows you to execute a function in a separate thread. basic usage:
/* function to execute in a thread */
function foo(n, m){
return n + m;
}
/* create an object, which you pass to vkThread as an argument*/
var param = {
fn: foo // <-- function to execute
args: [1, 2] // <-- arguments for this function
};
/* run thread */
vkThread.exec(param).then(
function (data) {
console.log(data); // <-- thread returns 3
}
);
Examples and API doc: http://www.eslinstructor.net/ng-vkthread/demo/
--Vadim