├── .bowerrc ├── .editorconfig ├── .gitignore ├── .jshintrc ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Gruntfile.js ├── LICENSE ├── README.md ├── bower.json ├── dist ├── angular-workers.js └── angular-workers.min.js ├── example ├── app.js └── index.html ├── karma.conf.js ├── package.json ├── src ├── angular-workers.js └── test │ └── PiCalculator.js └── test └── unit ├── angular-angular-1-4-Spec.js ├── angular-angular-1-5-Spec.js └── angular-workersSpec.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | node_modules 4 | components 5 | bower_components 6 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "es5": true, 4 | "esnext": false, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": false, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": true, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true 21 | } 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | 5 | before_install: 6 | - npm install -g grunt-cli karma bower 7 | - bower install 8 | 9 | before_script: 10 | - export DISPLAY=:99.0 11 | - sh -e /etc/init.d/xvfb start 12 | 13 | script: "grunt" 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FredrikSandell/angular-workers/357bec609b34c23c2bf181a3c83c521d388d79c7/CHANGELOG.md -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | Contributing to this repo is fairly easy. This document shows you how to 4 | get the project, run all provided tests and generate a production ready build. 5 | 6 | It also covers provided grunt tasks, that help you developing on this repo. 7 | 8 | ## Dependencies 9 | 10 | To make sure, that the following instructions work, please install the following dependecies 11 | on you machine: 12 | 13 | - Node.js 14 | - npm 15 | - Git 16 | 17 | If you install node through the binary installation file, **npm** will be already there. 18 | When **npm** is installed, use it to install the needed npm packages: 19 | 20 | - bower npm install -g bower 21 | - grunt-cli npm install -g grunt-cli 22 | - karma npm install -g karma 23 | 24 | ## Installation 25 | 26 | To get the source of this project clone the git repository via: 27 | 28 | ```` 29 | $ git clone https://github.com/PascalPrecht/angular-component-seed 30 | ```` 31 | 32 | This will clone the complete source to your local machine. Navigate to the project folder 33 | and install all needed dendencies via **npm** and **bower**: 34 | 35 | ```` 36 | $ npm install 37 | $ bower install 38 | ```` 39 | 40 | The project is now installed and ready to be built. 41 | 42 | ## Building 43 | 44 | This repo comes with a few **grunt tasks** which help you to automate 45 | the development process. The following grunt tasks are provided: 46 | 47 | #### grunt 48 | 49 | Running grunt without any parameters, will actually execute the registered 50 | default task. This task is currently nothing more then a **lint task**, to make sure 51 | that your JavaScript code is written well. 52 | 53 | #### grunt test 54 | 55 | grunt test executes (as you might thought) the unit tests, which are located 56 | in test/unit. The task uses **karma** the spectacular test runner to executes 57 | the tests with the **jasmine testing framework**. 58 | 59 | #### grunt build 60 | 61 | You only have to use this task, if you want to generate a production ready build of 62 | this project. This task will also **lint**, **test** and **minify** the 63 | source. After running this task, you'll find the following files in a generated 64 | dist folder: 65 | 66 | ```` 67 | dist/angular--x.x.x.js 68 | dist/angular--x.x.x.min.js 69 | ```` 70 | 71 | #### grunt watch 72 | 73 | This task will watch all relevant files. When it notice a change, it'll run the 74 | **lint** and **test** task. Use this task while developing on the source 75 | to make sure, everytime you make a change you get notified if your code is incosistent 76 | or doesn't pass the tests. 77 | 78 | ## Contributing/Submitting changes 79 | 80 | - Checkout a new branch based on master and name it to what you intend to do: 81 | - Example: 82 | ```` 83 | $ git checkout -b BRANCH_NAME 84 | ```` 85 | - Use one branch per fix/feature 86 | - Make your changes 87 | - Make sure to provide a spec for unit tests 88 | - Run your tests with either karma start or grunt test 89 | - When all tests pass, everything's fine 90 | - Commit your changes 91 | - Please provide a git message which explains what you've done 92 | - This repo uses [Brian's conventional-changelog task](https://github.com/btford/grunt-conventional-changelog) so please make sure your commits follow the [conventions](https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit) 93 | - Commit to the forked repository 94 | - Make a pull request 95 | - Make sure you send the PR to the canary branch 96 | - Travis CI is watching you! 97 | 98 | If you follow these instructions, your PR will land pretty safety in the main repo! 99 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | 'use strict'; 3 | 4 | require('load-grunt-tasks')(grunt); 5 | var _ = require('lodash'); 6 | 7 | var karmaConfig = function(configFile, customOptions) { 8 | var options = { configFile: configFile, keepalive: true }; 9 | var travisOptions = process.env.TRAVIS && { browsers: ['Firefox'], reporters: 'dots' }; 10 | return _.extend(options, customOptions, travisOptions); 11 | }; 12 | 13 | grunt.initConfig({ 14 | pkg: grunt.file.readJSON('bower.json'), 15 | meta: { 16 | banner: '/*! <%= pkg.title || pkg.name %> - v<%= pkg.version %> - ' + 17 | '<%= grunt.template.today("yyyy-mm-dd") %>\n' + 18 | '<%= pkg.homepage ? "* " + pkg.homepage + "\n" : "" %>' + 19 | '* Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %>;' + 20 | ' Licensed <%= _.pluck(pkg.licenses, "type").join(", ") %> */' 21 | }, 22 | watch: { 23 | scripts: { 24 | files: ['Gruntfile.js', 'src/**/*.js', 'test/**/*.js'], 25 | tasks: ['jshint', 'karma:unit'] 26 | } 27 | }, 28 | jshint: { 29 | all: ['Gruntfile.js', 'src/**/*.js', 'test/**/*.js'], 30 | options: { 31 | eqeqeq: true, 32 | globals: { 33 | angular: true 34 | } 35 | } 36 | }, 37 | concat: { 38 | src: { 39 | src: ['src/angular-workers.js'], 40 | dest: 'dist/angular-workers.js' 41 | } 42 | }, 43 | uglify: { 44 | src: { 45 | files: { 46 | 'dist/angular-workers.min.js': '<%= concat.src.dest %>' 47 | } 48 | } 49 | }, 50 | karma: { 51 | unit: { 52 | options: karmaConfig('karma.conf.js', { 53 | singleRun: true 54 | }) 55 | }, 56 | server: { 57 | options: karmaConfig('karma.conf.js', { 58 | singleRun: false 59 | }) 60 | } 61 | }, 62 | changelog: { 63 | options: { 64 | dest: 'CHANGELOG.md' 65 | } 66 | }, 67 | ngmin: { 68 | src: { 69 | src: '<%= concat.src.dest %>', 70 | dest: '<%= concat.src.dest %>' 71 | } 72 | }, 73 | clean: ['dist/*'] 74 | }); 75 | 76 | grunt.registerTask('default', ['jshint', 'karma:unit']); 77 | grunt.registerTask('test', ['karma:unit']); 78 | grunt.registerTask('test-server', ['karma:server']); 79 | grunt.registerTask('build', ['clean', 'jshint', 'karma:unit', 'concat', 'ngmin', 'uglify']); 80 | }; 81 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 FredrikSandell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # angular-workers 2 | 3 | > A wrapper for web workers in angular 4 | 5 | ###Why? 6 | 7 | Using web workers is somewhat awkward in raw Javascript. Doing it in angular applications even more so. 8 | Each web worker runs in it's own context, and this context is isolated from the angular application. 9 | 10 | ###What does angular-workers do 11 | angular-workers provides an angular service which upon request creates a web worker. 12 | The returned web worker runs it's own angular context which allows it to resolve angular dependencies. 13 | 14 | Mor information about how angular-worker work can be found in this [blog post](http://kindofcode.com/web-workers-in-angular/). 15 | 16 | ##Installation 17 | 18 | install with bower using: 19 | 20 | 21 | ```javascript 22 | bower install angular-workers 23 | ``` 24 | 25 | 26 | ##How to use 27 | 28 | * Depend on the WorkerService. 29 | * Specify the URL to the file containing the angular script by invoking: 30 | 31 | ```javascript 32 | // The URL must be absolute because of the URL blob specification 33 | WorkerService.setAngularUrl(url) 34 | ``` 35 | 36 | * OPTIONALLY: Specify how the web worker is to find any dependencies by invoking: 37 | 38 | ```javascript 39 | // The URL must be absolute because of the URL blob specification 40 | WorkerService.addDependency(serviceName, moduleName, url) 41 | ``` 42 | 43 | * Create create a promise of an angularWorker by invoking: 44 | 45 | ```javascript 46 | var workerPromise = WorkerService.createAngularWorker(['input', 'output' /*additional optional deps*/, 47 | function(input, output /*additional optional deps*/) { 48 | // This contains the worker body. 49 | // The function must be self contained. The function body will be 50 | // converted to source and passed to the worker. 51 | // The input parameter is what will be passed to the worker when 52 | // it is executed. It must be a serializable object. 53 | // The output parameter is a promise and is what the 54 | // worker will return to the main thread. 55 | // All communication from the worker to the main thread is performed 56 | // by resolving, rejecting or notifying the output promise. 57 | // We may optionally depend on other angular services. 58 | // These services can be used just as in the main thread. 59 | // But be aware that no state changes in the angular services in the 60 | // worker are propagates to the main thread. Workers run in fully isolated 61 | // contexts. All communication must be performed through the output parameter. 62 | }]); 63 | ``` 64 | 65 | * When the workerPromise resolves the worker is initialized with it's own angular context and is ready to use. Like so: 66 | 67 | ```javascript 68 | workerPromise.then(function success(angularWorker) { 69 | //The input must be serializable 70 | return angularWorker.run(inputObject); 71 | }, function error(reason) { 72 | //for some reason the worker failed to initialize 73 | //not all browsers support the HTML5 tech that is required, see below. 74 | }).then(function success(result) { 75 | //handle result 76 | }, function error(reason) { 77 | //handle error 78 | }, function notify(update) { 79 | //handle update 80 | }); 81 | ``` 82 | 83 | The same initialized worker can be used many times with different input. 84 | 85 | ##Requirements 86 | 87 | The browser running the angular service needs to support the following: 88 | * [Web Workers](http://caniuse.com/#feat=webworkers) (angular-workers does not use shared workers) 89 | * [Blob URLs](http://caniuse.com/#feat=bloburls), specifically [creating blobs from strings](https://developer.mozilla.org/en-US/docs/Web/API/URL.createObjectURL#Browser_compatibility) 90 | 91 | ##Limitations 92 | 93 | The angular-workers is a wrapper around standard web workers. So all limitations with web workers apply. 94 | * Data sent between the worker and main thread is deep cloned. (angular-workers does not use transferable objects, yet) 95 | This means transferring large object (about >20Mb, [Communicating Large Objects with Web Workers in javascript, Samuel Mendenhall](http://developerblog.redhat.com/2014/05/20/communicating-large-objects-with-web-workers-in-javascript/)) 96 | will cause noticeable delays. Samuel Mendenhall recommends sending the data in chunks. This can be achieved using the notify 97 | in the angular promise. 98 | * There is no DOM in the worker. Other things are missing as well. No global "document" object. The bare minimum of these 99 | have been mocked to allow angular to start in the worker. 100 | * The web worker share no runtime data with the main thread. This is great since it prevents deadlock, starvation and many 101 | other concurrency issues. But it also means that any angular service instance upon which your worker depends is created 102 | in that worker, and not shared with the main thread. One can not communicate data between worker and main thread 103 | by using service states. All communication must be done through the input object and output promise. 104 | * Running in a separate context means the web worker does not share the cookies set in the main thread! If you depend on 105 | cookies for authentication pass these manually to the worker. 106 | 107 | 108 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Fredrik Sandell", 3 | "name": "angular-workers", 4 | "description": "Web workers wrapper for angular", 5 | "version": "1.0.4", 6 | "main": "./dist/angular-workers.js", 7 | "homepage": "http://github.com/FredrikSandell/angular-workers", 8 | "repository": { 9 | "type": "git", 10 | "url": "git://github.com/FredrikSandell/angular-workers" 11 | }, 12 | "ignore": [ 13 | "src", 14 | "test", 15 | ".bowerrc", 16 | ".gitignore", 17 | ".jshintrc", 18 | ".travis.yml", 19 | ".editorconfig", 20 | "bower.json", 21 | "Gruntfile.js", 22 | "package.json", 23 | "karma.conf.js" 24 | ], 25 | "dependencies": { 26 | "angular": "~1.3.*" 27 | }, 28 | "devDependencies": { 29 | "angular-mocks": "~1.3.*" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /dist/angular-workers.js: -------------------------------------------------------------------------------- 1 | angular.module('FredrikSandell.worker-pool', []).service('WorkerService', [ 2 | '$q', 3 | function ($q) { 4 | var that = {}; 5 | var urlToAngular; 6 | var serviceToUrlMap = {}; 7 | var messageIndex = 0; 8 | var indexToDeferMap = {}; 9 | /*jshint laxcomma:true */ 10 | /*jshint quotmark: false */ 11 | var workerTemplate = [ 12 | '', 13 | 'var window = self;', 14 | 'self.history = {};', 15 | 'self.Node = function () {};', 16 | 'var document = {', 17 | ' readyState: \'complete\',', 18 | ' cookie: \'\',', 19 | ' querySelector: function () {},', 20 | ' createElement: function () {', 21 | ' return {', 22 | ' pathname: \'\',', 23 | ' setAttribute: function () {}', 24 | ' };', 25 | ' }', 26 | '};', 27 | 'importScripts(\'\');', 28 | '', 29 | 'angular = window.angular;', 30 | 'var workerApp = angular.module(\'WorkerApp\', []);', 31 | 'workerApp.run([\'$q\', function ($q) {', 32 | ' self.addEventListener(\'message\', function(e) {', 33 | ' var input = e.data.input;', 34 | ' var output = $q.defer();', 35 | ' var id = e.data.id;', 36 | ' var promise = output.promise;', 37 | ' promise.then(function(success) {', 38 | ' self.postMessage({event:\'success\', id: id, data : success});', 39 | ' }, function(reason) {', 40 | ' self.postMessage({event:\'failure\', id: id, data : reason});', 41 | ' }, function(update) {', 42 | ' self.postMessage({event:\'update\', id: id, data : update});', 43 | ' });', 44 | ' ;', 45 | ' });', 46 | ' self.postMessage({event:\'initDone\'});', 47 | '}]);', 48 | 'angular.bootstrap(null, [\'WorkerApp\']);' 49 | ].join('\n'); 50 | that.setAngularUrl = function (urlToAngularJs) { 51 | urlToAngular = urlToAngularJs; 52 | return that; 53 | }; 54 | that.addDependency = function (serviceName, moduleName, url) { 55 | serviceToUrlMap[serviceName] = { 56 | url: url, 57 | moduleName: moduleName 58 | }; 59 | return that; 60 | }; 61 | that.createAngularWorker = function (depFuncList) { 62 | //validate the input 63 | if (!Array.isArray(depFuncList) || depFuncList.length < 3 || typeof depFuncList[depFuncList.length - 1] !== 'function') { 64 | throw new Error('Input needs to be: [\'input\',\'output\'/*optional additional dependencies*/,\n' + ' function(workerInput, deferredOutput /*optional additional dependencies*/)\n' + ' {/*worker body*/}' + ']'); 65 | } 66 | if (typeof urlToAngular !== 'string') { 67 | throw new Error('The url to angular must be defined before worker creation'); 68 | } 69 | var deferred = $q.defer(); 70 | var dependencyMetaData = createDependencyMetaData(extractDependencyList(depFuncList)); 71 | var blobURL = (window.URL ? URL : webkitURL).createObjectURL(new Blob([populateWorkerTemplate(workerFunctionToString(depFuncList[depFuncList.length - 1], dependencyMetaData.workerFuncParamList), dependencyMetaData)], { type: 'application/javascript' })); 72 | var worker = new Worker(blobURL); 73 | //wait for the worker to load resources 74 | worker.addEventListener('message', function (e) { 75 | var callee = arguments.callee; 76 | var eventId = e.data.event; 77 | if (eventId === 'initDone') { 78 | worker.removeEventListener('message', callee, false); 79 | //add event listener for each promise 80 | worker.addEventListener('message', function (e) { 81 | var eventId = e.data.event; 82 | var messageId = e.data.id; 83 | var deferred = indexToDeferMap[messageId]; 84 | if (eventId === 'initDone') { 85 | throw new Error('Received worker initialization in run method. This should already have occurred!'); 86 | } else if (eventId === 'success') { 87 | deferred.resolve(e.data.data); 88 | delete indexToDeferMap[messageId]; 89 | } else if (eventId === 'failure') { 90 | deferred.reject(e.data.data); 91 | delete indexToDeferMap[messageId]; 92 | } else if (eventId === 'update') { 93 | deferred.notify(e.data.data); 94 | } else { 95 | deferred.reject(e); 96 | } 97 | }); 98 | deferred.resolve(buildAngularWorker(worker)); 99 | } else { 100 | deferred.reject(e); 101 | } 102 | }); 103 | return deferred.promise; 104 | }; 105 | function createIncludeStatements(listOfServiceNames) { 106 | var includeString = ''; 107 | angular.forEach(listOfServiceNames, function (serviceName) { 108 | if (serviceToUrlMap[serviceName]) { 109 | includeString += 'importScripts(\'' + serviceToUrlMap[serviceName].url + '\');'; 110 | } 111 | }); 112 | return includeString; 113 | } 114 | function createModuleList(listOfServiceNames) { 115 | var moduleNameList = []; 116 | angular.forEach(listOfServiceNames, function (serviceName) { 117 | if (serviceToUrlMap[serviceName]) { 118 | moduleNameList.push('\'' + serviceToUrlMap[serviceName].moduleName + '\''); 119 | } 120 | }); 121 | return moduleNameList.join(','); 122 | } 123 | function createDependencyMetaData(dependencyList) { 124 | var dependencyServiceNames = dependencyList.filter(function (dep) { 125 | return dep !== 'input' && dep !== 'output' && dep !== '$q'; 126 | }); 127 | var depMetaData = { 128 | dependencies: dependencyServiceNames, 129 | moduleList: createModuleList(dependencyServiceNames), 130 | angularDepsAsStrings: dependencyServiceNames.length > 0 ? ',' + dependencyServiceNames.map(function (dep) { 131 | return '\'' + dep + '\''; 132 | }).join(',') : '', 133 | angularDepsAsParamList: dependencyServiceNames.length > 0 ? ',' + dependencyServiceNames.join(',') : '', 134 | servicesIncludeStatements: createIncludeStatements(dependencyServiceNames) 135 | }; 136 | depMetaData.workerFuncParamList = 'input,output' + depMetaData.angularDepsAsParamList; 137 | return depMetaData; 138 | } 139 | function populateWorkerTemplate(workerFunc, dependencyMetaData) { 140 | return workerTemplate.replace('', urlToAngular).replace('', dependencyMetaData.servicesIncludeStatements).replace('', dependencyMetaData.moduleList).replace('', dependencyMetaData.angularDepsAsStrings).replace('', dependencyMetaData.angularDepsAsParamList).replace('', workerFunc.toString()); 141 | } 142 | var buildAngularWorker = function (initializedWorker) { 143 | var that = {}; 144 | that.worker = initializedWorker; 145 | that.run = function (input) { 146 | var index = messageIndex++; 147 | indexToDeferMap[index] = $q.defer(); 148 | var param = { 149 | id: index, 150 | input: input 151 | }; 152 | initializedWorker.postMessage(param); 153 | return indexToDeferMap[index].promise; 154 | }; 155 | that.terminate = function () { 156 | initializedWorker.terminate(); 157 | }; 158 | return that; 159 | }; 160 | function extractDependencyList(depFuncList) { 161 | return depFuncList.slice(0, depFuncList.length - 1); 162 | } 163 | function workerFunctionToString(func, paramList) { 164 | return '(' + func.toString() + ')(' + paramList + ')'; 165 | } 166 | return that; 167 | } 168 | ]); -------------------------------------------------------------------------------- /dist/angular-workers.min.js: -------------------------------------------------------------------------------- 1 | angular.module("FredrikSandell.worker-pool",[]).service("WorkerService",["$q",function(a){function b(a){var b="";return angular.forEach(a,function(a){j[a]&&(b+="importScripts('"+j[a].url+"');")}),b}function c(a){var b=[];return angular.forEach(a,function(a){j[a]&&b.push("'"+j[a].moduleName+"'")}),b.join(",")}function d(a){var d=a.filter(function(a){return"input"!==a&&"output"!==a&&"$q"!==a}),e={dependencies:d,moduleList:c(d),angularDepsAsStrings:d.length>0?","+d.map(function(a){return"'"+a+"'"}).join(","):"",angularDepsAsParamList:d.length>0?","+d.join(","):"",servicesIncludeStatements:b(d)};return e.workerFuncParamList="input,output"+e.angularDepsAsParamList,e}function e(a,b){return m.replace("",h).replace("",b.servicesIncludeStatements).replace("",b.moduleList).replace("",b.angularDepsAsStrings).replace("",b.angularDepsAsParamList).replace("",a.toString())}function f(a){return a.slice(0,a.length-1)}function g(a,b){return"("+a.toString()+")("+b+")"}var h,i={},j={},k=0,l={},m=["","var window = self;","self.history = {};","self.Node = function () {};","var document = {"," readyState: 'complete',"," cookie: '',"," querySelector: function () {},"," createElement: function () {"," return {"," pathname: '',"," setAttribute: function () {}"," };"," }","};","importScripts('');","","angular = window.angular;","var workerApp = angular.module('WorkerApp', []);","workerApp.run(['$q', function ($q) {"," self.addEventListener('message', function(e) {"," var input = e.data.input;"," var output = $q.defer();"," var id = e.data.id;"," var promise = output.promise;"," promise.then(function(success) {"," self.postMessage({event:'success', id: id, data : success});"," }, function(reason) {"," self.postMessage({event:'failure', id: id, data : reason});"," }, function(update) {"," self.postMessage({event:'update', id: id, data : update});"," });"," ;"," });"," self.postMessage({event:'initDone'});","}]);","angular.bootstrap(null, ['WorkerApp']);"].join("\n");i.setAngularUrl=function(a){return h=a,i},i.addDependency=function(a,b,c){return j[a]={url:c,moduleName:b},i},i.createAngularWorker=function(b){if(!Array.isArray(b)||b.length<3||"function"!=typeof b[b.length-1])throw new Error("Input needs to be: ['input','output'/*optional additional dependencies*/,\n function(workerInput, deferredOutput /*optional additional dependencies*/)\n {/*worker body*/}]");if("string"!=typeof h)throw new Error("The url to angular must be defined before worker creation");var c=a.defer(),i=d(f(b)),j=(window.URL?URL:webkitURL).createObjectURL(new Blob([e(g(b[b.length-1],i.workerFuncParamList),i)],{type:"application/javascript"})),k=new Worker(j);return k.addEventListener("message",function(a){var b=arguments.callee,d=a.data.event;"initDone"===d?(k.removeEventListener("message",b,!1),k.addEventListener("message",function(a){var b=a.data.event,c=a.data.id,d=l[c];if("initDone"===b)throw new Error("Received worker initialization in run method. This should already have occurred!");"success"===b?(d.resolve(a.data.data),delete l[c]):"failure"===b?(d.reject(a.data.data),delete l[c]):"update"===b?d.notify(a.data.data):d.reject(a)}),c.resolve(n(k))):c.reject(a)}),c.promise};var n=function(b){var c={};return c.worker=b,c.run=function(c){var d=k++;l[d]=a.defer();var e={id:d,input:c};return b.postMessage(e),l[d].promise},c.terminate=function(){b.terminate()},c};return i}]); -------------------------------------------------------------------------------- /example/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc overview 5 | * @name fullGridWebworkersApp 6 | * @description 7 | * # fullGridWebworkersApp 8 | * 9 | * Main module of the application. 10 | */ 11 | var app = angular 12 | .module('angular-workers-example', ['FredrikSandell.worker-pool']) 13 | .run(function (WorkerService) { 14 | //WorkerService.setAngularUrl('../bower_components/angular/angular.js'); 15 | WorkerService.setAngularUrl('https://ajax.googleapis.com/ajax/libs/angularjs/1.3.15/angular.min.js'); 16 | //WorkerService.addDependency(serviceName, moduleName, url); 17 | }); 18 | 19 | var workerPromise; 20 | 21 | app.controller("myChartCtrl", function($scope,WorkerService) { 22 | 23 | 24 | 25 | $scope.awesomeThings = ['HTML5 Boilerplate', 'AngularJS', 'Karma']; 26 | $scope.data = {}; 27 | 28 | $scope.data.reply1 = 'a'; 29 | $scope.data.reply2 = 'b'; 30 | $scope.data.reply3 = 'c'; 31 | 32 | $scope.test = function (arg) { 33 | 34 | /** 35 | // This contains the worker body. 36 | // The function must be self contained. 37 | // The function body will be converted to source and passed to the worker. 38 | // The input parameter is what will be passed to the worker when it is executed. It must be a serializable object. 39 | // The output parameter is a promise and is what the worker will return to the main thread. 40 | // All communication from the worker to the main thread is performed by resolving, rejecting or notifying the output promise. 41 | // We may optionally depend on other angular services. These services can be used just as in the main thread. 42 | // But be aware that no state changes in the angular services in the worker are propagates to the main thread. Workers run in fully isolated contexts. 43 | // All communication must be performed through the output parameter. 44 | */ 45 | if (!workerPromise) { 46 | workerPromise = WorkerService.createAngularWorker(['input', 'output', '$http', function (input, output, $http) { 47 | 48 | var i=0; 49 | 50 | var callback = function(i) { 51 | output.notify(i); 52 | i++; 53 | }; 54 | 55 | 56 | //for (var i = 0; i < 10; i++) { callback(i); } 57 | //var intervalID = setInterval(callback(i), 3000); 58 | setInterval(function(){ callback(++i); }, Math.floor((Math.random() * 1000) + 100)); 59 | 60 | //output.resolve(true); 61 | //output.reject(false); 62 | 63 | }]); 64 | } 65 | 66 | workerPromise 67 | .then(function success(angularWorker) { 68 | //The input must be serializable 69 | return angularWorker.run($scope.awesomeThings); 70 | }, function error(reason) { 71 | 72 | console.log('callback error'); 73 | console.log(reason); 74 | 75 | //for some reason the worker failed to initialize 76 | //not all browsers support the HTML5 tech that is required, see below. 77 | }).then(function success(result) { 78 | 79 | console.log('success'); 80 | console.log(result); 81 | 82 | //handle result 83 | }, function error(reason) { 84 | //handle error 85 | console.log('error'); 86 | console.log(reason); 87 | 88 | }, function notify(update) { 89 | //handle update 90 | 91 | $scope.data['reply' + arg] += update + '\n'; 92 | //console.log(arg); 93 | //console.log(update); 94 | }); 95 | 96 | }; 97 | }); 98 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Angular Workers Example 8 | 9 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 24 | 25 | 26 | 27 |
28 | 29 |
30 |

HTML5 WebWorkers with angular-worker

31 |

Using webworkers with angular environment, but in an isolated context

32 |
33 | 34 |
35 |
36 |

Thread 1

37 |

{{data.reply1}}

38 |

39 | Execute! » 40 |

41 |
42 | 43 | 44 |
45 |

Thread 2

46 |

{{data.reply2}}

47 | Execute! » 48 |

49 |
50 | 51 | 52 |
53 |

Thread 3

54 |

{{data.reply3}}

55 | Execute! » 56 |

57 |
58 | 59 |
60 | 61 | 62 |
63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | 'use strict'; 3 | 4 | config.set({ 5 | 6 | // base path, that will be used to resolve files and exclude 7 | basePath: '', 8 | 9 | 10 | // list of files / patterns to load in the browser 11 | files: [ 12 | 'bower_components/angular/angular.js', 13 | 'bower_components/angular-mocks/angular-mocks.js', 14 | 'src/**/*.js', 15 | 'test/**/*Spec.js' 16 | ], 17 | 18 | frameworks: ['jasmine'], 19 | 20 | 21 | // list of files to exclude 22 | exclude: [ 23 | 24 | ], 25 | 26 | 27 | // test results reporter to use 28 | // possible values: 'dots', 'progress', 'junit' 29 | reporters: ['progress'], 30 | 31 | // web server port 32 | port: 9876, 33 | 34 | 35 | // cli runner port 36 | runnerPort: 9100, 37 | 38 | 39 | // enable / disable colors in the output (reporters and logs) 40 | colors: true, 41 | 42 | 43 | // level of logging 44 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 45 | logLevel: config.LOG_INFO, 46 | 47 | 48 | // enable / disable watching file and executing tests whenever any file changes 49 | autoWatch: true, 50 | 51 | 52 | // Start these browsers, currently available: 53 | // - Chrome 54 | // - ChromeCanary 55 | // - Firefox 56 | // - Opera 57 | // - Safari (only Mac) 58 | // - PhantomJS <- does not work, the browser needs web worker support and support for URL blobs. 59 | // - IE (only Windows) 60 | browsers: ['Chrome', 'Firefox'], 61 | 62 | 63 | // If browser does not capture in given timeout [ms], kill it 64 | captureTimeout: 60000, 65 | 66 | 67 | // Continuous Integration mode 68 | // if true, it capture browsers, run tests and exit 69 | singleRun: false 70 | //singleRun: true 71 | }); 72 | }; 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-workers", 3 | "version": "1.0.1", 4 | "description": "Web worker wrapper for angular", 5 | "main": "angular-workers.js", 6 | "scripts": { 7 | "test": "./node_modules/karma/bin/karma start --browsers Firefox --single-run" 8 | }, 9 | "author": { 10 | "name": "Fredrik Sandell" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/FredrikSandell/angular-workers" 15 | }, 16 | "license": "MIT", 17 | "devDependencies": { 18 | "grunt": "~0.4.2", 19 | "grunt-contrib-clean": "~0.5.0", 20 | "grunt-contrib-concat": "~0.3.x", 21 | "grunt-contrib-jshint": "~0.4.x", 22 | "grunt-contrib-uglify": "~0.2.x", 23 | "grunt-contrib-watch": "~0.5.3", 24 | "grunt-conventional-changelog": "~1.0.x", 25 | "grunt-karma": "~0.7.x", 26 | "grunt-ngmin": "~0.0.3", 27 | "jasmine-core": "^2.1.3", 28 | "karma": "~0.12.0", 29 | "karma-chrome-launcher": "^0.1.7", 30 | "karma-firefox-launcher": "^0.1.4", 31 | "karma-jasmine": "^0.3.5", 32 | "load-grunt-tasks": "~0.2.0", 33 | "lodash": "~2.4.x" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/angular-workers.js: -------------------------------------------------------------------------------- 1 | angular.module('FredrikSandell.worker-pool', []) 2 | .service('WorkerService', ['$q', function ($q) { 3 | var that = {}; 4 | var urlToAngular; 5 | var serviceToUrlMap = {}; 6 | var messageIndex = 0; 7 | var indexToDeferMap = {}; 8 | var importURLs = []; 9 | /*jshint laxcomma:true */ 10 | /*jshint quotmark: false */ 11 | var workerTemplate = ["" 12 | // Angular needs a global window object \ 13 | , "var window = self;" 14 | // Skeleton properties to get Angular to load and bootstrap. \ 15 | , "self.history = {};" 16 | , "self.Node = function () {};" 17 | , "var document = {" 18 | , " readyState: 'complete'," 19 | , " cookie: ''," 20 | , " querySelector: function () {}," 21 | , " createElement: function () {" 22 | , " return {" 23 | , " pathname: ''," 24 | , " setAttribute: function () {}" 25 | , " };" 26 | , " }" 27 | , "};" 28 | // Load Angular: must be on same domain as this script \ 29 | , "importScripts('');" 30 | , "" 31 | // Put angular on global scope \ 32 | , "angular = window.angular;" 33 | , "var workerApp = angular.module('WorkerApp', []);" 34 | , "workerApp.run(['$q', function ($q) {" 35 | , " self.addEventListener('message', function(e) {" 36 | , " var input = e.data.input;" 37 | , " var output = $q.defer();" 38 | , " var id = e.data.id;" 39 | , " var promise = output.promise;" 40 | , " promise.then(function(success) {" 41 | , " self.postMessage({event:'success', id: id, data : success});" 42 | , " }, function(reason) {" 43 | , " self.postMessage({event:'failure', id: id, data : reason});" 44 | , " }, function(update) {" 45 | , " self.postMessage({event:'update', id: id, data : update});" 46 | , " });" 47 | , " ;" 48 | , " });" 49 | , " self.postMessage({event:'initDone'});" 50 | , "}]);" 51 | , "angular.bootstrap(null, ['WorkerApp']);"].join("\n"); 52 | 53 | 54 | that.setAngularUrl = function(urlToAngularJs) { 55 | urlToAngular = urlToAngularJs; 56 | return that; 57 | }; 58 | 59 | that.addDependency = function (serviceName, moduleName, url) { 60 | serviceToUrlMap[serviceName] = { 61 | url : url, 62 | moduleName: moduleName 63 | }; 64 | return that; 65 | }; 66 | 67 | // add explicit URLs for importScripts(). Use-case: angular or/and worker code uses global libraries. 68 | that.addImportURL = function (url) { 69 | importURLs.push(url); 70 | }; 71 | 72 | that.createAngularWorker = function (depFuncList) { 73 | //validate the input 74 | 75 | if (!Array.isArray(depFuncList) || 76 | depFuncList.length < 3 || 77 | typeof depFuncList[depFuncList.length - 1] !== 'function') { 78 | throw new Error('Input needs to be: [\'input\',\'output\'\/*optional additional dependencies*\/,\n' + 79 | ' function(workerInput, deferredOutput \/*optional additional dependencies*\/)\n' + 80 | ' {\/*worker body*\/}' + 81 | ']'); 82 | } 83 | if(typeof urlToAngular !== 'string') { 84 | throw new Error('The url to angular must be defined before worker creation'); 85 | } 86 | var deferred = $q.defer(); 87 | 88 | var dependencyMetaData = createDependencyMetaData(extractDependencyList(depFuncList)); 89 | 90 | var blobURL = (window.URL ? URL : webkitURL).createObjectURL(new Blob([ 91 | populateWorkerTemplate( 92 | workerFunctionToString( 93 | depFuncList[depFuncList.length - 1], 94 | dependencyMetaData.workerFuncParamList), 95 | dependencyMetaData 96 | )], { type: 'application/javascript' })); 97 | 98 | 99 | var worker = new Worker(blobURL); 100 | 101 | //wait for the worker to load resources 102 | worker.addEventListener('message', function (e) { 103 | var callee = arguments.callee; 104 | var eventId = e.data.event; 105 | if (eventId === 'initDone') { 106 | worker.removeEventListener('message', callee, false); 107 | //add event listener for each promise 108 | worker.addEventListener('message', function (e) { 109 | var eventId = e.data.event; 110 | var messageId = e.data.id; 111 | var deferred = indexToDeferMap[messageId]; 112 | if (eventId === 'initDone') { 113 | throw new Error('Received worker initialization in run method. This should already have occurred!'); 114 | } else if (eventId === 'success') { 115 | deferred.resolve(e.data.data); 116 | delete indexToDeferMap[messageId]; 117 | } else if (eventId === 'failure') { 118 | deferred.reject(e.data.data); 119 | delete indexToDeferMap[messageId]; 120 | } else if (eventId === 'update') { 121 | deferred.notify(e.data.data); 122 | } else { 123 | deferred.reject(e); 124 | } 125 | }); 126 | deferred.resolve(buildAngularWorker(worker)); 127 | } else { 128 | deferred.reject(e); 129 | } 130 | }); 131 | 132 | return deferred.promise; 133 | }; 134 | 135 | function createIncludeStatements(listOfServiceNames) { 136 | var includeString = ''; 137 | angular.forEach(importURLs, function (url) { 138 | includeString += 'importScripts(\''+url+'\');'; 139 | }); 140 | angular.forEach(listOfServiceNames, function (serviceName) { 141 | if (serviceToUrlMap[serviceName]) { 142 | includeString += 'importScripts(\''+serviceToUrlMap[serviceName].url+'\');'; 143 | } 144 | }); 145 | return includeString; 146 | } 147 | 148 | function createModuleList(listOfServiceNames) { 149 | var moduleNameList = []; 150 | angular.forEach(listOfServiceNames, function (serviceName) { 151 | if (serviceToUrlMap[serviceName]) { 152 | moduleNameList.push('\'' + serviceToUrlMap[serviceName].moduleName + '\''); 153 | } 154 | }); 155 | return moduleNameList.join(','); 156 | } 157 | 158 | function createDependencyMetaData(dependencyList) { 159 | var dependencyServiceNames = dependencyList.filter(function(dep) { 160 | return dep !== 'input' && dep !== 'output' && dep !== '$q'; 161 | }); 162 | var depMetaData = { 163 | dependencies: dependencyServiceNames, 164 | moduleList: createModuleList(dependencyServiceNames), 165 | angularDepsAsStrings: dependencyServiceNames.length > 0 ? ',' + dependencyServiceNames.map(function (dep) { 166 | return '\'' + dep + '\''; 167 | }).join(',') : '', 168 | angularDepsAsParamList: dependencyServiceNames.length > 0 ? ',' + dependencyServiceNames.join(',') : '', 169 | servicesIncludeStatements: createIncludeStatements(dependencyServiceNames) 170 | }; 171 | depMetaData.workerFuncParamList = 'input,output'+depMetaData.angularDepsAsParamList; 172 | return depMetaData; 173 | } 174 | 175 | function populateWorkerTemplate(workerFunc, dependencyMetaData) { 176 | return workerTemplate.replace('', urlToAngular) 177 | .replace('', dependencyMetaData.servicesIncludeStatements) 178 | .replace('', dependencyMetaData.moduleList) 179 | .replace('', dependencyMetaData.angularDepsAsStrings) 180 | .replace('', dependencyMetaData.angularDepsAsParamList) 181 | .replace('', workerFunc.toString()); 182 | } 183 | 184 | var buildAngularWorker = function (initializedWorker) { 185 | var that = {}; 186 | that.worker = initializedWorker; 187 | that.run = function (input) { 188 | var index = messageIndex++; 189 | indexToDeferMap[index] = $q.defer(); 190 | var param = { 191 | id: index, 192 | input: input, 193 | }; 194 | initializedWorker.postMessage(param); 195 | return indexToDeferMap[index].promise; 196 | }; 197 | 198 | that.terminate = function () { 199 | initializedWorker.terminate(); 200 | }; 201 | 202 | return that; 203 | }; 204 | 205 | function extractDependencyList(depFuncList) { 206 | return depFuncList.slice(0, depFuncList.length - 1); 207 | } 208 | 209 | function workerFunctionToString(func, paramList) { 210 | return '('+func.toString() + ')(' + paramList + ')'; 211 | } 212 | 213 | return that; 214 | }]); 215 | -------------------------------------------------------------------------------- /src/test/PiCalculator.js: -------------------------------------------------------------------------------- 1 | angular.module('FredrikSandell.worker-pool-pi-calculator', []).service('PiCalculatorService', ['$q', function ($q) { 2 | var that = {}; 3 | /** 4 | * If this is done correctly really doesn't matter. What we need is a long running task which hogs CPU-time. 5 | * Number of decimals to calculate for PI 6 | * @param num 7 | */ 8 | that.calculatePi = function(num) { 9 | var deferred = $q.defer(); 10 | var pi = 4, top = 4, bot = 3, minus = true; 11 | var startTime = Date.now(); 12 | var somewhatExactPi = next(pi, top, bot, minus, num, deferred); 13 | deferred.resolve({ 14 | pi: somewhatExactPi, 15 | runtime:calculateRuntime(startTime, Date.now()) 16 | }); 17 | return deferred.promise; 18 | }; 19 | 20 | function next(pi, top, bot, minus, num, deferred) { 21 | for (var i = 0; i < num; i++) { 22 | pi += minus ? -(top / bot) : (top / bot); 23 | minus = !minus; 24 | bot += 2; 25 | if(i%1000 === 0) { 26 | deferred.notify(pi); 27 | } 28 | } 29 | return pi; 30 | } 31 | 32 | function calculateRuntime(start,end) { 33 | var total = end - start; 34 | if(total >= 1000){ 35 | total = (total/1000)+'seconds'; 36 | }else{ 37 | total += 'ms'; 38 | } 39 | return total; 40 | } 41 | 42 | return that; 43 | }]); -------------------------------------------------------------------------------- /test/unit/angular-angular-1-4-Spec.js: -------------------------------------------------------------------------------- 1 | describe('FredrikSandell.worker-pool', function () { 2 | 3 | var WorkerService = null; 4 | var rootScope = null; 5 | 6 | beforeEach(function() { 7 | module('FredrikSandell.worker-pool'); 8 | inject(function (_WorkerService_, $rootScope) { 9 | WorkerService = _WorkerService_; 10 | rootScope = $rootScope; 11 | }); 12 | }); 13 | beforeEach(function() { 14 | WorkerService.setAngularUrl('https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.7/angular.js'); 15 | 16 | }); 17 | 18 | it('should be an object', function () { 19 | expect(typeof WorkerService).toBe('object'); 20 | }); 21 | 22 | it('should have a method addDependency()', function () { 23 | expect(WorkerService.addDependency).toBeDefined(); 24 | expect(typeof WorkerService.addDependency).toBe('function'); 25 | }); 26 | 27 | it('should have a method createAngularWorker()', function () { 28 | expect(WorkerService.createAngularWorker).toBeDefined(); 29 | expect(typeof WorkerService.createAngularWorker).toBe('function'); 30 | }); 31 | 32 | it('should have a method setAngularUrl()', function () { 33 | expect(WorkerService.setAngularUrl).toBeDefined(); 34 | expect(typeof WorkerService.setAngularUrl).toBe('function'); 35 | }); 36 | 37 | function waitUntilCompletedToTriggerPromiseResolve(completed, rootScope) { 38 | //must wait before triggering digest loop which resolves of the promises 39 | //worker must be given time to initialize 40 | var checker = setInterval(function(){ 41 | if(completed) { 42 | clearInterval(checker); 43 | } else { 44 | rootScope.$apply(); 45 | } 46 | }, 100); 47 | } 48 | 49 | it('createAngularWorker() should return a valid AngularWorker object', function (done) { 50 | var completed = false; 51 | var worker = WorkerService.createAngularWorker(['input', 'output', function (input, output) { 52 | //the worker method does not matter 53 | //should always return a promise resolving to an initialized worker 54 | }]); 55 | worker.then(function(worker) { 56 | expect(typeof worker).toBe('object'); 57 | expect(typeof worker.run).toBe('function'); 58 | expect(typeof worker.terminate).toBe('function'); 59 | }, function(data) { 60 | expect(true).toBe(false); //initialization should be ok 61 | })['finally'](function() { 62 | done(); 63 | completed = true; 64 | }); 65 | waitUntilCompletedToTriggerPromiseResolve(completed, rootScope); 66 | }); 67 | 68 | 69 | 70 | it('should be possible to pass arguments to worker, send updates from workers, and successfully terminate workers', function (done) { 71 | var completed = false; 72 | var workerPromise = WorkerService.createAngularWorker(['input', 'output', function (input, output) { 73 | /* 74 | This is the body of the worker. This must be self contained. I.e. contain no references to variables 75 | outside of this function body. References to services listed in the dependency list above is of course ok! 76 | The next test provides more details regarding dependencies. 77 | 78 | input, is a required dependency for the worker. It should be an object which is serializable through 79 | invocation of JSON.stringify() 80 | 81 | output is a required dependency for the worker. It is always a promise which is to be used when communicating 82 | results from the worker. Please not that all objects passed to output should be serializable through 83 | JSON.stringify() 84 | */ 85 | output.notify({status: (input.aNumber+2)}); 86 | 87 | output.resolve({result: input.aNumber*3}); 88 | }]); 89 | //for clarity the promises are not chained 90 | var workerExecutionPromise = workerPromise.then(function(worker) { 91 | //once we reach this point, the worker has its own initialized angular context. 92 | return worker.run({aNumber: 3}); //execute the run method on the fully initialized worker 93 | }, function(data) { 94 | expect(true).toBe(false); //initialization should be ok 95 | }); 96 | 97 | workerExecutionPromise.then(function success(data) { 98 | expect(data.result).toBe(9); 99 | },function error(reason) { 100 | expect(true).toBe(false); //worker should not fail in this test 101 | }, function update(data) { 102 | expect(data.status).toBe(5); 103 | })['finally'](function() { 104 | done(); 105 | completed = true; 106 | }); 107 | waitUntilCompletedToTriggerPromiseResolve(completed, rootScope); 108 | }); 109 | 110 | it('should be possible reject promise from worker', function (done) { 111 | var completed = false; 112 | var workerPromise = WorkerService.createAngularWorker(['input', 'output', function (input, output) { 113 | output.reject("very bad"); 114 | }]).then(function(worker) { 115 | return worker.run({aNumber: 3}); //execute the run method on the fully initialized worker 116 | }).then(function success(data) { 117 | expect(true).toBe(false); //worker should not succeed in this test 118 | },function error(reason) { 119 | expect(reason).toBe("very bad"); 120 | })['finally'](function() { 121 | done(); 122 | completed = true; 123 | }); 124 | waitUntilCompletedToTriggerPromiseResolve(completed, rootScope); 125 | }); 126 | 127 | it('should be possible inject angular dependencies in worker', function (done) { 128 | var completed = false; 129 | var workerPromise = WorkerService.createAngularWorker(['input', 'output', '$http', function (input, output, $http) { 130 | //the URL has to be absolute because this is executed based on a blob 131 | output.notify('Getting data from backend'); 132 | $http.get('http://localhost:9876/some/url').then(function success() { 133 | //usually we would parse data here. But we don't have a backend:) 134 | }, function error(reason) { 135 | output.notify('Starting to preccess retreived information in a CPU intensive way'); 136 | output.resolve(100); 137 | }); 138 | }]).then(function(worker) { 139 | return worker.run(); //execute the run method on the fully initialized worker 140 | }).then(function success(data) { 141 | expect(data).toBe(100); //worker should not succeed in this test 142 | })['finally'](function() { 143 | done(); 144 | completed = true; 145 | }); 146 | waitUntilCompletedToTriggerPromiseResolve(completed, rootScope); 147 | }); 148 | 149 | it('should be possible inject custom dependencies in worker', function (done) { 150 | var completed = false; 151 | //the worker runs in its own angular context. It needs to load all dependencies indenpendently of the main 152 | //application. To do this it needs information about how to load the scripts. 153 | //It is important that the URL is absolute! 154 | WorkerService.addDependency( 155 | 'PiCalculatorService', 156 | 'FredrikSandell.worker-pool-pi-calculator', 157 | 'http://localhost:9876/base/src/test/PiCalculator.js' 158 | ); 159 | var workerPromise = WorkerService.createAngularWorker(['input', 'output', 'PiCalculatorService', function (input, output, PiCalculatorService) { 160 | output.notify("starting with accuracy: "+input.accuracy); 161 | output.resolve(PiCalculatorService.calculatePi(input.accuracy)); 162 | }]).then(function(worker) { 163 | return worker.run({accuracy: 100000}); //execute the run method on the fully initialized worker 164 | }, function error(reason) { 165 | expect(false).toBe(true); 166 | }).then(function success(data) { 167 | expect(data.pi).toBe(3.1416026534897203); //worker should not succeed in this test 168 | })['finally'](function() { 169 | done(); 170 | completed = true; 171 | }); 172 | waitUntilCompletedToTriggerPromiseResolve(completed, rootScope); 173 | }); 174 | 175 | }); 176 | -------------------------------------------------------------------------------- /test/unit/angular-angular-1-5-Spec.js: -------------------------------------------------------------------------------- 1 | describe('FredrikSandell.worker-pool', function () { 2 | 3 | var WorkerService = null; 4 | var rootScope = null; 5 | 6 | beforeEach(function() { 7 | module('FredrikSandell.worker-pool'); 8 | inject(function (_WorkerService_, $rootScope) { 9 | WorkerService = _WorkerService_; 10 | rootScope = $rootScope; 11 | }); 12 | }); 13 | beforeEach(function() { 14 | WorkerService.setAngularUrl('https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.7/angular.js'); 15 | 16 | }); 17 | 18 | it('should be an object', function () { 19 | expect(typeof WorkerService).toBe('object'); 20 | }); 21 | 22 | it('should have a method addDependency()', function () { 23 | expect(WorkerService.addDependency).toBeDefined(); 24 | expect(typeof WorkerService.addDependency).toBe('function'); 25 | }); 26 | 27 | it('should have a method createAngularWorker()', function () { 28 | expect(WorkerService.createAngularWorker).toBeDefined(); 29 | expect(typeof WorkerService.createAngularWorker).toBe('function'); 30 | }); 31 | 32 | it('should have a method setAngularUrl()', function () { 33 | expect(WorkerService.setAngularUrl).toBeDefined(); 34 | expect(typeof WorkerService.setAngularUrl).toBe('function'); 35 | }); 36 | 37 | function waitUntilCompletedToTriggerPromiseResolve(completed, rootScope) { 38 | //must wait before triggering digest loop which resolves of the promises 39 | //worker must be given time to initialize 40 | var checker = setInterval(function(){ 41 | if(completed) { 42 | clearInterval(checker); 43 | } else { 44 | rootScope.$apply(); 45 | } 46 | }, 100); 47 | } 48 | 49 | it('createAngularWorker() should return a valid AngularWorker object', function (done) { 50 | var completed = false; 51 | var worker = WorkerService.createAngularWorker(['input', 'output', function (input, output) { 52 | //the worker method does not matter 53 | //should always return a promise resolving to an initialized worker 54 | }]); 55 | worker.then(function(worker) { 56 | expect(typeof worker).toBe('object'); 57 | expect(typeof worker.run).toBe('function'); 58 | expect(typeof worker.terminate).toBe('function'); 59 | }, function(data) { 60 | expect(true).toBe(false); //initialization should be ok 61 | })['finally'](function() { 62 | done(); 63 | completed = true; 64 | }); 65 | waitUntilCompletedToTriggerPromiseResolve(completed, rootScope); 66 | }); 67 | 68 | 69 | 70 | it('should be possible to pass arguments to worker, send updates from workers, and successfully terminate workers', function (done) { 71 | var completed = false; 72 | var workerPromise = WorkerService.createAngularWorker(['input', 'output', function (input, output) { 73 | /* 74 | This is the body of the worker. This must be self contained. I.e. contain no references to variables 75 | outside of this function body. References to services listed in the dependency list above is of course ok! 76 | The next test provides more details regarding dependencies. 77 | 78 | input, is a required dependency for the worker. It should be an object which is serializable through 79 | invocation of JSON.stringify() 80 | 81 | output is a required dependency for the worker. It is always a promise which is to be used when communicating 82 | results from the worker. Please not that all objects passed to output should be serializable through 83 | JSON.stringify() 84 | */ 85 | output.notify({status: (input.aNumber+2)}); 86 | 87 | output.resolve({result: input.aNumber*3}); 88 | }]); 89 | //for clarity the promises are not chained 90 | var workerExecutionPromise = workerPromise.then(function(worker) { 91 | //once we reach this point, the worker has its own initialized angular context. 92 | return worker.run({aNumber: 3}); //execute the run method on the fully initialized worker 93 | }, function(data) { 94 | expect(true).toBe(false); //initialization should be ok 95 | }); 96 | 97 | workerExecutionPromise.then(function success(data) { 98 | expect(data.result).toBe(9); 99 | },function error(reason) { 100 | expect(true).toBe(false); //worker should not fail in this test 101 | }, function update(data) { 102 | expect(data.status).toBe(5); 103 | })['finally'](function() { 104 | done(); 105 | completed = true; 106 | }); 107 | waitUntilCompletedToTriggerPromiseResolve(completed, rootScope); 108 | }); 109 | 110 | it('should be possible reject promise from worker', function (done) { 111 | var completed = false; 112 | var workerPromise = WorkerService.createAngularWorker(['input', 'output', function (input, output) { 113 | output.reject("very bad"); 114 | }]).then(function(worker) { 115 | return worker.run({aNumber: 3}); //execute the run method on the fully initialized worker 116 | }).then(function success(data) { 117 | expect(true).toBe(false); //worker should not succeed in this test 118 | },function error(reason) { 119 | expect(reason).toBe("very bad"); 120 | })['finally'](function() { 121 | done(); 122 | completed = true; 123 | }); 124 | waitUntilCompletedToTriggerPromiseResolve(completed, rootScope); 125 | }); 126 | 127 | it('should be possible inject angular dependencies in worker', function (done) { 128 | var completed = false; 129 | var workerPromise = WorkerService.createAngularWorker(['input', 'output', '$http', function (input, output, $http) { 130 | //the URL has to be absolute because this is executed based on a blob 131 | output.notify('Getting data from backend'); 132 | $http.get('http://localhost:9876/some/url').then(function success() { 133 | //usually we would parse data here. But we don't have a backend:) 134 | }, function error(reason) { 135 | output.notify('Starting to preccess retreived information in a CPU intensive way'); 136 | output.resolve(100); 137 | }); 138 | }]).then(function(worker) { 139 | return worker.run(); //execute the run method on the fully initialized worker 140 | }).then(function success(data) { 141 | expect(data).toBe(100); //worker should not succeed in this test 142 | })['finally'](function() { 143 | done(); 144 | completed = true; 145 | }); 146 | waitUntilCompletedToTriggerPromiseResolve(completed, rootScope); 147 | }); 148 | 149 | it('should be possible inject custom dependencies in worker', function (done) { 150 | var completed = false; 151 | //the worker runs in its own angular context. It needs to load all dependencies indenpendently of the main 152 | //application. To do this it needs information about how to load the scripts. 153 | //It is important that the URL is absolute! 154 | WorkerService.addDependency( 155 | 'PiCalculatorService', 156 | 'FredrikSandell.worker-pool-pi-calculator', 157 | 'http://localhost:9876/base/src/test/PiCalculator.js' 158 | ); 159 | var workerPromise = WorkerService.createAngularWorker(['input', 'output', 'PiCalculatorService', function (input, output, PiCalculatorService) { 160 | output.notify("starting with accuracy: "+input.accuracy); 161 | output.resolve(PiCalculatorService.calculatePi(input.accuracy)); 162 | }]).then(function(worker) { 163 | return worker.run({accuracy: 100000}); //execute the run method on the fully initialized worker 164 | }, function error(reason) { 165 | expect(false).toBe(true); 166 | }).then(function success(data) { 167 | expect(data.pi).toBe(3.1416026534897203); //worker should not succeed in this test 168 | })['finally'](function() { 169 | done(); 170 | completed = true; 171 | }); 172 | waitUntilCompletedToTriggerPromiseResolve(completed, rootScope); 173 | }); 174 | 175 | }); 176 | -------------------------------------------------------------------------------- /test/unit/angular-workersSpec.js: -------------------------------------------------------------------------------- 1 | describe('FredrikSandell.worker-pool', function () { 2 | 3 | var WorkerService = null; 4 | var rootScope = null; 5 | 6 | beforeEach(function() { 7 | module('FredrikSandell.worker-pool'); 8 | inject(function (_WorkerService_, $rootScope) { 9 | WorkerService = _WorkerService_; 10 | rootScope = $rootScope; 11 | }); 12 | }); 13 | beforeEach(function() { 14 | WorkerService.setAngularUrl('http://localhost:9876/base/bower_components/angular/angular.js'); 15 | 16 | }); 17 | 18 | it('should be an object', function () { 19 | expect(typeof WorkerService).toBe('object'); 20 | }); 21 | 22 | it('should have a method addDependency()', function () { 23 | expect(WorkerService.addDependency).toBeDefined(); 24 | expect(typeof WorkerService.addDependency).toBe('function'); 25 | }); 26 | 27 | it('should have a method createAngularWorker()', function () { 28 | expect(WorkerService.createAngularWorker).toBeDefined(); 29 | expect(typeof WorkerService.createAngularWorker).toBe('function'); 30 | }); 31 | 32 | it('should have a method setAngularUrl()', function () { 33 | expect(WorkerService.setAngularUrl).toBeDefined(); 34 | expect(typeof WorkerService.setAngularUrl).toBe('function'); 35 | }); 36 | 37 | function waitUntilCompletedToTriggerPromiseResolve(completed, rootScope) { 38 | //must wait before triggering digest loop which resolves of the promises 39 | //worker must be given time to initialize 40 | var checker = setInterval(function(){ 41 | if(completed) { 42 | clearInterval(checker); 43 | } else { 44 | rootScope.$apply(); 45 | } 46 | }, 100); 47 | } 48 | 49 | it('createAngularWorker() should return a valid AngularWorker object', function (done) { 50 | var completed = false; 51 | var worker = WorkerService.createAngularWorker(['input', 'output', function (input, output) { 52 | //the worker method does not matter 53 | //should always return a promise resolving to an initialized worker 54 | }]); 55 | worker.then(function(worker) { 56 | expect(typeof worker).toBe('object'); 57 | expect(typeof worker.run).toBe('function'); 58 | expect(typeof worker.terminate).toBe('function'); 59 | }, function(data) { 60 | expect(true).toBe(false); //initialization should be ok 61 | })['finally'](function() { 62 | done(); 63 | completed = true; 64 | }); 65 | waitUntilCompletedToTriggerPromiseResolve(completed, rootScope); 66 | }); 67 | 68 | 69 | 70 | it('should be possible to pass arguments to worker, send updates from workers, and successfully terminate workers', function (done) { 71 | var completed = false; 72 | var workerPromise = WorkerService.createAngularWorker(['input', 'output', function (input, output) { 73 | /* 74 | This is the body of the worker. This must be self contained. I.e. contain no references to variables 75 | outside of this function body. References to services listed in the dependency list above is of course ok! 76 | The next test provides more details regarding dependencies. 77 | 78 | input, is a required dependency for the worker. It should be an object which is serializable through 79 | invocation of JSON.stringify() 80 | 81 | output is a required dependency for the worker. It is always a promise which is to be used when communicating 82 | results from the worker. Please not that all objects passed to output should be serializable through 83 | JSON.stringify() 84 | */ 85 | output.notify({status: (input.aNumber+2)}); 86 | 87 | output.resolve({result: input.aNumber*3}); 88 | }]); 89 | //for clarity the promises are not chained 90 | var workerExecutionPromise = workerPromise.then(function(worker) { 91 | //once we reach this point, the worker has its own initialized angular context. 92 | return worker.run({aNumber: 3}); //execute the run method on the fully initialized worker 93 | }, function(data) { 94 | expect(true).toBe(false); //initialization should be ok 95 | }); 96 | 97 | workerExecutionPromise.then(function success(data) { 98 | expect(data.result).toBe(9); 99 | },function error(reason) { 100 | expect(true).toBe(false); //worker should not fail in this test 101 | }, function update(data) { 102 | expect(data.status).toBe(5); 103 | })['finally'](function() { 104 | done(); 105 | completed = true; 106 | }); 107 | waitUntilCompletedToTriggerPromiseResolve(completed, rootScope); 108 | }); 109 | 110 | it('should be possible reject promise from worker', function (done) { 111 | var completed = false; 112 | var workerPromise = WorkerService.createAngularWorker(['input', 'output', function (input, output) { 113 | output.reject("very bad"); 114 | }]).then(function(worker) { 115 | return worker.run({aNumber: 3}); //execute the run method on the fully initialized worker 116 | }).then(function success(data) { 117 | expect(true).toBe(false); //worker should not succeed in this test 118 | },function error(reason) { 119 | expect(reason).toBe("very bad"); 120 | })['finally'](function() { 121 | done(); 122 | completed = true; 123 | }); 124 | waitUntilCompletedToTriggerPromiseResolve(completed, rootScope); 125 | }); 126 | 127 | it('should be possible inject angular dependencies in worker', function (done) { 128 | var completed = false; 129 | var workerPromise = WorkerService.createAngularWorker(['input', 'output', '$http', function (input, output, $http) { 130 | //the URL has to be absolute because this is executed based on a blob 131 | output.notify('Getting data from backend'); 132 | $http.get('http://localhost:9876/some/url').then(function success() { 133 | //usually we would parse data here. But we don't have a backend:) 134 | }, function error(reason) { 135 | output.notify('Starting to preccess retreived information in a CPU intensive way'); 136 | output.resolve(100); 137 | }); 138 | }]).then(function(worker) { 139 | return worker.run(); //execute the run method on the fully initialized worker 140 | }).then(function success(data) { 141 | expect(data).toBe(100); //worker should not succeed in this test 142 | })['finally'](function() { 143 | done(); 144 | completed = true; 145 | }); 146 | waitUntilCompletedToTriggerPromiseResolve(completed, rootScope); 147 | }); 148 | 149 | it('should be possible inject custom dependencies in worker', function (done) { 150 | var completed = false; 151 | //the worker runs in its own angular context. It needs to load all dependencies indenpendently of the main 152 | //application. To do this it needs information about how to load the scripts. 153 | //It is important that the URL is absolute! 154 | WorkerService.addDependency( 155 | 'PiCalculatorService', 156 | 'FredrikSandell.worker-pool-pi-calculator', 157 | 'http://localhost:9876/base/src/test/PiCalculator.js' 158 | ); 159 | var workerPromise = WorkerService.createAngularWorker(['input', 'output', 'PiCalculatorService', function (input, output, PiCalculatorService) { 160 | output.notify("starting with accuracy: "+input.accuracy); 161 | output.resolve(PiCalculatorService.calculatePi(input.accuracy)); 162 | }]).then(function(worker) { 163 | return worker.run({accuracy: 100000}); //execute the run method on the fully initialized worker 164 | }, function error(reason) { 165 | expect(false).toBe(true); 166 | }).then(function success(data) { 167 | expect(data.pi).toBe(3.1416026534897203); //worker should not succeed in this test 168 | })['finally'](function() { 169 | done(); 170 | completed = true; 171 | }); 172 | waitUntilCompletedToTriggerPromiseResolve(completed, rootScope); 173 | }); 174 | 175 | }); 176 | --------------------------------------------------------------------------------