├── .gitignore ├── .travis.yml ├── loader.js ├── .editorconfig ├── demo.html ├── package.json ├── README.md └── src └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | build 4 | .DS_Store 5 | package-lock.json 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | - "10" 5 | git: 6 | depth: 5 -------------------------------------------------------------------------------- /loader.js: -------------------------------------------------------------------------------- 1 | try { 2 | module.exports = require('workerize-loader'); 3 | } 4 | catch (e) { 5 | console.warn("Warning: workerize-loader is not installed."); 6 | module.exports = function() { 7 | throw "To use workerize as a loader, you must install workerize-loader."; 8 | } 9 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [{package.json,.*rc,*.yml}] 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Workerize Demo 5 | 6 | 7 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "workerize", 3 | "version": "0.1.8", 4 | "description": "Run a module in a Web Worker.", 5 | "main": "dist/workerize.js", 6 | "module": "dist/workerize.m.js", 7 | "source": "src/index.js", 8 | "repository": "developit/workerize", 9 | "loader": "./loader.js", 10 | "scripts": { 11 | "build": "microbundle", 12 | "prepublishOnly": "npm run build", 13 | "release": "npm t && git commit -am $npm_package_version && git tag $npm_package_version && git push && git push --tags && npm publish", 14 | "test": "echo \"Error: no test specified\" && exit 0" 15 | }, 16 | "eslintConfig": { 17 | "extends": "eslint-config-developit", 18 | "rules": { 19 | "prefer-spread": 0, 20 | "prefer-rest-params": 0 21 | } 22 | }, 23 | "files": [ 24 | "src", 25 | "dist", 26 | "loader.js" 27 | ], 28 | "keywords": [ 29 | "worker", 30 | "web workers", 31 | "threads" 32 | ], 33 | "author": "Jason Miller (http://jasonformat.com)", 34 | "license": "MIT", 35 | "devDependencies": { 36 | "eslint": "^4.16.0", 37 | "eslint-config-developit": "^1.1.1", 38 | "microbundle": "^0.4.3" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 💖 Using Webpack? You want workerize-loader ➡️ 3 |

4 | 5 | workerize 6 | 7 | # Workerize [![npm](https://img.shields.io/npm/v/workerize.svg?style=flat)](https://www.npmjs.org/package/workerize) [![travis](https://travis-ci.org/developit/workerize.svg?branch=master)](https://travis-ci.org/developit/workerize) 8 | 9 | > Moves a module into a Web Worker, automatically reflecting exported functions as asynchronous proxies. 10 | 11 | - Bundles a tiny, purpose-built RPC implementation into your app 12 | - If exported module methods are already async, signature is unchanged 13 | - Supports synchronous and asynchronous worker functions 14 | - Works beautifully with async/await 15 | - Just **800 bytes** of gzipped ES3 16 | 17 | 18 | ## Install 19 | 20 | ```sh 21 | npm install --save workerize 22 | ``` 23 | 24 | 25 | ### Usage 26 | 27 | Pass either a function or a string containing code. 28 | 29 | **worker.js**: 30 | 31 | ```js 32 | let worker = workerize(` 33 | export function add(a, b) { 34 | // block for half a second to demonstrate asynchronicity 35 | let start = Date.now(); 36 | while (Date.now()-start < 500); 37 | return a + b; 38 | } 39 | `); 40 | 41 | (async () => { 42 | console.log('3 + 9 = ', await worker.add(3, 9)); 43 | console.log('1 + 2 = ', await worker.add(1, 2)); 44 | })(); 45 | ``` 46 | 47 | ### License 48 | 49 | [MIT License](https://oss.ninja/mit/developit/) © [Jason Miller](https://jasonformat.com) 50 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 2 | /** TODO: 3 | * - pooling (+ load balancing by tracking # of open calls) 4 | * - queueing (worth it? sortof free via postMessage already) 5 | * 6 | * @example 7 | * let worker = workerize(` 8 | * export function add(a, b) { 9 | * // block for a quarter of a second to demonstrate asynchronicity 10 | * let start = Date.now(); 11 | * while (Date.now()-start < 250); 12 | * return a + b; 13 | * } 14 | * `); 15 | * (async () => { 16 | * console.log('3 + 9 = ', await worker.add(3, 9)); 17 | * console.log('1 + 2 = ', await worker.add(1, 2)); 18 | * })(); 19 | */ 20 | export default function workerize(code, options) { 21 | let exports = {}; 22 | let exportsObjName = `__xpo${Math.random().toString().substring(2)}__`; 23 | if (typeof code==='function') code = `(${Function.prototype.toString.call(code)})(${exportsObjName})`; 24 | code = toCjs(code, exportsObjName, exports) + `\n(${Function.prototype.toString.call(setup)})(self,${exportsObjName},{})`; 25 | let url = URL.createObjectURL(new Blob([code],{ type: 'text/javascript' })), 26 | worker = new Worker(url, options), 27 | term = worker.terminate, 28 | callbacks = {}, 29 | counter = 0, 30 | i; 31 | worker.kill = signal => { 32 | worker.postMessage({ type: 'KILL', signal }); 33 | setTimeout(worker.terminate); 34 | }; 35 | worker.terminate = () => { 36 | URL.revokeObjectURL(url); 37 | term.call(worker); 38 | }; 39 | worker.call = (method, params) => new Promise( (resolve, reject) => { 40 | let id = `rpc${++counter}`; 41 | callbacks[id] = [resolve, reject]; 42 | worker.postMessage({ type: 'RPC', id, method, params }); 43 | }); 44 | worker.rpcMethods = {}; 45 | setup(worker, worker.rpcMethods, callbacks); 46 | worker.expose = methodName => { 47 | worker[methodName] = function() { 48 | return worker.call(methodName, [].slice.call(arguments)); 49 | }; 50 | }; 51 | for (i in exports) if (!(i in worker)) worker.expose(i); 52 | return worker; 53 | } 54 | 55 | function setup(ctx, rpcMethods, callbacks) { 56 | ctx.addEventListener('message', ({ data }) => { 57 | let id = data.id; 58 | if (data.type!=='RPC' || id==null) return; 59 | if (data.method) { 60 | let method = rpcMethods[data.method]; 61 | if (method==null) { 62 | ctx.postMessage({ type: 'RPC', id, error: 'NO_SUCH_METHOD' }); 63 | } 64 | else { 65 | Promise.resolve() 66 | .then( () => method.apply(null, data.params) ) 67 | .then( result => { ctx.postMessage({ type: 'RPC', id, result }); }) 68 | .catch( err => { ctx.postMessage({ type: 'RPC', id, error: ''+err }); }); 69 | } 70 | } 71 | else { 72 | let callback = callbacks[id]; 73 | if (callback==null) throw Error(`Unknown callback ${id}`); 74 | delete callbacks[id]; 75 | if (data.error) callback[1](Error(data.error)); 76 | else callback[0](data.result); 77 | } 78 | }); 79 | } 80 | 81 | function toCjs(code, exportsObjName, exports) { 82 | code = code.replace(/^(\s*)export\s+default\s+/m, (s, before) => { 83 | exports.default = true; 84 | return `${before}${exportsObjName}.default=`; 85 | }); 86 | code = code.replace(/^(\s*)export\s+((?:async\s*)?function(?:\s*\*)?|const|let|var)(\s+)([a-zA-Z$_][a-zA-Z0-9$_]*)/mg, (s, before, type, ws, name) => { 87 | exports[name] = true; 88 | return `${before}${exportsObjName}.${name}=${type}${ws}${name}`; 89 | }); 90 | return `var ${exportsObjName}={};\n${code}\n${exportsObjName};`; 91 | } 92 | --------------------------------------------------------------------------------