├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── Thread.js ├── ThreadPool.js ├── ThreadQueue.js ├── example ├── index.html └── index.umd.html ├── package-lock.json ├── package.json ├── test └── test.js ├── umd ├── Thread.js ├── ThreadPool.js └── ThreadQueue.js └── webpack.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 4 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = 2 | { 3 | "root": true, 4 | "parserOptions": { 5 | "ecmaVersion": 9, 6 | "ecmaFeatures": { 7 | "jsx": true 8 | }, 9 | "sourceType": "module" 10 | }, 11 | "env": { 12 | "es6": true, 13 | "node": true, 14 | "browser": true 15 | }, 16 | "plugins": [], 17 | "globals": { 18 | "document": false, 19 | "navigator": false, 20 | "window": false 21 | }, 22 | "rules": { 23 | "accessor-pairs": "error", 24 | "arrow-spacing": [ 25 | "error", 26 | { 27 | "before": true, 28 | "after": true 29 | } 30 | ], 31 | "block-spacing": [ 32 | "error", 33 | "always" 34 | ], 35 | "brace-style": [ 36 | "error", 37 | "1tbs", 38 | { 39 | "allowSingleLine": true 40 | } 41 | ], 42 | "camelcase": [ 43 | "error", 44 | { 45 | "properties": "never" 46 | } 47 | ], 48 | "comma-dangle": [ 49 | "error", "always-multiline" 50 | ], 51 | "comma-spacing": [ 52 | "error", 53 | { 54 | "before": false, 55 | "after": true 56 | } 57 | ], 58 | "comma-style": [ 59 | "error", 60 | "last" 61 | ], 62 | "constructor-super": "error", 63 | "curly": [ 64 | "error", 65 | "multi-line" 66 | ], 67 | "dot-location": [ 68 | "error", 69 | "property" 70 | ], 71 | "eol-last": "error", 72 | "eqeqeq": [ 73 | "error", 74 | "always", 75 | { 76 | "null": "ignore" 77 | } 78 | ], 79 | "func-call-spacing": [ 80 | "error", 81 | "never" 82 | ], 83 | "generator-star-spacing": [ 84 | "error", 85 | { 86 | "before": true, 87 | "after": true 88 | } 89 | ], 90 | "handle-callback-err": [ 91 | "error", 92 | "^(err|error)$" 93 | ], 94 | "indent": [ 95 | "error", 96 | 4, 97 | { 98 | "SwitchCase": 1, 99 | "VariableDeclarator": 1, 100 | "outerIIFEBody": 1, 101 | "MemberExpression": 1, 102 | "FunctionDeclaration": { 103 | "parameters": 1, 104 | "body": 1 105 | }, 106 | "FunctionExpression": { 107 | "parameters": 1, 108 | "body": 1 109 | }, 110 | "CallExpression": { 111 | "arguments": 1 112 | }, 113 | "ArrayExpression": 1, 114 | "ObjectExpression": 1, 115 | "ImportDeclaration": 1, 116 | "flatTernaryExpressions": false, 117 | "ignoreComments": false 118 | } 119 | ], 120 | "key-spacing": [ 121 | "error", 122 | { 123 | "beforeColon": false, 124 | "afterColon": true 125 | } 126 | ], 127 | "keyword-spacing": [ 128 | "error", 129 | { 130 | "before": true, 131 | "after": true 132 | } 133 | ], 134 | "new-cap": [ 135 | "error", 136 | { 137 | "newIsCap": true, 138 | "capIsNew": false 139 | } 140 | ], 141 | "new-parens": "error", 142 | "no-array-constructor": "error", 143 | "no-caller": "error", 144 | "no-class-assign": "error", 145 | "no-compare-neg-zero": "error", 146 | "no-cond-assign": "error", 147 | "no-const-assign": "error", 148 | "no-constant-condition": [ 149 | "error", 150 | { 151 | "checkLoops": false 152 | } 153 | ], 154 | "no-control-regex": "error", 155 | "no-debugger": "error", 156 | "no-delete-var": "error", 157 | "no-dupe-args": "error", 158 | "no-dupe-class-members": "error", 159 | "no-dupe-keys": "error", 160 | "no-duplicate-case": "error", 161 | "no-empty-character-class": "error", 162 | "no-empty-pattern": "error", 163 | "no-eval": "error", 164 | "no-ex-assign": "error", 165 | "no-extend-native": "error", 166 | "no-extra-bind": "error", 167 | "no-extra-boolean-cast": "error", 168 | "no-extra-parens": [ 169 | "error", 170 | "functions" 171 | ], 172 | "no-fallthrough": "error", 173 | "no-floating-decimal": "error", 174 | "no-func-assign": "error", 175 | "no-global-assign": "error", 176 | "no-implied-eval": "error", 177 | "no-inner-declarations": [ 178 | "error", 179 | "functions" 180 | ], 181 | "no-invalid-regexp": "error", 182 | "no-irregular-whitespace": "error", 183 | "no-iterator": "error", 184 | "no-label-var": "error", 185 | "no-labels": [ 186 | "error", 187 | { 188 | "allowLoop": false, 189 | "allowSwitch": false 190 | } 191 | ], 192 | "no-mixed-operators": [ 193 | "error", 194 | { 195 | "groups": [ 196 | [ 197 | "==", 198 | "!=", 199 | "===", 200 | "!==", 201 | ">", 202 | ">=", 203 | "<", 204 | "<=" 205 | ], 206 | [ 207 | "&&", 208 | "||" 209 | ], 210 | [ 211 | "in", 212 | "instanceof" 213 | ] 214 | ], 215 | "allowSamePrecedence": true 216 | } 217 | ], 218 | "no-mixed-spaces-and-tabs": "error", 219 | "no-multi-spaces": "error", 220 | "no-multi-str": "error", 221 | "no-multiple-empty-lines": [ 222 | "error", 223 | { 224 | "max": 1, 225 | "maxEOF": 0 226 | } 227 | ], 228 | "no-negated-in-lhs": "error", 229 | "no-new": "error", 230 | "no-new-func": "error", 231 | "no-new-object": "error", 232 | "no-new-require": "error", 233 | "no-new-symbol": "error", 234 | "no-new-wrappers": "error", 235 | "no-obj-calls": "error", 236 | "no-octal": "error", 237 | "no-octal-escape": "error", 238 | "no-path-concat": "error", 239 | "no-proto": "error", 240 | "no-redeclare": "error", 241 | "no-regex-spaces": "error", 242 | "no-return-await": "error", 243 | "no-self-assign": "error", 244 | "no-self-compare": "error", 245 | "no-sequences": "error", 246 | "no-shadow-restricted-names": "error", 247 | "no-sparse-arrays": "error", 248 | "no-tabs": "error", 249 | "no-template-curly-in-string": "error", 250 | "no-this-before-super": "error", 251 | "no-throw-literal": "error", 252 | "no-trailing-spaces": "error", 253 | "no-undef": "error", 254 | "no-undef-init": "error", 255 | "no-unexpected-multiline": "error", 256 | "no-unmodified-loop-condition": "error", 257 | "no-unneeded-ternary": [ 258 | "error", 259 | { 260 | "defaultAssignment": false 261 | } 262 | ], 263 | "no-unreachable": "error", 264 | "no-unsafe-finally": "error", 265 | "no-unsafe-negation": "error", 266 | "no-unused-expressions": [ 267 | "error", 268 | { 269 | "allowShortCircuit": true, 270 | "allowTernary": true, 271 | "allowTaggedTemplates": true 272 | } 273 | ], 274 | "no-unused-vars": [ 275 | "error", 276 | { 277 | "vars": "all", 278 | "args": "none", 279 | "ignoreRestSiblings": true 280 | } 281 | ], 282 | "no-use-before-define": [ 283 | "error", 284 | { 285 | "functions": false, 286 | "classes": false, 287 | "variables": false 288 | } 289 | ], 290 | "no-useless-call": "error", 291 | "no-useless-computed-key": "error", 292 | "no-useless-constructor": "error", 293 | "no-useless-escape": "error", 294 | "no-useless-rename": "error", 295 | "no-useless-return": "error", 296 | "no-whitespace-before-property": "error", 297 | "no-with": "error", 298 | "object-property-newline": [ 299 | "error", 300 | { 301 | "allowMultiplePropertiesPerLine": true 302 | } 303 | ], 304 | "operator-linebreak": [ 305 | "error", 306 | "after", 307 | { 308 | "overrides": { 309 | "?": "before", 310 | ":": "before" 311 | } 312 | } 313 | ], 314 | "padded-blocks": [ 315 | "error", 316 | { 317 | // This doesn't allow functions to be declared as a single line 318 | // https://github.com/eslint/eslint/issues/7145#issuecomment-281882639 319 | // "blocks": "always", 320 | "switches": "always", 321 | "classes": "always" 322 | } 323 | ], 324 | "prefer-promise-reject-errors": "error", 325 | "prefer-const": "error", 326 | "quotes": [ 327 | "error", 328 | "single", 329 | { 330 | "avoidEscape": true, 331 | "allowTemplateLiterals": true 332 | } 333 | ], 334 | "rest-spread-spacing": [ 335 | "error", 336 | "never" 337 | ], 338 | "semi": [ 339 | "error", 340 | "always" 341 | ], 342 | "semi-spacing": [ 343 | "error", 344 | { 345 | "before": false, 346 | "after": true 347 | } 348 | ], 349 | "space-before-blocks": [ 350 | "error", 351 | "always" 352 | ], 353 | "space-before-function-paren": [ 354 | "error", 355 | "never" 356 | ], 357 | "space-in-parens": [ 358 | "error", 359 | "never" 360 | ], 361 | "space-infix-ops": "error", 362 | "space-unary-ops": [ 363 | "error", 364 | { 365 | "words": true, 366 | "nonwords": false 367 | } 368 | ], 369 | "spaced-comment": [ 370 | "error", 371 | "always", 372 | { 373 | "line": { 374 | "markers": [ 375 | "*package", 376 | "!", 377 | "/", 378 | ",", 379 | "=" 380 | ] 381 | }, 382 | "block": { 383 | "balanced": true, 384 | "markers": [ 385 | "*package", 386 | "!", 387 | ",", 388 | ":", 389 | "::", 390 | "flow-include" 391 | ], 392 | "exceptions": [ 393 | "*" 394 | ] 395 | } 396 | } 397 | ], 398 | "symbol-description": "error", 399 | "template-curly-spacing": [ 400 | "error", 401 | "always" 402 | ], 403 | "template-tag-spacing": [ 404 | "error", 405 | "never" 406 | ], 407 | "unicode-bom": [ 408 | "error", 409 | "never" 410 | ], 411 | "use-isnan": "error", 412 | "valid-typeof": [ 413 | "error", 414 | { 415 | "requireStringLiterals": true 416 | } 417 | ], 418 | "wrap-iife": [ 419 | "error", 420 | "any", 421 | { 422 | "functionPrototypeMethods": true 423 | } 424 | ], 425 | "yield-star-spacing": [ 426 | "error", 427 | "both" 428 | ], 429 | "yoda": [ 430 | "error", 431 | "never" 432 | ] 433 | } 434 | } 435 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8.9.4" 4 | script: 5 | - npm run lint 6 | - npm test 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [1.0.6] - 2019-04-03 8 | ### Fixed 9 | - Remove console log from the generated worker. 10 | 11 | ## [1.0.5] - 2018-08-23 12 | ### Fixed 13 | - Bug causing `reject` to not get called on runs canceled before `ready` was true 14 | - Thread not being able to be diposed of immediately 15 | 16 | ## [1.0.4] - 2018-08-04 17 | ### Changes 18 | - Minimize the files included in the package 19 | 20 | ## [1.0.3] - 2017-12-16 21 | ### Fixes 22 | - Fix bug causing class functions to not be able to be passed to the web worker 23 | 24 | ## [1.0.2] - 2017-12-10 25 | ### Fixes 26 | - Add semicolon before self-calling function to avoid semicolon-less call ambiguity 27 | 28 | ## [1.0.1] - 2017-11-26 29 | ### Fixes 30 | - Remove dangling semicolon in `package.json` 31 | 32 | ## [1.0.0] - 2017-11-21 33 | - Initial release 34 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for your interest in contributing to the project! 4 | 5 | Contributions of all kinds are welcome including pull requests, issues, and reports of or links to repos using the project! 6 | 7 | ## Filing Issues 8 | 9 | When submitting a bug report try to include a clear, minimal repro case along with the issue. More information means the problem can be fixed faster and better! 10 | 11 | When submitting a feature request please include a well-defined use case and even better if you include code modeling how the new feature could be used with a proposed API! 12 | 13 | Promote discussion! Let's talk about the change and discuss what the best, most flexible option might be. 14 | 15 | ## Pull Requests 16 | 17 | Keep it simple! Code clean up and linting changes should be submitted as separate PRS from logic changes so the impact to the codebase is clear. 18 | 19 | Keep PRs with logic changes to the essential modifications if possible -- people have to read it! 20 | 21 | Open an issue for discussion first so we can have consensus on the change and be sure to reference the issue that the PR is addressing. 22 | 23 | Keep commit messages descriptive. "Update" and "oops" doesn't tell anyone what happened there! 24 | 25 | Don't modify existing commits when responding to PR comments. New commits make it easier to follow what changed. 26 | 27 | ## Code Style 28 | 29 | Follow the `.editorconfig`, `.babelrc`, `.stylelintrc`, and `.htmlhintrc` style configurations included in the repo to keep the code looking consistent. 30 | 31 | Try to keep code as clear as possible! Code for readability! For example longer, descriptive variable names are preferred to short ones. If a line of code includes a lot of nested statements (even just one or two) consider breaking the line up into multiple variables to improve the clarity of what's happening. 32 | 33 | Include comments describing _why_ a change was made. If code was moved from one part of a function to another then tell what happened and why the change got made so it doesn't get moved back. Comments aren't just for others, they're for your future self, too! 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Garrett Johnson 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # threading-js 2 | 3 | [![npm version](https://img.shields.io/npm/v/threading-js.svg?style=flat-square)](https://www.npmjs.com/package/threading-js) 4 | [![travis build](https://img.shields.io/travis/gkjohnson/threading-js.svg?style=flat-square)](https://travis-ci.org/gkjohnson/threading-js) 5 | [![lgtm code quality](https://img.shields.io/lgtm/grade/javascript/g/gkjohnson/threading-js.svg?style=flat-square&label=code-quality)](https://lgtm.com/projects/g/gkjohnson/threading-js/) 6 | 7 | Small wrapper for browser [Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers) that simplfies running tasks and allows running without having to serve a worker script to the client. 8 | 9 | ## Use 10 | Simple example showing how to use a Thread to interleave two arrays together using a SharedArrayBuffer. Using basic arrays increases the run time due to copying the data. ArrayBuffer ownership can be transferred using the `transferList` parameter in `run` and `postMessage`. 11 | 12 | The function being passed to the thread must be completely self-contained and only reference data in the passed 'context' object or loaded scripts. All data passed into the context object will be stringified to be copied. 13 | 14 | #### With ES6 Imports 15 | 16 | Example [here](./example/index.html) 17 | 18 | ```js 19 | import Thread from '.../node_modules/threading-js/Thread.js' 20 | 21 | // Operation functions 22 | const interleave = (a, b, res) => { 23 | let i = 0 24 | while(true) { 25 | if (i >= a.length || i >= b.length) break 26 | 27 | res[2 * i + 0] = a[i] 28 | res[2 * i + 1] = b[i] 29 | i++ 30 | } 31 | return res 32 | } 33 | 34 | const threadFunc = args => { 35 | const arr1 = args.arr1 36 | const arr2 = args.arr2 37 | const res = args.res 38 | 39 | postMessage('starting') 40 | const data = interleave(arr1, arr2, res) 41 | postMessage('done') 42 | 43 | return data 44 | } 45 | 46 | // Create thread 47 | const thread = new Thread(threadFunc, { interleave }) 48 | 49 | // Create data 50 | const ARRAY_LENGTH = 10000000 51 | const arr1 = new Float32Array(new SharedArrayBuffer(ARRAY_LENGTH * 4)) 52 | const arr2 = new Float32Array(new SharedArrayBuffer(ARRAY_LENGTH * 4)) 53 | const sharedres = new Float32Array(new SharedArrayBuffer(ARRAY_LENGTH * 4 * 2)) 54 | for(let i = 0; i < ARRAY_LENGTH; i++) { 55 | arr1[i] = Math.random() 56 | arr2[i] = Math.random() 57 | } 58 | 59 | // Run the tests 60 | console.time('main thread run') 61 | interleave(arr1, arr2, sharedres) 62 | console.timeEnd('main thread run') 63 | 64 | console.time('initial thread run') 65 | thread 66 | .run({ arr1, arr2, res: sharedres }, log => console.log(log)) 67 | .then(res => { 68 | console.timeEnd('initial thread run') 69 | 70 | console.time('subsequent thread run') 71 | return thread.run({ arr1, arr2, res: sharedres }, log => console.log(log)) 72 | }) 73 | .then(res => { 74 | console.timeEnd('subsequent thread run') 75 | }) 76 | 77 | // main thread run: 30.962158203125ms 78 | // starting 79 | // done 80 | // initial thread run: 111.95703125ms 81 | // starting 82 | // done 83 | // subsequent thread run: 35.179931640625ms 84 | ``` 85 | 86 | #### With UMD 87 | 88 | Example [here](./example/index.umd.html) 89 | 90 | ```html 91 | 92 | 93 | 94 | 95 | 101 | ``` 102 | 103 | #### Getting the Best Performance 104 | 105 | ##### Data Clone Pitfalls 106 | 107 | When basic Javascript objects are transferred between the UI thread and a Web Worker (via `run()` in this library), they are copied using the [Structured Clone Algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm), which introduces a significant overhead that can be so bad that it completely defeats the purpose of using a thread. Using shared or transferred buffers is preferable. 108 | 109 | ##### Transferable Objects and ArrayBuffers 110 | 111 | Some objects can have their [ownership transferred between the threads](https://developer.mozilla.org/en-US/docs/Web/API/Transferable), removing the need for cloning the data and associated overhead. A buffer is transferred using the `run()` function in this library and passing the object into the `transferList` array. Once an object has been transferred it's no longer accessible from the original thread and must be explicitly transferred back using a call to `postMessage()`. If an item is not in the transferList, then it is copied. 112 | 113 | ##### SharedArrayBuffers 114 | 115 | [SharedArrayBuffers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer) are not copied, but don't need to be in the `transferList`, either. These buffers can be read from multiple threads at once making them an ideal vessel for data processing and return objects. Synchronized writes, however, must be accounted for. 116 | 117 | # API 118 | 119 | ## Thread 120 | 121 | ### constructor(threadFunc, context, srcs) 122 | The constructor takes a function to run, a dictionary of context data and functions for use in the thread function, and an array of remote source URLs to load libraries in from. 123 | 124 | `threadFunc` is the function to run in the worker. The value returned by this function will be passed back as the result of the run. `postMessage` can be used in this, as well, to post intermediate results back to the main thread. 125 | 126 | `context` is a shallow dictionary of data or functions to be injected into the web worker scope. 127 | 128 | `srcs` is an array of script URLs to load from. 129 | 130 | ### running 131 | Whether or not the thread is running 132 | 133 | ### ready 134 | Whether or not the thread is ready 135 | 136 | ### run(args, intermediateFunc, transferList) 137 | Runs the thread function with the args value as an argument to the function. 138 | 139 | `intermediateFunc` is a callback to recieve intermediate postMessage results from the function. Use the intermediate postMessage function to transfer items as there's no way to return a list of items to transfer from thread function. 140 | 141 | `transferList` the equivelant of the `postMessage` transfer list argument. Note that items in the transfer list are automatically retured once the run is completed. 142 | 143 | Returns a promise. 144 | 145 | ### cancel() 146 | Cancels the current run and prepares for a new one. 147 | 148 | ### dipose() 149 | Disposes of the Thread data so it can't be used anymore. 150 | 151 | ## ThreadPool 152 | A thread pool for easily running many of the same type of task in parallel. 153 | 154 | ### constructor(capacity, func, context, srcs, options) 155 | Like the thread constructor, but takes a capacity as the first argument. 156 | 157 | **options.initializeImmediately = true**: Creates all the threads immediately instead of lazily creating them so no intialization overhead is incurred. 158 | 159 | ### ready 160 | Whether the pool has inactive threads. 161 | 162 | ### activeThreads 163 | The number of currently inactive threads. 164 | 165 | ### capacity 166 | The total possible number of threads the pool can support. 167 | 168 | ### run(...) 169 | Get and use an available thread to run the requested task. Behaves like `Thread.run`, but returns `null` if there are not threads available in the pool. 170 | 171 | ### dispose() 172 | Dispose of all threads and data. 173 | 174 | ## ThreadQueue 175 | A helper class for creating a job queue and running through the tasks using as many threads to work as the capacity allows. 176 | 177 | ### constructor(...) 178 | Same as the `ThreadPool` constructor. 179 | 180 | ### run(...) 181 | Same as `Thread.run`. 182 | 183 | ### dispose() 184 | Same as `ThreadPool.dispose` 185 | -------------------------------------------------------------------------------- /Thread.js: -------------------------------------------------------------------------------- 1 | // Thread class for running a function in a webworker without 2 | // serving a script from a server 3 | class Thread { 4 | 5 | static funcToString(f) { 6 | 7 | // class functions can't be evaluated once stringified because 8 | // the `function` keyword is needed in front of it, so correct 9 | // that here. Example: 10 | // "funcName() {}" => "function() {}" 11 | return f.toString().replace(/^[^(\s]+\(/, 'function('); 12 | 13 | } 14 | 15 | get running() { 16 | 17 | return !!this._process; 18 | 19 | } 20 | 21 | get ready() { 22 | 23 | return !!this._ready; 24 | 25 | } 26 | 27 | constructor(func, context = {}, srcs = []) { 28 | 29 | if (!(func instanceof Function)) throw new Error(func, ' is not a function'); 30 | 31 | // load the scripts from the network if they're 32 | // not cached already 33 | const scripts = new Array(srcs.length); 34 | const promises = []; 35 | srcs.forEach((s, i) => { 36 | 37 | const script = Thread._getScript(s); 38 | if (script) { 39 | 40 | scripts[i] = script; 41 | 42 | } else { 43 | 44 | const prom = Thread._getScriptPromise(s); 45 | prom.then(text => scripts[i] = text); 46 | promises.push(prom); 47 | 48 | } 49 | 50 | }); 51 | 52 | this._disposed = false; 53 | 54 | Promise 55 | .all(promises) 56 | .then(() => this._initWorker(func, context, scripts)); 57 | 58 | } 59 | 60 | /* Public API */ 61 | // Runs the function on a webworker with the given args 62 | // 'intermediateFunc' is a function that will get run 63 | // when results re posted back to the main thread while 64 | // the function is running 65 | // Returns a promise 66 | run(args, intermediateFunc, transferList) { 67 | 68 | this.cancel(); 69 | if (!this.ready) { 70 | 71 | // queue up the first run if we're not quite ready yet 72 | this._lateRun = () => { 73 | 74 | this._worker.postMessage({ args, transferList }, transferList); 75 | delete this._lateRun; 76 | 77 | }; 78 | 79 | } else { 80 | 81 | this._worker.postMessage({ args, transferList }, transferList); 82 | 83 | } 84 | 85 | return new Promise((resolve, reject) => { 86 | 87 | this._process = { resolve, reject, intermediateFunc }; 88 | 89 | }); 90 | 91 | } 92 | 93 | // Cancels the currently running process 94 | cancel() { 95 | 96 | if (this._process) { 97 | 98 | this._process.reject({ 99 | type: 'cancel', 100 | msg: null, 101 | }); 102 | 103 | this._process = null; 104 | 105 | if (this.ready && this.running) { 106 | 107 | this._worker.terminate(); 108 | this._constructWorker(); 109 | 110 | } 111 | 112 | } 113 | delete this._lateRun; 114 | 115 | } 116 | 117 | // disposes the current thread so it can 118 | // no longer be used 119 | dispose() { 120 | 121 | this.cancel(); 122 | if (this._worker) this._worker.terminate(); 123 | this._ready = false; 124 | this._disposed = true; 125 | 126 | } 127 | 128 | /* Private Functions */ 129 | // initialize the worker and cache the script 130 | // to use in the worker 131 | _initWorker(func, context, scripts) { 132 | 133 | if (this._disposed) return; 134 | 135 | this._cachedScript = ` 136 | // context definition 137 | ${ 138 | Object 139 | .keys(context) 140 | .map(key => { 141 | 142 | // manually stringify functions 143 | const data = context[key]; 144 | const isFunc = data instanceof Function; 145 | let str = null; 146 | if (isFunc) str = Thread.funcToString(data); 147 | else str = JSON.stringify(data); 148 | 149 | return `const ${ key } = ${ str };`; 150 | 151 | }) 152 | .join('\n') 153 | } 154 | 155 | // scripts 156 | ${ scripts.join('\n') } 157 | 158 | // self calling function so the thread function 159 | // doesn't have access to our scope 160 | ;(function(threadFunc) { 161 | 162 | // override the "postMessage" function 163 | const __postMessage = postMessage 164 | postMessage = msg => { 165 | __postMessage({ 166 | type: 'intermediate', 167 | data: msg 168 | }); 169 | }; 170 | 171 | // set the on message function to start a 172 | // thread run 173 | onmessage = e => { 174 | 175 | const res = threadFunc(e.data.args); 176 | const doComplete = data => { 177 | __postMessage({ 178 | type: 'complete', 179 | data: data 180 | }, 181 | e.data.transferList) 182 | }; 183 | 184 | if (res instanceof Promise) res.then(data => doComplete(data)); 185 | else doComplete(res); 186 | }; 187 | })(${ Thread.funcToString(func) }) 188 | `; 189 | 190 | this._constructWorker(); 191 | 192 | } 193 | 194 | // consruct the worker 195 | _constructWorker() { 196 | 197 | // create the blob 198 | const blob = new Blob([this._cachedScript], { type: 'plain/text' }); 199 | const url = URL.createObjectURL(blob); 200 | 201 | // create the worker 202 | this._worker = new Worker(url); 203 | this._worker.onmessage = msg => { 204 | 205 | if (msg.data.type === 'complete') { 206 | 207 | // set the process to null before resolving 208 | // in case you want to run in the resolve function 209 | const pr = this._process; 210 | this._process = null; 211 | pr.resolve(msg.data.data); 212 | 213 | } else if (this._process.intermediateFunc) { 214 | 215 | this._process.intermediateFunc(msg.data.data); 216 | 217 | } 218 | 219 | }; 220 | this._worker.onerror = e => { 221 | 222 | this._process.reject({ type: 'error', msg: e.message }); 223 | this._process = null; 224 | 225 | }; 226 | 227 | // dispose of the blob on the next frame because 228 | // we need to make sure the worker has loaded it 229 | requestAnimationFrame(() => URL.revokeObjectURL(url)); 230 | 231 | this._ready = true; 232 | if (this._lateRun) this._lateRun(); 233 | 234 | } 235 | 236 | } 237 | 238 | // Thrad script cache 239 | Thread._cachedScripts = {}; 240 | Thread._scriptPromises = {}; 241 | 242 | Thread._getScript = src => src in Thread._cachedScripts ? Thread._cachedScripts[src] : null; 243 | Thread._getScriptPromise = src => { 244 | 245 | if (src in Thread._scriptPromises) return Thread._scriptPromises[src]; 246 | 247 | return Thread._scriptPromises[src] = new Promise((res, rej) => { 248 | 249 | fetch(src, { credentials: 'same-origin' }) 250 | .then(data => data.text()) 251 | .then(text => { 252 | 253 | Thread._cachedScripts[src] = text; 254 | res(text); 255 | 256 | }) 257 | .catch(e => { 258 | 259 | console.error(`Could not load script from '${ src }'`); 260 | console.error(e); 261 | 262 | }); 263 | 264 | }); 265 | 266 | }; 267 | 268 | export default Thread; 269 | -------------------------------------------------------------------------------- /ThreadPool.js: -------------------------------------------------------------------------------- 1 | import Thread from './Thread.js'; 2 | 3 | // Thread Pool for creating multiple threads with the same 4 | // function and running them in parallel 5 | class ThreadPool { 6 | 7 | // whether the pool has available threads 8 | get ready() { 9 | 10 | return this._activeThreads < this._capacity; 11 | 12 | } 13 | 14 | get activeThreads() { 15 | 16 | return this._activeThreads; 17 | 18 | } 19 | 20 | get capacity() { 21 | 22 | return this._capacity; 23 | 24 | } 25 | 26 | constructor(capacity, func, context = {}, srcs = [], options = {}) { 27 | 28 | this._capacity = capacity; 29 | this._activeThreads = 0; 30 | this._threads = []; 31 | 32 | this._threadArgs = [func, context, srcs]; 33 | 34 | if (options.initializeImmediately) this._createThreadsUpTo(this.capacity); 35 | 36 | } 37 | 38 | /* Public API */ 39 | run() { 40 | 41 | // Increment the number of running threads up to 42 | // capacity. 43 | this._activeThreads = Math.min(this._activeThreads + 1, this._capacity); 44 | this._createThreadsUpTo(this._activeThreads); 45 | 46 | // Find a thread to run. If we can't find a thread that is 47 | // ready and able to run, we return null 48 | const currThread = this._threads.filter(t => !t.running)[0]; 49 | if (!currThread) return null; 50 | 51 | return new Promise((res, rej) => { 52 | 53 | currThread 54 | .run(...arguments) 55 | .then(d => { 56 | 57 | this._activeThreads--; 58 | res(d); 59 | 60 | }) 61 | .catch(e => { 62 | 63 | this._activeThreads--; 64 | rej(e); 65 | 66 | }); 67 | 68 | }); 69 | 70 | } 71 | 72 | dispose() { 73 | 74 | this._capacity = 0; 75 | this._activeThreads = 0; 76 | this._threads.forEach(t => t.dispose()); 77 | this._threads = []; 78 | 79 | } 80 | 81 | /* Private Functions */ 82 | _createThread() { 83 | 84 | this._threads.push(new Thread(...this._threadArgs)); 85 | 86 | } 87 | 88 | _createThreadsUpTo(count) { 89 | 90 | count = Math.min(count, this.capacity); 91 | if (count > this._threads.length) this._createThread(); 92 | 93 | } 94 | 95 | } 96 | 97 | export default ThreadPool; 98 | -------------------------------------------------------------------------------- /ThreadQueue.js: -------------------------------------------------------------------------------- 1 | import ThreadPool from './ThreadPool.js'; 2 | 3 | // Class to enqueue jobs and run across multiple threads 4 | class ThreadQueue extends ThreadPool { 5 | 6 | get ready() { 7 | 8 | return true; 9 | 10 | } 11 | 12 | /* Public API */ 13 | run() { 14 | 15 | this._queue = this._queue || []; 16 | 17 | // create the job to enqueue 18 | const job = { args: [...arguments], promise: null }; 19 | const pr = new Promise((resolve, reject) => job.promise = { resolve, reject }); 20 | 21 | this._queue.push(job); 22 | this._tryRunQueue(); 23 | return pr; 24 | 25 | } 26 | 27 | dispose() { 28 | 29 | super.dispose(); 30 | this._queue = []; 31 | 32 | } 33 | 34 | /* Private Functions */ 35 | // Try to run the jobs on the queue 36 | _tryRunQueue() { 37 | 38 | // run jobs on the queue on the threadpool is 39 | // saturated 40 | while (super.ready && this._queue.length) { 41 | 42 | const job = this._queue.shift(); 43 | super 44 | .run(...job.args) 45 | .then(d => { 46 | 47 | job.promise.resolve(d); 48 | this._tryRunQueue(); 49 | 50 | }) 51 | .catch(e => { 52 | 53 | job.promise.reject(e); 54 | this._tryRunQueue(); 55 | 56 | }); 57 | 58 | } 59 | 60 | } 61 | 62 | } 63 | 64 | export default ThreadQueue; 65 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /example/index.umd.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "threading-js", 3 | "version": "1.0.6", 4 | "description": "Small wrapper for web workers that allows for running tasks without having to serve a worker script to the client.", 5 | "dependencies": {}, 6 | "devDependencies": { 7 | "cross-env": "^5.1.1", 8 | "eslint": "^5.16.0", 9 | "jest": "^29.7.0", 10 | "jest-cli": "^29.7.0", 11 | "nyc": "^13.3.0", 12 | "puppeteer": "^1.7.0", 13 | "puppeteer-to-istanbul": "^1.2.2", 14 | "static-server": "^2.0.6", 15 | "webpack": "^4.29.6", 16 | "webpack-cli": "^3.3.0" 17 | }, 18 | "files": [ 19 | "Thread.js", 20 | "ThreadPool.js", 21 | "ThreadQueue.js", 22 | "umd/" 23 | ], 24 | "scripts": { 25 | "build": "webpack", 26 | "start": "static-server", 27 | "lint": "eslint *.js", 28 | "test": "jest" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "git+https://github.com/gkjohnson/javascript-thread-runner.git" 33 | }, 34 | "keywords": [ 35 | "javascript", 36 | "performance", 37 | "threads", 38 | "webworker", 39 | "webworkers", 40 | "process", 41 | "subprocess", 42 | "browser", 43 | "thread", 44 | "transfer", 45 | "workers" 46 | ], 47 | "author": "Garrett Johnson ", 48 | "license": "MIT", 49 | "bugs": { 50 | "url": "https://github.com/gkjohnson/javascript-thread-runner/issues" 51 | }, 52 | "homepage": "https://github.com/gkjohnson/javascript-thread-runner#readme" 53 | } 54 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /* global 2 | describe it beforeAll afterAll expect 3 | */ 4 | const puppeteer = require('puppeteer'); 5 | // const pti = require('puppeteer-to-istanbul'); 6 | const path = require('path'); 7 | 8 | let browser = null, page = null; 9 | beforeAll(async() => { 10 | 11 | // TODO: Figure out how to get coverage of the modules in the tests. 12 | // May have to run static server to load the modules. 13 | browser = await puppeteer.launch({ 14 | headless: true, 15 | 16 | // --no-sandbox is required to run puppeteer in Travis. 17 | // https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md#running-puppeteer-on-travis-ci 18 | args: ['--no-sandbox'], 19 | }); 20 | page = await browser.newPage(); 21 | console.log(path.join(__dirname, '../umd/Thead.js')); 22 | await page.addScriptTag({ path: path.join(__dirname, '../umd/Thread.js') }); 23 | await page.addScriptTag({ path: path.join(__dirname, '../umd/ThreadPool.js') }); 24 | await page.addScriptTag({ path: path.join(__dirname, '../umd/ThreadQueue.js') }); 25 | await page.evaluate(() => { 26 | 27 | window.Thread = window.Thread.default; 28 | window.ThreadPool = window.ThreadPool.default; 29 | window.ThreadQueue = window.ThreadQueue.default; 30 | 31 | }); 32 | 33 | await page.coverage.startJSCoverage(); 34 | 35 | page.on('error', e => { throw new Error(e); }); 36 | page.on('pageerror', e => { throw new Error(e); }); 37 | page.on('console', e => { 38 | 39 | if (e.type() === 'error') { 40 | 41 | throw new Error(e.text()); 42 | 43 | } 44 | 45 | }); 46 | 47 | }); 48 | 49 | describe('Thread', () => { 50 | 51 | it('should be able to transfer data to the thread', async() => { 52 | 53 | const res = 54 | await page.evaluate(async() => { 55 | 56 | class TestClass { 57 | 58 | static staticFunc(a, b) { return a + b; } 59 | 60 | constructor(a, b) { 61 | this.a = a; 62 | this.b = b; 63 | } 64 | 65 | instFunc(a, b) { return a + b; } 66 | 67 | } 68 | 69 | /* eslint-disable */ 70 | const thread = 71 | new window.Thread(() => ({ 72 | stringVal, 73 | numberVal, 74 | arrayVal, 75 | objectVal, 76 | 77 | funcVal: funcVal.toString(), 78 | funcRes: funcVal(1, 2), 79 | arrowVal: arrowVal.toString(), 80 | arrowRes: arrowVal(1, 2), 81 | inlineFuncVal: inlineFuncVal.toString(), 82 | inlineFuncRes: inlineFuncVal(1, 2), 83 | 84 | staticFuncVal: staticFuncVal.toString(), 85 | staticFuncRes: staticFuncVal(1, 2), 86 | instFuncVal: instFuncVal.toString(), 87 | instFuncRes: instFuncVal(1, 2), 88 | 89 | classVal: Klass.toString(), 90 | classInst: new Klass(10, 20) 91 | }), { 92 | stringVal: 'test', 93 | numberVal: 100, 94 | arrayVal: [1, 2, 3], 95 | objectVal: { a: 1, b: 2 }, 96 | 97 | funcVal: function(a, b) { return a + b; }, 98 | arrowVal: (a, b) => a + b, 99 | inlineFuncVal(a, b) { return a + b; }, 100 | 101 | staticFuncVal: TestClass.staticFunc, 102 | instFuncVal: (new TestClass().instFunc), 103 | 104 | Klass: TestClass 105 | }); 106 | /* eslint-enable */ 107 | 108 | const res = await thread.run(); 109 | 110 | thread.dispose(); 111 | 112 | return res; 113 | 114 | }); 115 | 116 | expect(res.stringVal).toEqual('test'); 117 | expect(res.numberVal).toEqual(100); 118 | expect(res.arrayVal).toEqual([1, 2, 3]); 119 | expect(res.objectVal).toEqual({ a: 1, b: 2 }); 120 | expect(res.funcVal.replace(/\s+/g, '')).toEqual('function(a,b){returna+b;}'); 121 | expect(res.funcRes).toEqual(3); 122 | expect(res.arrowVal).toEqual('(a, b) => a + b'); 123 | expect(res.arrowRes).toEqual(3); 124 | expect(res.inlineFuncVal.replace(/\s+/g, '')).toEqual('function(a,b){returna+b;}'); 125 | expect(res.inlineFuncRes).toEqual(3); 126 | 127 | expect(res.staticFuncVal.replace(/\s+/g, '')).toEqual('function(a,b){returna+b;}'); 128 | expect(res.staticFuncRes).toEqual(3); 129 | expect(res.instFuncVal.replace(/\s+/g, '')).toEqual('function(a,b){returna+b;}'); 130 | expect(res.instFuncRes).toEqual(3); 131 | 132 | expect(res.classVal.replace(/\s+/g, 'classTestClass{staticstaticFunc(a,b){returna+b;}constructor(a,b){this.a=a;this.b=b;}instFunc(a,b){returna+b;}}')); 133 | expect(res.classInst).toEqual({ a: 10, b: 20 }); 134 | 135 | }); 136 | 137 | it('ArrayBuffers should be transferred back after a thread completes', async() => { 138 | 139 | const res = 140 | await page.evaluate(async() => { 141 | 142 | const ab = new Uint8Array(new ArrayBuffer(3)); 143 | ab[0] = 1; 144 | ab[1] = 2; 145 | ab[2] = 3; 146 | 147 | const thread = new window.Thread(ab => ({ data: [ab[0], ab[1], ab[2]], ab })); 148 | await new Promise(resolve => { 149 | 150 | function waiting() { 151 | 152 | if (thread.ready) resolve(); 153 | else requestAnimationFrame(waiting); 154 | 155 | } 156 | waiting(); 157 | 158 | }); 159 | 160 | const length1 = ab.length; 161 | 162 | const pr = thread.run(ab, null, [ab.buffer]); 163 | const length2 = ab.length; 164 | 165 | const result = await pr; 166 | 167 | const length3 = result.ab.length; 168 | 169 | thread.dispose(); 170 | 171 | return { length1, length2, length3, result: result.data }; 172 | 173 | }); 174 | 175 | expect(res.length1).toEqual(3); 176 | expect(res.length2).toEqual(0); 177 | expect(res.length3).toEqual(3); 178 | expect(res.result).toEqual([1, 2, 3]); 179 | 180 | }); 181 | 182 | it('should call the `intermediateFunc` when postMessage is called', async() => { 183 | 184 | const res = 185 | await page.evaluate(async() => { 186 | 187 | let result = null; 188 | const thread = new window.Thread(() => postMessage(100)); 189 | await thread.run(null, data => result = data); 190 | 191 | thread.dispose(); 192 | 193 | return result; 194 | 195 | }); 196 | 197 | expect(res).toEqual(100); 198 | 199 | }); 200 | 201 | it('`running` should reflect the thread state', async() => { 202 | 203 | const res = 204 | await page.evaluate(async() => { 205 | 206 | const thread = new window.Thread(() => {}); 207 | await new Promise(resolve => { 208 | 209 | function waiting() { 210 | 211 | if (thread.ready) resolve(); 212 | else requestAnimationFrame(waiting); 213 | 214 | } 215 | waiting(); 216 | 217 | }); 218 | 219 | const state1 = thread.running; 220 | const pr = thread.run(); 221 | const state2 = thread.running; 222 | await pr; 223 | const state3 = thread.running; 224 | 225 | thread.dispose(); 226 | 227 | return { state1, state2, state3 }; 228 | 229 | }); 230 | 231 | expect(res.state1).toEqual(false); 232 | expect(res.state2).toEqual(true); 233 | expect(res.state3).toEqual(false); 234 | 235 | }); 236 | 237 | it('should cancel a run if run is called again', async() => { 238 | 239 | const res = 240 | await page.evaluate(async() => { 241 | 242 | const thread = new window.Thread(data => data); 243 | 244 | let canceled = false; 245 | let result = null; 246 | thread.run(1).catch(e => canceled = e.type === 'cancel' && e.msg === null); 247 | result = await thread.run(2); 248 | 249 | thread.dispose(); 250 | 251 | return { result, canceled }; 252 | 253 | }); 254 | 255 | expect(res.result).toEqual(2); 256 | expect(res.canceled).toEqual(true); 257 | 258 | }); 259 | 260 | it('should be able to cancel immediately', async() => { 261 | 262 | const res = 263 | await page.evaluate(async() => { 264 | 265 | const thread = new window.Thread(() => {}); 266 | 267 | const ready1 = thread.ready; 268 | let canceled = false; 269 | const pr = thread.run().catch(e => canceled = e.type === 'cancel' && e.msg === null); 270 | 271 | const running1 = thread.running; 272 | thread.dispose(); 273 | 274 | const ready2 = thread.ready; 275 | const running2 = thread.running; 276 | 277 | await pr; 278 | 279 | return { ready1, ready2, running1, running2, canceled }; 280 | 281 | }); 282 | 283 | expect(res.ready1).toEqual(false); 284 | expect(res.running1).toEqual(true); 285 | expect(res.ready2).toEqual(false); 286 | expect(res.running2).toEqual(false); 287 | expect(res.canceled).toEqual(true); 288 | 289 | }); 290 | 291 | }); 292 | 293 | describe('ThreadPool', () => { 294 | 295 | it('should be able to run concurrent jobs', async() => { 296 | 297 | const res = 298 | await page.evaluate(async() => { 299 | 300 | const pool = new window.ThreadPool(5, data => data); 301 | const res = 302 | await Promise.all( 303 | new Array(5) 304 | .fill() 305 | .map((d, i) => pool.run(i)) 306 | ); 307 | 308 | pool.dispose(); 309 | 310 | return res; 311 | 312 | }); 313 | 314 | expect(res).toEqual([0, 1, 2, 3, 4]); 315 | 316 | }); 317 | 318 | it('should be able to run concurrent jobs up to `capacity`', async() => { 319 | 320 | const res = 321 | await page.evaluate(async() => { 322 | 323 | const pool = new window.ThreadPool(5, data => data); 324 | const activeCount = []; 325 | const readyState = []; 326 | const returnedPromise = []; 327 | const capacity = pool.capacity; 328 | 329 | activeCount.push(pool.activeThreads); 330 | readyState.push(pool.ready); 331 | 332 | Promise.all( 333 | new Array(5) 334 | .fill() 335 | .map(() => { 336 | const pr = pool.run(0); 337 | returnedPromise.push(pr instanceof Promise); 338 | activeCount.push(pool.activeThreads); 339 | readyState.push(pool.ready); 340 | 341 | return pr; 342 | }) 343 | ).catch(() => {}); 344 | 345 | returnedPromise.push(pool.run(0) instanceof Promise); 346 | activeCount.push(pool.activeThreads); 347 | readyState.push(pool.ready); 348 | 349 | pool.dispose(); 350 | 351 | return { activeCount, readyState, returnedPromise, capacity }; 352 | 353 | }); 354 | 355 | expect(res.capacity).toEqual(5); 356 | expect(res.activeCount).toEqual([0, 1, 2, 3, 4, 5, 5]); 357 | expect(res.readyState).toEqual([true, true, true, true, true, false, false]); 358 | expect(res.returnedPromise).toEqual([true, true, true, true, true, false]); 359 | 360 | }); 361 | 362 | }); 363 | 364 | describe('ThreadQueue', () => { 365 | 366 | it('should be able to queue jobs beyoned the capacity', async() => { 367 | 368 | const res = 369 | await page.evaluate(async() => { 370 | 371 | const queue = new window.ThreadQueue(1, data => data); 372 | const res = 373 | await Promise.all( 374 | new Array(100) 375 | .fill() 376 | .map((d, i) => queue.run(i)) 377 | ); 378 | 379 | queue.dispose(); 380 | 381 | return res; 382 | 383 | }); 384 | 385 | expect(res).toEqual(new Array(100).fill().map((d, i) => i)); 386 | 387 | }); 388 | 389 | }); 390 | 391 | afterAll(async() => { 392 | 393 | // const coverage = await page.coverage.stopJSCoverage(); 394 | // const threadCoverage = coverage.filter(o => /Thread\.js$/.test(o.url)); 395 | // pti.write(threadCoverage); 396 | 397 | browser.close(); 398 | 399 | }); 400 | -------------------------------------------------------------------------------- /umd/Thread.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Thread=t():e.Thread=t()}(window,function(){return function(e){var t={};function n(r){if(t[r])return t[r].exports;var s=t[r]={i:r,l:!1,exports:{}};return e[r].call(s.exports,s,s.exports,n),s.l=!0,s.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var s in e)n.d(r,s,function(t){return e[t]}.bind(null,s));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t,n){"use strict";n.r(t);class r{static funcToString(e){return e.toString().replace(/^[^(\s]+\(/,"function(")}get running(){return!!this._process}get ready(){return!!this._ready}constructor(e,t={},n=[]){if(!(e instanceof Function))throw new Error(e," is not a function");const s=new Array(n.length),o=[];n.forEach((e,t)=>{const n=r._getScript(e);if(n)s[t]=n;else{const n=r._getScriptPromise(e);n.then(e=>s[t]=e),o.push(n)}}),this._disposed=!1,Promise.all(o).then(()=>this._initWorker(e,t,s))}run(e,t,n){return this.cancel(),this.ready?this._worker.postMessage({args:e,transferList:n},n):this._lateRun=(()=>{this._worker.postMessage({args:e,transferList:n},n),delete this._lateRun}),new Promise((e,n)=>{this._process={resolve:e,reject:n,intermediateFunc:t}})}cancel(){this._process&&(this._process.reject({type:"cancel",msg:null}),this._process=null,this.ready&&this.running&&(this._worker.terminate(),this._constructWorker())),delete this._lateRun}dispose(){this.cancel(),this._worker&&this._worker.terminate(),this._ready=!1,this._disposed=!0}_initWorker(e,t,n){this._disposed||(this._cachedScript=`\n // context definition\n ${Object.keys(t).map(e=>{const n=t[e];let s=null;return`const ${e} = ${s=n instanceof Function?r.funcToString(n):JSON.stringify(n)};`}).join("\n")}\n\n // scripts\n ${n.join("\n")}\n\n // self calling function so the thread function\n // doesn't have access to our scope\n ;(function(threadFunc) {\n\n // override the "postMessage" function\n const __postMessage = postMessage\n postMessage = msg => {\n __postMessage({\n type: 'intermediate',\n data: msg\n });\n };\n\n // set the on message function to start a\n // thread run\n onmessage = e => {\n\n const res = threadFunc(e.data.args);\n const doComplete = data => {\n __postMessage({\n type: 'complete',\n data: data\n },\n e.data.transferList)\n };\n\n if (res instanceof Promise) res.then(data => doComplete(data));\n else doComplete(res);\n };\n })(${r.funcToString(e)})\n `,this._constructWorker())}_constructWorker(){const e=new Blob([this._cachedScript],{type:"plain/text"}),t=URL.createObjectURL(e);this._worker=new Worker(t),this._worker.onmessage=(e=>{if("complete"===e.data.type){const t=this._process;this._process=null,t.resolve(e.data.data)}else this._process.intermediateFunc&&this._process.intermediateFunc(e.data.data)}),this._worker.onerror=(e=>{this._process.reject({type:"error",msg:e.message}),this._process=null}),requestAnimationFrame(()=>URL.revokeObjectURL(t)),this._ready=!0,this._lateRun&&this._lateRun()}}r._cachedScripts={},r._scriptPromises={},r._getScript=(e=>e in r._cachedScripts?r._cachedScripts[e]:null),r._getScriptPromise=(e=>e in r._scriptPromises?r._scriptPromises[e]:r._scriptPromises[e]=new Promise((t,n)=>{fetch(e,{credentials:"same-origin"}).then(e=>e.text()).then(n=>{r._cachedScripts[e]=n,t(n)}).catch(t=>{console.error(`Could not load script from '${e}'`),console.error(t)})})),t.default=r}])}); -------------------------------------------------------------------------------- /umd/ThreadPool.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("./Thread.js")):"function"==typeof define&&define.amd?define(["./Thread.js"],t):"object"==typeof exports?exports.ThreadPool=t(require("./Thread.js")):e.ThreadPool=t(e.Thread)}(window,function(e){return function(e){var t={};function r(i){if(t[i])return t[i].exports;var a=t[i]={i:i,l:!1,exports:{}};return e[i].call(a.exports,a,a.exports,r),a.l=!0,a.exports}return r.m=e,r.c=t,r.d=function(e,t,i){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:i})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var i=Object.create(null);if(r.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var a in e)r.d(i,a,function(t){return e[t]}.bind(null,a));return i},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=1)}([function(t,r){t.exports=e},function(e,t,r){"use strict";r.r(t);var i=r(0),a=r.n(i);t.default=class{get ready(){return this._activeThreads!e.running)[0];return e?new Promise((t,r)=>{e.run(...arguments).then(e=>{this._activeThreads--,t(e)}).catch(e=>{this._activeThreads--,r(e)})}):null}dispose(){this._capacity=0,this._activeThreads=0,this._threads.forEach(e=>e.dispose()),this._threads=[]}_createThread(){this._threads.push(new a.a(...this._threadArgs))}_createThreadsUpTo(e){(e=Math.min(e,this.capacity))>this._threads.length&&this._createThread()}}}])}); -------------------------------------------------------------------------------- /umd/ThreadQueue.js: -------------------------------------------------------------------------------- 1 | !function(e,r){"object"==typeof exports&&"object"==typeof module?module.exports=r(require("./ThreadPool.js")):"function"==typeof define&&define.amd?define(["./ThreadPool.js"],r):"object"==typeof exports?exports.ThreadQueue=r(require("./ThreadPool.js")):e.ThreadQueue=r(e.ThreadPool)}(window,function(e){return function(e){var r={};function t(u){if(r[u])return r[u].exports;var n=r[u]={i:u,l:!1,exports:{}};return e[u].call(n.exports,n,n.exports,t),n.l=!0,n.exports}return t.m=e,t.c=r,t.d=function(e,r,u){t.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:u})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,r){if(1&r&&(e=t(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var u=Object.create(null);if(t.r(u),Object.defineProperty(u,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)t.d(u,n,function(r){return e[r]}.bind(null,n));return u},t.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(r,"a",r),r},t.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},t.p="",t(t.s=1)}([function(r,t){r.exports=e},function(e,r,t){"use strict";t.r(r);var u=t(0),n=t.n(u);r.default=class extends n.a{get ready(){return!0}run(){this._queue=this._queue||[];const e={args:[...arguments],promise:null},r=new Promise((r,t)=>e.promise={resolve:r,reject:t});return this._queue.push(e),this._tryRunQueue(),r}dispose(){super.dispose(),this._queue=[]}_tryRunQueue(){for(;super.ready&&this._queue.length;){const e=this._queue.shift();super.run(...e.args).then(r=>{e.promise.resolve(r),this._tryRunQueue()}).catch(r=>{e.promise.reject(r),this._tryRunQueue()})}}}}])}); -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | // Build all three files independently 4 | const packages = ['Thread', 'ThreadPool', 'ThreadQueue']; 5 | 6 | module.exports = 7 | packages 8 | .map(pkgName => { 9 | 10 | // This packages library name and 11 | // the file name 12 | const libraryName = pkgName; 13 | const fileName = `${ pkgName }.js`; 14 | 15 | // Push the other packages into the 16 | // externals object 17 | const externals = {}; 18 | packages 19 | .filter(p => pkgName !== p) 20 | .forEach(pkgName => { 21 | 22 | const file = `./${ pkgName }.js`; 23 | externals[file] = { 24 | commonjs2: file, 25 | commonjs: file, 26 | amd: file, 27 | root: pkgName, 28 | }; 29 | 30 | }); 31 | 32 | return { 33 | entry: `./${ fileName }`, 34 | 35 | // Target the same file destination but in the UMD directory 36 | // with a target format of UMD 37 | output: { 38 | path: path.resolve(__dirname, 'umd'), 39 | filename: fileName, 40 | library: libraryName, 41 | libraryTarget: 'umd', 42 | }, 43 | 44 | externals, 45 | }; 46 | 47 | }); 48 | --------------------------------------------------------------------------------